[
  {
    "path": ".cargo/config.toml",
    "content": "[target.aarch64-unknown-linux-gnu]\nlinker = \"aarch64-linux-gnu-gcc\"\n\n[target.arm-unknown-linux-gnueabihf]\nlinker = \"arm-linux-gnueabihf-gcc\"\n\n[target.armv7-unknown-linux-gnueabihf]\nlinker = \"arm-linux-gnueabihf-gcc\"\n"
  },
  {
    "path": ".clippy.toml",
    "content": "msrv = \"1.88.0\"\ncognitive-complexity-threshold = 18\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n[*.rs]\nindent_style = tab\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: extrawurst"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: 'bug'\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Context (please complete the following information):**\n - OS/Distro + Version: [e.g. `macOS 10.15.5`]\n - GitUI Version [e.g. `0.5`]\n - Rust version: [e.g `1.44`]\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: 'feature-request'\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "<!---\nThank you for contributing to GitUI! Please fill out the template below, and remove or add any\ninformation as you feel necessary.\n--->\n\nThis Pull Request fixes/closes #{issue_num}.\n\nIt changes the following:\n-\n-\n\nI followed the checklist:\n- [ ] I added unittests\n- [ ] I ran `make check` without errors\n- [ ] I tested the overall application\n- [ ] I added an appropriate item to the changelog"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n- package-ecosystem: cargo\n  directory: \"/\"\n  schedule:\n    interval: daily\n  open-pull-requests-limit: 10\n  groups:\n    cargo-minor:\n      patterns: [\"*\"]\n      update-types:\n        - 'minor'\n    cargo-patch:\n      patterns: [\"*\"]\n      update-types:\n        - 'patch'\n"
  },
  {
    "path": ".github/stale.yml",
    "content": "# Number of days of inactivity before an issue becomes stale\ndaysUntilStale: 180\n# Number of days of inactivity before a stale issue is closed\ndaysUntilClose: 14\n# Issues with these labels will never be considered stale\nexemptLabels:\n  - pinned\n  - security\n  - nostale\n# Label to use when marking an issue as stale\nstaleLabel: dormant\n# Comment to post when marking an issue as stale. Set to `false` to disable\nmarkComment: >\n  This issue has been automatically marked as stale because it has not had\n  any activity half a year. It will be closed in 14 days if no further activity occurs. Thank you\n  for your contributions.\n# Comment to post when closing a stale issue. Set to `false` to disable\ncloseComment: false\n"
  },
  {
    "path": ".github/workflows/brew.yml",
    "content": "name: brew update\n\non:\n  # only manually\n  workflow_dispatch:\n    inputs:\n      tag-name:\n        required: true\n        description: 'release tag'\n\njobs:\n  update_brew:\n    runs-on: ubuntu-latest\n    steps:\n    - name: Bump homebrew-core formula\n      uses: mislav/bump-homebrew-formula-action@v3\n      env:\n        COMMITTER_TOKEN: ${{ secrets.BREW_TOKEN }}\n      with:\n        formula-name: gitui\n        # https://github.com/mislav/bump-homebrew-formula-action/issues/58\n        formula-path: Formula/g/gitui.rb\n        tag-name: ${{ github.event.inputs.tag-name }}\n"
  },
  {
    "path": ".github/workflows/cd.yml",
    "content": "name: CD\n\non:\n  push:\n    tags:\n      - \"*\"\n  workflow_dispatch:\n\npermissions:\n  contents: write\n\njobs:\n  release:\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-latest, macos-latest, windows-latest, ubuntu-22.04]\n    runs-on: ${{ matrix.os }}\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Get version\n        id: get_version\n        run: echo \"version=${GITHUB_REF/refs\\/tags\\//}\" >> $GITHUB_OUTPUT\n\n      - name: Restore cargo cache\n        uses: Swatinem/rust-cache@v2\n        env:\n          cache-name: ci\n        with:\n          shared-key: ${{ matrix.os }}-${{ env.cache-name }}-stable\n\n      - name: Install Rust\n        uses: dtolnay/rust-toolchain@stable\n        with:\n          components: clippy\n\n      - uses: taiki-e/install-action@nextest\n\n      - name: Build\n        if: matrix.os != 'ubuntu-22.04'\n        env:\n          GITUI_RELEASE: 1\n        run: cargo build\n      - name: Run tests\n        if: matrix.os != 'ubuntu-22.04'\n        run: make test\n      - name: Run clippy\n        if: matrix.os != 'ubuntu-22.04'\n        run: |\n          cargo clean\n          make clippy\n\n      - name: Setup MUSL\n        if: matrix.os == 'ubuntu-latest'\n        run: |\n          rustup target add x86_64-unknown-linux-musl\n          sudo apt-get -qq install musl-tools\n\n      - name: Setup ARM toolchain\n        if: matrix.os == 'ubuntu-22.04'\n        run: |\n          rustup target add aarch64-unknown-linux-gnu\n          rustup target add armv7-unknown-linux-gnueabihf\n          rustup target add arm-unknown-linux-gnueabihf\n\n          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\n          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\n\n          tar xf $GITHUB_WORKSPACE/aarch64.tar.xz\n          tar xf $GITHUB_WORKSPACE/arm.tar.xz\n\n          echo \"$GITHUB_WORKSPACE/gcc-arm-8.2-2018.08-x86_64-aarch64-linux-gnu/bin\" >> $GITHUB_PATH\n          echo \"$GITHUB_WORKSPACE/gcc-arm-8.2-2018.08-x86_64-arm-linux-gnueabihf/bin\" >> $GITHUB_PATH\n\n      - name: Build Release Mac\n        if: matrix.os == 'macos-latest'\n        env:\n          GITUI_RELEASE: 1\n        run: make release-mac\n      - name: Build Release Mac x86\n        if: matrix.os == 'macos-latest'\n        env:\n          GITUI_RELEASE: 1\n        run: |\n          rustup target add x86_64-apple-darwin\n          make release-mac-x86\n      - name: Build Release Linux\n        if: matrix.os == 'ubuntu-latest'\n        env:\n          GITUI_RELEASE: 1\n        run: make release-linux-musl\n      - name: Build Release Win\n        if: matrix.os == 'windows-latest'\n        env:\n          GITUI_RELEASE: 1\n        run: make release-win\n      - name: Build Release Linux ARM\n        if: matrix.os == 'ubuntu-22.04'\n        env:\n          GITUI_RELEASE: 1\n        run: make release-linux-arm\n\n      - name: Set SHA\n        if: matrix.os == 'macos-latest'\n        id: shasum\n        run: |\n          echo sha=\"$(shasum -a 256 ./release/gitui-mac.tar.gz | awk '{printf $1}')\" >> $GITHUB_OUTPUT\n\n      - name: Extract release notes\n        if: matrix.os == 'ubuntu-latest'\n        id: release_notes\n        uses: ffurrer2/extract-release-notes@v2\n\n      - name: Release\n        uses: softprops/action-gh-release@v2\n        with:\n          body: ${{ steps.release_notes.outputs.release_notes }}\n          prerelease: ${{ contains(github.ref, '-') }}\n          files: |\n            ./release/*.tar.gz\n            ./release/*.zip\n            ./release/*.msi\n\n      - name: Bump homebrew-core formula\n        uses: mislav/bump-homebrew-formula-action@v3\n        if: \"matrix.os == 'macos-latest' && !contains(github.ref, '-')\" # skip prereleases\n        env:\n          COMMITTER_TOKEN: ${{ secrets.BREW_TOKEN }}\n        with:\n          formula-name: gitui\n          # https://github.com/mislav/bump-homebrew-formula-action/issues/58\n          formula-path: Formula/g/gitui.rb\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  schedule:\n    - cron: \"0 2 * * *\"\n  push:\n    branches: [\"*\"]\n  pull_request:\n    branches: [master]\n\nenv:\n  CARGO_TERM_COLOR: always\n\njobs:\n  build:\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-latest, macos-latest, windows-latest]\n        rust: [nightly, stable, \"1.88\"]\n    runs-on: ${{ matrix.os }}\n    continue-on-error: ${{ matrix.rust == 'nightly' }}\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Restore cargo cache\n        uses: Swatinem/rust-cache@v2\n        env:\n          cache-name: ci\n        with:\n          shared-key: ${{ matrix.os }}-${{ env.cache-name }}-${{ matrix.rust }}\n\n      - name: MacOS Workaround\n        if: matrix.os == 'macos-latest'\n        run: cargo clean -p serde_derive -p thiserror\n\n      - name: Install Rust\n        uses: dtolnay/rust-toolchain@master\n        with:\n          toolchain: ${{ matrix.rust }}\n          components: clippy\n\n      - name: Override rust toolchain\n        run: rustup override set ${{ matrix.rust }}\n\n      - name: Rustup Show\n        run: rustup show\n\n      - uses: taiki-e/install-action@nextest\n\n      - name: Build Debug\n        run: |\n          cargo build\n\n      - name: Run tests\n        run: make test\n\n      - name: Run clippy\n        run: |\n          make clippy\n\n      - name: Build Release\n        run: make build-release\n\n      - name: Test Install\n        run: cargo install --path \".\" --force --locked\n\n      - name: Binary Size (unix)\n        if: matrix.os != 'windows-latest'\n        run: |\n          ls -l ./target/release/gitui\n\n      - name: Binary Size (win)\n        if: matrix.os == 'windows-latest'\n        run: |\n          ls -l ./target/release/gitui.exe\n\n      - name: Binary dependencies (mac)\n        if: matrix.os == 'macos-latest'\n        run: |\n          otool -L ./target/release/gitui\n\n      - name: Build MSI (windows)\n        if: matrix.os == 'windows-latest'\n        run: |\n          cargo install cargo-wix --version 0.3.3 --locked\n          cargo wix --version\n          cargo wix -p gitui --no-build --nocapture --output ./target/wix/gitui-win.msi\n          ls -l ./target/wix/gitui-win.msi\n\n  build-linux-musl:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        rust: [nightly, stable, \"1.88\"]\n    continue-on-error: ${{ matrix.rust == 'nightly' }}\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Restore cargo cache\n        uses: Swatinem/rust-cache@v2\n        env:\n          cache-name: ci\n        with:\n          key: ubuntu-latest-${{ env.cache-name }}-${{ matrix.rust }}\n\n      - name: Install Rust\n        uses: dtolnay/rust-toolchain@master\n        with:\n          toolchain: ${{ matrix.rust }}\n          targets: x86_64-unknown-linux-musl\n\n      # The build would fail without manually installing the target.\n      # https://github.com/dtolnay/rust-toolchain/issues/83\n      - name: Manually install target\n        run: rustup target add x86_64-unknown-linux-musl\n\n      - name: Override rust toolchain\n        run: rustup override set ${{ matrix.rust }}\n\n      - name: Rustup Show\n        run: rustup show\n\n      - uses: taiki-e/install-action@nextest\n\n      - name: Setup MUSL\n        run: |\n          sudo apt-get -qq install musl-tools\n      - name: Build Debug\n        run: |\n          make build-linux-musl-debug\n          ./target/x86_64-unknown-linux-musl/debug/gitui --version\n      - name: Build Release\n        run: |\n          make build-linux-musl-release\n          ./target/x86_64-unknown-linux-musl/release/gitui --version\n          ls -l ./target/x86_64-unknown-linux-musl/release/gitui\n      - name: Test\n        run: |\n          make test-linux-musl\n      - name: Test Install\n        run: cargo install --path \".\" --force --locked\n\n  build-linux-arm:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        rust: [nightly, stable, \"1.88\"]\n    continue-on-error: ${{ matrix.rust == 'nightly' }}\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Restore cargo cache\n        uses: Swatinem/rust-cache@v2\n        env:\n          cache-name: ci\n        with:\n          key: ubuntu-latest-${{ env.cache-name }}-${{ matrix.rust }}\n\n      - name: Install Rust\n        uses: dtolnay/rust-toolchain@master\n        with:\n          toolchain: ${{ matrix.rust }}\n\n      - name: Override rust toolchain\n        run: rustup override set ${{ matrix.rust }}\n\n      - name: Setup ARM toolchain\n        run: |\n          rustup target add aarch64-unknown-linux-gnu\n          rustup target add armv7-unknown-linux-gnueabihf\n          rustup target add arm-unknown-linux-gnueabihf\n\n          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\n          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\n\n          tar xf $GITHUB_WORKSPACE/aarch64.tar.xz\n          tar xf $GITHUB_WORKSPACE/arm.tar.xz\n\n          echo \"$GITHUB_WORKSPACE/gcc-arm-8.2-2018.08-x86_64-aarch64-linux-gnu/bin\" >> $GITHUB_PATH\n          echo \"$GITHUB_WORKSPACE/gcc-arm-8.2-2018.08-x86_64-arm-linux-gnueabihf/bin\" >> $GITHUB_PATH\n\n      - name: Rustup Show\n        run: rustup show\n\n      - name: Build Debug\n        run: |\n          make build-linux-arm-debug\n      - name: Build Release\n        run: |\n          make build-linux-arm-release\n          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\n\n  build-apple-x86:\n    runs-on: macos-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        rust: [nightly, stable, \"1.88\"]\n    continue-on-error: ${{ matrix.rust == 'nightly' }}\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Restore cargo cache\n        uses: Swatinem/rust-cache@v2\n        env:\n          cache-name: ci\n        with:\n          key: apple-x86-${{ env.cache-name }}-${{ matrix.rust }}\n\n      - name: Install Rust\n        uses: dtolnay/rust-toolchain@master\n        with:\n          toolchain: ${{ matrix.rust }}\n\n      - name: Override rust toolchain\n        run: rustup override set ${{ matrix.rust }}\n\n      - name: Setup target\n        run: rustup target add x86_64-apple-darwin\n\n      - name: Rustup Show\n        run: rustup show\n\n      - name: Build Debug\n        run: |\n          make build-apple-x86-debug\n      - name: Build Release\n        run: |\n          make build-apple-x86-release\n          ls -l ./target/x86_64-apple-darwin/release/gitui\n\n  linting:\n    name: Lints\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Restore cargo cache\n        uses: Swatinem/rust-cache@v2\n        env:\n          cache-name: ci\n        with:\n          key: ubuntu-latest-${{ env.cache-name }}-stable\n\n      - name: Install Rust\n        uses: dtolnay/rust-toolchain@stable\n        with:\n          components: rustfmt\n\n      - run: cargo fmt -- --check\n\n      - name: cargo-sort\n        run: |\n          cargo install cargo-sort --force\n          cargo sort -c -w\n\n      - name: cargo-deny install\n        run: |\n          cargo install --locked cargo-deny\n\n      - name: cargo-deny checks\n        run: |\n          cargo deny check\n\n  udeps:\n    name: udeps\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Restore cargo cache\n        uses: Swatinem/rust-cache@v2\n        env:\n          cache-name: ci\n        with:\n          key: ubuntu-latest-${{ env.cache-name }}-nightly\n\n      - name: Install Rust\n        uses: dtolnay/rust-toolchain@nightly\n\n      - name: build cargo-udeps\n        run: cargo install --git https://github.com/est31/cargo-udeps --locked\n\n      - name: run cargo-udeps\n        run: cargo +nightly udeps --all-targets\n\n  log-test:\n    name: Changelog Test\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Extract release notes\n        id: extract_release_notes\n        uses: ffurrer2/extract-release-notes@v2\n        with:\n          release_notes_file: ./release-notes.txt\n      - uses: actions/upload-artifact@v4\n        with:\n          name: release-notes.txt\n          path: ./release-notes.txt\n\n  test-homebrew:\n    name: Test Homebrew Formula (macOS)\n    runs-on: macos-latest\n    steps:\n      - name: Set up Homebrew\n        uses: Homebrew/actions/setup-homebrew@master\n\n      - name: Install stable Rust\n        uses: actions-rs/toolchain@v1\n        with:\n          toolchain: stable\n\n      - name: Let Homebrew build gitui from source\n        run: brew install --build-from-source gitui\n"
  },
  {
    "path": ".github/workflows/nightly.yml",
    "content": "name: Build Nightly Releases\n\non:\n  schedule:\n    - cron: \"0 3 * * *\"\n  workflow_dispatch:\n\nenv:\n  CARGO_TERM_COLOR: always\n  AWS_BUCKET_NAME: s3://gitui/nightly/\n\njobs:\n  release:\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-latest, macos-latest, windows-latest, ubuntu-22.04]\n    runs-on: ${{ matrix.os }}\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Restore cargo cache\n        uses: Swatinem/rust-cache@v2\n        env:\n          cache-name: ci\n        with:\n          shared-key: ${{ matrix.os }}-${{ env.cache-name }}-stable\n\n      - name: Install Rust\n        uses: dtolnay/rust-toolchain@stable\n        with:\n          components: clippy\n\n      - uses: taiki-e/install-action@nextest\n\n      # ideally we trigger the nightly build/deploy only if the normal nightly CI finished successfully\n      - name: Run tests\n        if: matrix.os != 'ubuntu-22.04'\n        run: make test\n      - name: Run clippy\n        if: matrix.os != 'ubuntu-22.04'\n        run: |\n          cargo clean\n          make clippy\n\n      - name: Setup MUSL\n        if: matrix.os == 'ubuntu-latest'\n        run: |\n          rustup target add x86_64-unknown-linux-musl\n          sudo apt-get -qq install musl-tools\n\n      - name: Setup ARM toolchain\n        if: matrix.os == 'ubuntu-22.04'\n        run: |\n          rustup target add aarch64-unknown-linux-gnu\n          rustup target add armv7-unknown-linux-gnueabihf\n          rustup target add arm-unknown-linux-gnueabihf\n\n          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\n          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\n\n          tar xf $GITHUB_WORKSPACE/aarch64.tar.xz\n          tar xf $GITHUB_WORKSPACE/arm.tar.xz\n\n          echo \"$GITHUB_WORKSPACE/gcc-arm-8.2-2018.08-x86_64-aarch64-linux-gnu/bin\" >> $GITHUB_PATH\n          echo \"$GITHUB_WORKSPACE/gcc-arm-8.2-2018.08-x86_64-arm-linux-gnueabihf/bin\" >> $GITHUB_PATH\n\n      - name: Build Release Mac\n        if: matrix.os == 'macos-latest'\n        run: make release-mac\n      - name: Build Release Mac x86\n        if: matrix.os == 'macos-latest'\n        run: |\n          rustup target add x86_64-apple-darwin\n          make release-mac-x86\n      - name: Build Release Linux\n        if: matrix.os == 'ubuntu-latest'\n        run: make release-linux-musl\n      - name: Build Release Win\n        if: matrix.os == 'windows-latest'\n        run: make release-win\n      - name: Build Release Linux ARM\n        if: matrix.os == 'ubuntu-22.04'\n        run: make release-linux-arm\n\n      - name: Ubuntu 22.04 Upload Artifact\n        if: matrix.os == 'ubuntu-22.04'\n        env:\n          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}\n          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_KEY_SECRET }}\n          AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}\n        run: |\n          aws s3 cp ./release/gitui-linux-armv7.tar.gz $AWS_BUCKET_NAME\n          aws s3 cp ./release/gitui-linux-arm.tar.gz $AWS_BUCKET_NAME\n          aws s3 cp ./release/gitui-linux-aarch64.tar.gz $AWS_BUCKET_NAME\n\n      - name: Ubuntu Latest Upload Artifact\n        if: matrix.os == 'ubuntu-latest'\n        env:\n          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}\n          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_KEY_SECRET }}\n          AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}\n        run: |\n          aws s3 cp ./release/gitui-linux-x86_64.tar.gz $AWS_BUCKET_NAME\n\n      - name: MacOS Upload Artifact\n        if: matrix.os == 'macos-latest'\n        env:\n          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}\n          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_KEY_SECRET }}\n          AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}\n        run: |\n          aws s3 cp ./release/gitui-mac.tar.gz $AWS_BUCKET_NAME\n          aws s3 cp ./release/gitui-mac-x86.tar.gz $AWS_BUCKET_NAME\n\n      - name: Windows Upload Artifact\n        if: matrix.os == 'windows-latest'\n        env:\n          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}\n          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_KEY_SECRET }}\n          AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}\n        run: |\n          aws s3 cp ./release/gitui-win.msi $env:AWS_BUCKET_NAME\n          aws s3 cp ./release/gitui-win.tar.gz $env:AWS_BUCKET_NAME\n"
  },
  {
    "path": ".gitignore",
    "content": "/target\n/release\n.DS_Store\n/.idea/\nflamegraph.svg\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"name\": \"(OSX) Launch\",\n            \"type\": \"lldb\",\n            \"request\": \"launch\",\n            \"program\": \"${workspaceRoot}/target/debug/gitui\",\n            \"args\": [],\n            \"cwd\": \"${workspaceRoot}\",\n        }\n    ]\n}"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"editor.formatOnSave\": true,\n    \"workbench.settings.enableNaturalLanguageSearch\": false,\n}"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## Unreleased\n\n### Changed\n* improve `gitui --version` message [[@hlsxx](https://github.com/hlsxx)] ([#2838](https://github.com/gitui-org/gitui/issues/2838))\n* rust msrv bumped to `1.88`\n\n### Fixed\n* fix extremely slow status loading in large repositories by replacing time-based cache invalidation with generation counter [[@DannyStoll1](https://github.com/DannyStoll1)] ([#2823](https://github.com/gitui-org/gitui/issues/2823))\n* fix panic when renaming or updating remote URL with no remotes configured [[@xvchris](https://github.com/xvchris)] ([#2868](https://github.com/gitui-org/gitui/issues/2868))\n\n## [0.28.0] - 2025-12-14\n\n**discard changes on checkout**\n![discard-changes-on-checkout](assets/discard-changes-on-checkout.png)\n\n**go to line in blame**\n![blame-goto-line](assets/blame-goto-line.png)\n\n### Added\n* support choosing checkout branch method when status is not empty [[@fatpandac](https://github.com/fatpandac)] ([#2404](https://github.com/extrawurst/gitui/issues/2404))\n* support pre-push hook [[@xlai89](https://github.com/xlai89)] ([#1933](https://github.com/extrawurst/gitui/issues/1933))\n* message tab supports pageUp and pageDown  [[@xlai89](https://github.com/xlai89)] ([#2623](https://github.com/extrawurst/gitui/issues/2623))\n* files and status tab support pageUp and pageDown  [[@fatpandac](https://github.com/fatpandac)] ([#1951](https://github.com/extrawurst/gitui/issues/1951))\n* support loading custom syntax highlighting themes from a file [[@acuteenvy](https://github.com/acuteenvy)] ([#2565](https://github.com/gitui-org/gitui/pull/2565))\n* select syntax highlighting theme out of the defaults from syntect [[@vasilismanol](https://github.com/vasilismanol)] ([#1931](https://github.com/extrawurst/gitui/issues/1931))\n* new command-line option to override the default log file path (`--logfile`) [[@acuteenvy](https://github.com/acuteenvy)] ([#2539](https://github.com/gitui-org/gitui/pull/2539))\n* dx: `make check` checks Cargo.toml dependency ordering using `cargo sort` [[@naseschwarz](https://github.com/naseschwarz)]\n* add `use_selection_fg` to theme file to allow customizing selection foreground color [[@Upsylonbare](https://github.com/Upsylonbare)] ([#2515](https://github.com/gitui-org/gitui/pull/2515))\n* add \"go to line\" command for the blame view [[@andrea-berling](https://github.com/andrea-berling)] ([#2262](https://github.com/extrawurst/gitui/pull/2262))\n* add `--file` cli flag to open the files tab with the given file already selected [[@laktak](https://github.com/laktak)] ([#2510](https://github.com/gitui-org/gitui/issues/2510))\n* add the ability to specify a custom keybinding/symbols file via the cli [[@0x61nas](https://github.com/0x61nas)] ([#2731](https://github.com/gitui-org/gitui/pull/2731))\n\n### Changed\n* execute git-hooks directly if possible (on *nix) else use sh instead of bash (without reading SHELL variable) [[@Joshix](https://github.com/Joshix-1)] ([#2483](https://github.com/extrawurst/gitui/pull/2483))\n* improve error messages [[@acuteenvy](https://github.com/acuteenvy)] ([#2617](https://github.com/gitui-org/gitui/pull/2617))\n* improve syntax highlighting file detection [[@acuteenvy](https://github.com/acuteenvy)] ([#2524](https://github.com/extrawurst/gitui/pull/2524))\n* after commit: jump back to unstaged area [[@tommady](https://github.com/tommady)] ([#2476](https://github.com/extrawurst/gitui/issues/2476))\n* the default key to close the commit error message popup is now the Escape key [[@wessamfathi](https://github.com/wessamfathi)] ([#2552](https://github.com/extrawurst/gitui/issues/2552))\n* use OSC52 copying in case other methods fail [[@naseschwarz](https://github.com/naseschwarz)] ([#2366](https://github.com/gitui-org/gitui/issues/2366))\n* push: respect `branch.*.merge` when push default is upstream [[@vlad-anger](https://github.com/vlad-anger)] ([#2542](https://github.com/gitui-org/gitui/pull/2542))\n* set the terminal title to `gitui ({repo_path})` [[@acuteenvy](https://github.com/acuteenvy)] ([#2462](https://github.com/gitui-org/gitui/issues/2462))\n* respect `.mailmap` [[@acuteenvy](https://github.com/acuteenvy)] ([#2406](https://github.com/gitui-org/gitui/issues/2406))\n* use `gitoxide` for `get_tags` [[@cruessler](https://github.com/cruessler)] ([#2664](https://github.com/gitui-org/gitui/issues/2664))\n* increase MSRV to 1.82\n\n### Fixes\n* resolve `core.hooksPath` relative to `GIT_WORK_TREE` [[@naseschwarz](https://github.com/naseschwarz)] ([#2571](https://github.com/gitui-org/gitui/issues/2571))\n* yanking commit ranges no longer generates incorrect dotted range notations, but lists each individual commit [[@naseschwarz](https://github.com/naseschwarz)] ([#2576](https://github.com/gitui-org/gitui/issues/2576))\n* print slightly nicer errors when failing to create a directory [[@linkmauve](https://github.com/linkmauve)] ([#2728](https://github.com/gitui-org/gitui/pull/2728))\n* when the terminal is insufficient to display all the commands, the cmdbar_bg configuration color does not fully take effect. ([#2347](https://github.com/extrawurst/gitui/issues/2347))\n* disable blame and history popup keybinds for untracked files [[@kpbaks](https://github.com/kpbaks)] ([#2489](https://github.com/gitui-org/gitui/pull/2489))\n* overwrites committer on amend of unsigned commits [[@cruessler](https://github.com/cruessler)] ([#2784](https://github.com/gitui-org/gitui/issues/2784))\n* Updated project links to point to `gitui-org` instead of `extrawurst`  [[@vasleymus](https://github.com/vasleymus)] ([#2538](https://github.com/gitui-org/gitui/pull/2538))\n\n## [0.27.0] - 2025-01-14\n\n**new: manage remotes**\n\n![add-remote](assets/add-remote.png)\n\n### Breaking Changes\n* use default shell instead of bash on Unix-like OS [[@yerke](https://github.com/yerke)] ([#2343](https://github.com/gitui-org/gitui/pull/2343))\n\n### Added\n* add popups for viewing, adding, updating and removing remotes [[@robin-thoene](https://github.com/robin-thoene)] ([#2172](https://github.com/gitui-org/gitui/issues/2172))\n* support for `Copy Path` action in WSL [[@johnDeSilencio](https://github.com/johnDeSilencio)] ([#2413](https://github.com/gitui-org/gitui/pull/2413))\n* help popup scrollbar [[@wugeer](https://github.com/wugeer)] ([#2388](https://github.com/gitui-org/gitui/pull/2388))\n\n### Fixes\n* respect env vars like `GIT_CONFIG_GLOBAL` ([#2298](https://github.com/gitui-org/gitui/issues/2298))\n* Set `CREATE_NO_WINDOW` flag when executing Git hooks on Windows ([#2371](https://github.com/gitui-org/gitui/pull/2371))\n\n## [0.26.3] - 2024-06-02\n\n### Breaking Changes\n\n#### Theme file format\n\n**note:** this actually applied to the previous release already: `0.26.2`\n\nRatatui (upstream terminal rendering crate) changed its serialization format for Colors. So the theme files have to be adjusted.\n\n`selection_fg: Some(White)` -> `selection_fg: Some(\"White\")`\n\nbut this also allows us now to define colors in the common hex format:\n\n`selection_fg: Some(Rgb(0,255,0))` -> `selection_fg: Some(\"#00ff00\")`\n\nCheckout [THEMES.md](./THEMES.md) for more info.\n\n### Added\n* due to github runner changes, the regular mac build is now arm64, so we added support for intel x86 apple build in nightlies and releases (via separate artifact)\n* support `BUILD_GIT_COMMIT_ID` enabling builds from `git archive` generated source tarballs or other outside a git repo [[@alerque](https://github.com/alerque)] ([#2187](https://github.com/gitui-org/gitui/pull/2187))\n\n### Fixes\n* update yanked dependency to `libc` to fix building with `--locked`.\n* document breaking change in theme file format.\n\n## [0.26.2] - 2024-04-17\n\n**note:** this release introduced a breaking change documented in the following release: `0.26.3`\n\n### Fixes\n* fix `cargo install` without `--locked` ([#2098](https://github.com/gitui-org/gitui/issues/2098))\n* respect configuration for remote when fetching (also applies to pulling) [[@cruessler](https://github.com/cruessler)] ([#1093](https://github.com/gitui-org/gitui/issues/1093))\n* add `:` character to sign-off trailer to comply with Conventinoal Commits standard [@semioticrobotic](https://github.com/semioticrobotic) ([#2196](https://github.com/gitui-org/gitui/issues/2196))\n\n### Added\n* support overriding `build_date` for [reproducible builds](https://reproducible-builds.org/) [[@bmwiedemann](https://github.com/bmwiedemann)] ([#2202](https://github.com/gitui-org/gitui/pull/2202))\n\n## [0.26.0+1] - 2024-04-14\n\n**0.26.1**\nthis release has no changes to `0.26.0` but provides windows binaries that were missing before.\n\n**commit signing**\n\n![signing](assets/gitui-signing.png)\n\n### Added\n* sign commits using openpgp [[@hendrikmaus](https://github.com/hendrikmaus)] ([#97](https://github.com/gitui-org/gitui/issues/97))\n* support ssh commit signing (when `user.signingKey` and `gpg.format = ssh` of gitconfig are set; ssh-agent isn't yet supported)  [[@yanganto](https://github.com/yanganto)] ([#1149](https://github.com/gitui-org/gitui/issues/1149))\n* provide nightly builds (see [NIGHTLIES.md](./NIGHTLIES.md)) ([#2083](https://github.com/gitui-org/gitui/issues/2083))\n* more version info in `gitui -V` and `help popup` (including git hash)\n* support `core.commitChar` filtering [[@concelare](https://github.com/concelare)] ([#2136](https://github.com/gitui-org/gitui/issues/2136))\n* allow reset in branch popup ([#2170](https://github.com/gitui-org/gitui/issues/2170))\n* respect configuration for remote when pushing [[@cruessler](https://github.com/cruessler)] ([#2156](https://github.com/gitui-org/gitui/issues/2156))\n\n### Changed\n* Make info and error message popups scrollable [[@MichaelAug](https://github.com/MichaelAug)] ([#1138](https://github.com/gitui-org/gitui/issues/1138))\n* clarify `x86_64` linux binary in artifact names: `gitui-linux-x86_64.tar.gz` (formerly known as `musl`) ([#2148](https://github.com/gitui-org/gitui/issues/2148))\n\n### Fixes\n* add syntax highlighting support for more file types, e.g. Typescript, TOML, etc. [[@martihomssoler](https://github.com/martihomssoler)] ([#2005](https://github.com/gitui-org/gitui/issues/2005))\n* windows release deployment was broken (reason for release `0.26.1`) [218d739](https://github.com/gitui-org/gitui/commit/218d739b035a034b7bf547629d24787909f467bf)\n\n## [0.25.2] - 2024-03-22\n\n### Fixes\n* blame sometimes crashed due to new syntax highlighting [[@tdtrung17693](https://github.com/tdtrung17693)] ([#2130](https://github.com/gitui-org/gitui/issues/2130))\n* going to file tree view at certin commit from the commit-details view broke [[@martihomssoler](https://github.com/martihomssoler)] ([#2114](https://github.com/gitui-org/gitui/issues/2114))\n* `0.25` broke creating annotated tags ([#2126](https://github.com/gitui-org/gitui/issues/2126))\n\n### Changed\n* re-enable clippy `missing_const_for_fn` linter warning and added const to functions where applicable ([#2116](https://github.com/gitui-org/gitui/issues/2116))\n\n## [0.25.1] - 2024-02-23\n\n### Fixes\n* bump yanked dependency `bumpalo` to fix build from source ([#2087](https://github.com/gitui-org/gitui/issues/2087))\n* pin `ratatui` version to fix building without locked `cargo install gitui` ([#2090](https://github.com/gitui-org/gitui/issues/2090))\n\n## [0.25.0] - 2024-02-21\n\n** multiline text editor **\n\n![multiline editor](assets/multiline-texteditor.gif)\n\n** syntax highlighting in blame **\n\n![syntax-highlighting-blame](assets/syntax-highlighting-blame.png)\n\n### Breaking Change\n\n#### commit key binding\n\nThe Commit message popup now supports multiline editing! Inserting a **newline** defaults to `enter`. This comes with a new default to confirm the commit message (`ctrl+d`).\nBoth commands can be overwritten via `newline` and `commit` in the key bindings. see [KEY_CONFIG](./KEY_CONFIG.md) on how.\nThese defaults require some adoption from existing users but feel more natural to new users.\n\n#### key binding bitflags\n\nModifiers like `SHIFT` or `CONTROL` are no longer configured via magic bitflags but via strings thanks to changes in the [bitflags crate](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md#changes-to-serde-serialization) we depend on. Please see [KEY_CONFIG.md](./KEY_CONFIG.md) or [vim_style_key_config.ron](./vim_style_key_config.ron) for more info and examples.\n\n### Added\n* support for new-line in text-input (e.g. commit message editor) [[@pm100]](https://github/pm100) ([#1662](https://github.com/gitui-org/gitui/issues/1662)).\n* add syntax highlighting for blame view [[@tdtrung17693](https://github.com/tdtrung17693)] ([#745](https://github.com/gitui-org/gitui/issues/745))\n* allow aborting pending commit log search [[@StemCll](https://github.com/StemCll)] ([#1860](https://github.com/gitui-org/gitui/issues/1860))\n* `theme.ron` now supports customizing line break symbol ([#1894](https://github.com/gitui-org/gitui/issues/1894))\n* add confirmation for dialog for undo commit [[@TeFiLeDo](https://github.com/TeFiLeDo)] ([#1912](https://github.com/gitui-org/gitui/issues/1912))\n* support `prepare-commit-msg` hook ([#1873](https://github.com/gitui-org/gitui/issues/1873))\n* new style `block_title_focused` to allow customizing title text of focused frame/block ([#2052](https://github.com/gitui-org/gitui/issues/2052)).\n* allow `fetch` command in both tabs of branchlist popup ([#2067](https://github.com/gitui-org/gitui/issues/2067))\n* check branch name validity while typing [[@sainad2222](https://github.com/sainad2222)] ([#2062](https://github.com/gitui-org/gitui/issues/2062))\n\n### Changed\n* do not allow tagging when `tag.gpgsign` enabled until gpg-signing is [supported](https://github.com/gitui-org/gitui/issues/97) [[@TeFiLeDo](https://github.com/TeFiLeDo)] ([#1915](https://github.com/gitui-org/gitui/pull/1915))\n\n### Fixes\n* stash window empty after file history popup closes ([#1986](https://github.com/gitui-org/gitui/issues/1986))\n* allow push to empty remote ([#1919](https://github.com/gitui-org/gitui/issues/1919))\n* better diagnostics for theme file loading ([#2007](https://github.com/gitui-org/gitui/issues/2007))\n* fix ordering of commits in diff view [[@Joshix-1](https://github.com/Joshix-1)]([#1747](https://github.com/gitui-org/gitui/issues/1747))\n\n## [0.24.3] - 2023-09-09\n\n### Fixes\n* log: major lag when going beyond last search hit ([#1876](https://github.com/gitui-org/gitui/issues/1876))\n\n### Changed\n* parallelise log search - performance gain ~100% ([#1869](https://github.com/gitui-org/gitui/issues/1869))\n* search message body/summary separately ([#1875](https://github.com/gitui-org/gitui/issues/1875))\n\n## [0.24.2] - 2023-09-03\n\n### Fixes\n* fix commit log not updating after branch switch ([#1862](https://github.com/gitui-org/gitui/issues/1862))\n* fix stashlist not updating after pop/drop ([#1864](https://github.com/gitui-org/gitui/issues/1864))\n* fix commit log corruption when tabbing in/out while parsing log ([#1866](https://github.com/gitui-org/gitui/issues/1866))\n\n## [0.24.1] - 2023-08-30\n\n### Fixes\n* fix performance problem in big repo with a lot of incoming commits ([#1845](https://github.com/gitui-org/gitui/issues/1845))\n* fix error switching to a branch with '/' in the name ([#1851](https://github.com/gitui-org/gitui/issues/1851))\n\n## [0.24.0] - 2023-08-27\n\n**search commits**\n\n![commit-search](assets/log-search.gif)\n\n**visualize empty lines in diff better**\n\n![diff-empty-line](assets/diff-empty-line.png)\n\n### Breaking Changes\n* Do you use a custom theme?\n\n  The way themes work got changed and simplified ([see docs](https://github.com/gitui-org/gitui/blob/master/THEMES.md) for more info):\n\n  * The format of `theme.ron` has changed: you only specify the colors etc. that should differ from their default value\n  * Future additions of colors etc. will not break existing themes anymore\n\n### Added\n* search commits by message, author or files in diff ([#1791](https://github.com/gitui-org/gitui/issues/1791))\n* support 'n'/'p' key to move to the next/prev hunk in diff component [[@hamflx](https://github.com/hamflx)] ([#1523](https://github.com/gitui-org/gitui/issues/1523))\n* simplify theme overrides [[@cruessler](https://github.com/cruessler)] ([#1367](https://github.com/gitui-org/gitui/issues/1367))\n* support for sign-off of commits [[@domtac](https://github.com/domtac)]([#1757](https://github.com/gitui-org/gitui/issues/1757))\n* switched from textwrap to bwrap for text wrapping [[@TheBlackSheep3](https://github.com/TheBlackSheep3/)] ([#1762](https://github.com/gitui-org/gitui/issues/1762))\n* more logging diagnostics when a repo cannot be opened\n* added to [anaconda](https://anaconda.org/conda-forge/gitui) [[@TheBlackSheep3](https://github.com/TheBlackSheep3/)] ([#1626](https://github.com/gitui-org/gitui/issues/1626))\n* visualize empty line substituted with content in diff better ([#1359](https://github.com/gitui-org/gitui/issues/1359))\n* checkout branch works with non-empty status report [[@lightsnowball](https://github.com/lightsnowball)]  ([#1399](https://github.com/gitui-org/gitui/issues/1399))\n* jump to commit by SHA [[@AmmarAbouZor](https://github.com/AmmarAbouZor)] ([#1818](https://github.com/gitui-org/gitui/pull/1818))\n\n### Fixes\n* fix commit dialog char count for multibyte characters ([#1726](https://github.com/gitui-org/gitui/issues/1726))\n* fix wrong hit highlighting in fuzzy find popup [[@UUGTech](https://github.com/UUGTech)] ([#1731](https://github.com/gitui-org/gitui/pull/1731))\n* fix symlink support for configuration files [[@TheBlackSheep3](https://github.com/TheBlackSheep3)] ([#1751](https://github.com/gitui-org/gitui/issues/1751))\n* fix expansion of `~` in `commit.template` ([#1745](https://github.com/gitui-org/gitui/pull/1745))\n* fix hunk (un)staging/reset for # of context lines != 3 ([#1746](https://github.com/gitui-org/gitui/issues/1746))\n* fix delay when opening external editor ([#1506](https://github.com/gitui-org/gitui/issues/1506))\n\n### Changed\n* Copy full Commit Hash by default [[@AmmarAbouZor](https://github.com/AmmarAbouZor)] ([#1836](https://github.com/gitui-org/gitui/issues/1836))\n\n## [0.23.0] - 2023-06-19\n\n**reset to commit**\n\n![reset](assets/reset_in_log.gif)\n\n**reword commit**\n\n![reword](assets/reword.gif)\n\n**fuzzy find branch**\n\n![fuzzy-branch](assets/fuzzy-find-branch.gif)\n\n### Breaking Change\n* `focus_XYZ` key bindings are merged into the `move_XYZ` set, so only one way to bind arrow-like keys from now on ([#1539](https://github.com/gitui-org/gitui/issues/1539))\n\n### Added\n* allow reset (soft,mixed,hard) from commit log ([#1500](https://github.com/gitui-org/gitui/issues/1500))\n* support **reword** of commit from log ([#829](https://github.com/gitui-org/gitui/pull/829))\n* fuzzy find branch [[@UUGTech](https://github.com/UUGTech)] ([#1350](https://github.com/gitui-org/gitui/issues/1350))\n* list changes in commit message inside external editor [[@bc-universe]](https://github.com/bc-universe) ([#1420](https://github.com/gitui-org/gitui/issues/1420))\n* allow detaching HEAD and checking out specific commit from log view [[@fralcow]](https://github.com/fralcow) ([#1499](https://github.com/gitui-org/gitui/pull/1499))\n* add no-verify option on commits to not run hooks [[@dam5h]](https://github.com/dam5h) ([#1374](https://github.com/gitui-org/gitui/issues/1374))\n* allow `fetch` on status tab [[@alensiljak]](https://github.com/alensiljak) ([#1471](https://github.com/gitui-org/gitui/issues/1471))\n* allow `copy` file path on revision files and status tree [[@yanganto]](https://github.com/yanganto)  ([#1516](https://github.com/gitui-org/gitui/pull/1516))\n* print message of where log will be written if `-l` is set ([#1472](https://github.com/gitui-org/gitui/pull/1472))\n* show remote branches in log [[@cruessler](https://github.com/cruessler)] ([#1501](https://github.com/gitui-org/gitui/issues/1501))\n* scrolling functionality to fuzzy-find [[@AmmarAbouZor](https://github.com/AmmarAbouZor)] ([#1732](https://github.com/gitui-org/gitui/issues/1732))\n\n### Fixes\n* fixed side effect of crossterm 0.26 on windows that caused double input of all keys [[@pm100]](https://github/pm100) ([#1686](https://github.com/gitui-org/gitui/pull/1686))\n* commit msg history ordered the wrong way ([#1445](https://github.com/gitui-org/gitui/issues/1445))\n* improve help documentation for amend cmd ([#1448](https://github.com/gitui-org/gitui/issues/1448))\n* lag issue when showing files tab ([#1451](https://github.com/gitui-org/gitui/issues/1451))\n* fix key binding shown in bottom bar for `stash_open` ([#1454](https://github.com/gitui-org/gitui/issues/1454))\n* `--bugreport` does not require param ([#1466](https://github.com/gitui-org/gitui/issues/1466))\n* `edit`-file command shown on commits msg ([#1461](https://github.com/gitui-org/gitui/issues/1461))\n* crash on branches popup in small terminal ([#1470](https://github.com/gitui-org/gitui/issues/1470))\n* `edit` command duplication ([#1489](https://github.com/gitui-org/gitui/issues/1489))\n* syntax errors in `key_bindings.ron` will be logged ([#1491](https://github.com/gitui-org/gitui/issues/1491))\n* Fix UI freeze when copying with xclip installed on Linux ([#1497](https://github.com/gitui-org/gitui/issues/1497))\n* Fix UI freeze when copying with wl-copy installed on Linux ([#1497](https://github.com/gitui-org/gitui/issues/1497))\n* commit hooks report \"command not found\" on Windows with wsl2 installed ([#1528](https://github.com/gitui-org/gitui/issues/1528))\n* crashes on entering submodules ([#1510](https://github.com/gitui-org/gitui/issues/1510))\n* fix race issue: revlog messages sometimes appear empty ([#1473](https://github.com/gitui-org/gitui/issues/1473))\n* default to tick-based updates [[@cruessler](https://github.com/cruessler)] ([#1444](https://github.com/gitui-org/gitui/issues/1444))\n* add support for options handling in log and stashes views [[@kamillo](https://github.com/kamillo)] ([#1661](https://github.com/gitui-org/gitui/issues/1661))\n\n### Changed\n* minimum supported rust version bumped to 1.65 (thank you `time` crate)\n\n## [0.22.1] - 2022-11-22\n\nBugfix followup release - check `0.22.0` notes for more infos!\n\n### Added\n* new arg `--polling` to use poll-based change detection and not filesystem watcher (use if you see problems running into file descriptor limits)\n\n### Fixes\n* improve performance by requesting branches info asynchronous ([92f63d1](https://github.com/gitui-org/gitui/commit/92f63d107c1dca1f10139668ff5b3ca752261b0f))\n* fix app startup delay due to using file watcher ([#1436](https://github.com/gitui-org/gitui/issues/1436))\n* make git tree file fetch async ([#734](https://github.com/gitui-org/gitui/issues/734))\n\n## [0.22.0] - 2022-11-19\n\n**submodules view**\n\n![submodules](assets/submodules.gif)\n\n**commit message history**\n\n![commit-history](assets/commit-msg-history.gif)\n\n### Added\n* submodules support ([#1087](https://github.com/gitui-org/gitui/issues/1087))\n* remember tab between app starts ([#1338](https://github.com/gitui-org/gitui/issues/1338))\n* repo specific gitui options saved in `.git/gitui.ron` ([#1340](https://github.com/gitui-org/gitui/issues/1340))\n* commit msg history ([#1345](https://github.com/gitui-org/gitui/issues/1345))\n* customizable `cmdbar_bg` theme color & screen spanning selected line bg [[@gigitsu](https://github.com/gigitsu)] ([#1299](https://github.com/gitui-org/gitui/pull/1299))\n* word motions to text input [[@Rodrigodd](https://github.com/Rodrigodd)] ([#1256](https://github.com/gitui-org/gitui/issues/1256))\n* file blame at right revision from commit-details [[@heiskane](https://github.com/heiskane)] ([#1122](https://github.com/gitui-org/gitui/issues/1122))\n* dedicated selection foreground theme color `selection_fg` ([#1365](https://github.com/gitui-org/gitui/issues/1365))\n* add `regex-fancy` and `regex-onig` features to allow building Syntect with Onigumara regex engine instead of the default engine based on fancy-regex [[@jirutka](https://github.com/jirutka)]\n* add `vendor-openssl` feature to allow building without vendored openssl [[@jirutka](https://github.com/jirutka)]\n* allow copying marked commits [[@remique](https://github.com/remique)] ([#1288](https://github.com/gitui-org/gitui/issues/1288))\n* feedback for success/failure of copying hash commit [[@sergioribera](https://github.com/sergioribera)]([#1160](https://github.com/gitui-org/gitui/issues/1160))\n* display tags and branches in the log view [[@alexmaco](https://github.com/alexmaco)] ([#1371](https://github.com/gitui-org/gitui/pull/1371))\n* display current repository path in the top-right corner [[@alexmaco](https://github.com/alexmaco)]([#1387](https://github.com/gitui-org/gitui/pull/1387))\n* add Linux targets for ARM, ARMv7 and AARCH64 [[@adur1990](https://github.com/adur1990)] ([#1419](https://github.com/gitui-org/gitui/pull/1419))\n* display commit description in file view [[@alexmaco](https://github.com/alexmaco)] ([#1380](https://github.com/gitui-org/gitui/pull/1380))\n* allow launching editor from Compare Commits view ([#1409](https://github.com/gitui-org/gitui/pull/1409))\n\n### Fixes\n* remove insecure dependency `ansi_term` ([#1290](https://github.com/gitui-org/gitui/issues/1290))\n* use filewatcher instead of polling updates ([#1](https://github.com/gitui-org/gitui/issues/1))\n\n## [0.21.0] - 2022-08-17\n\n**popup stacking**\n\n![popup-stacking](assets/popup-stacking.gif)\n\n**termux android support**\n\n![termux-android](assets/termux-android.jpg)\n\n### Added\n* stack popups ([#846](https://github.com/gitui-org/gitui/issues/846))\n* file history log [[@cruessler](https://github.com/cruessler)] ([#381](https://github.com/gitui-org/gitui/issues/381))\n* termux support on android [[@PeroSar](https://github.com/PeroSar)] ([#1139](https://github.com/gitui-org/gitui/issues/1139))\n* use `GIT_DIR` and `GIT_WORK_DIR` from environment if set ([#1191](https://github.com/gitui-org/gitui/pull/1191))\n* new [FAQ](./FAQ.md)s page\n* mention macports in install section [[@fs111](https://github.com/fs111)]([#1237](https://github.com/gitui-org/gitui/pull/1237))\n* support copy to clipboard on wayland [[@JayceFayne](https://github.com/JayceFayne)] ([#397](https://github.com/gitui-org/gitui/issues/397))\n\n### Fixed\n* opening tags list without remotes ([#1111](https://github.com/gitui-org/gitui/issues/1111))\n* tabs indentation in blame [[@fersilva16](https://github.com/fersilva16)] ([#1117](https://github.com/gitui-org/gitui/issues/1117))\n* switch focus to index after staging last file ([#1169](https://github.com/gitui-org/gitui/pull/1169))\n* fix stashlist multi marking not updated after dropping ([#1207](https://github.com/gitui-org/gitui/pull/1207))\n* exact matches have a higher priority and are placed to the top of the list when fuzzily finding files ([#1183](https://github.com/gitui-org/gitui/pull/1183))\n* support horizontal scrolling in diff view ([#1017](https://github.com/gitui-org/gitui/issues/1017))\n\n### Changed\n* minimum supported rust version bumped to 1.60 ([#1279](https://github.com/gitui-org/gitui/pull/1279))\n\n## [0.20.1] - 2022-01-26\n\nThis is was a immediate followup patch release to `0.20` see [release notes](https://github.com/gitui-org/gitui/releases/tag/v0.20.0) for the whole list of goodies in `0.20`.\n\n### Added\n* support proxy auto detection via env's like `HTTP_PROXY` ([#994](https://github.com/gitui-org/gitui/issues/994))\n\n### Fixed\n* severe performance regression in `0.20` ([#1102](https://github.com/gitui-org/gitui/issues/1102))\n* several smaller performance improvements via caching ([#1104](https://github.com/gitui-org/gitui/issues/1104))\n* windows release deployment via CD broken\n\n## [0.20] - 2022-01-25 - Tag Annotations\n\n**support tag annotations**\n\n![tag-annotation](assets/tag-annotation.gif)\n\n**delete tag on remote**\n\n![delete-tag-remote](assets/delete-tag-remote.gif)\n\n**revert commit from rev log**\n\n![revert-commit](assets/revert-commit.gif)\n\n### Added\n- support `core.hooksPath` ([#1044](https://github.com/gitui-org/gitui/issues/1044))\n- allow reverting a commit from the commit log ([#927](https://github.com/gitui-org/gitui/issues/927))\n- disable pull cmd on local-only branches ([#1047](https://github.com/gitui-org/gitui/issues/1047))\n- support adding annotations to tags ([#747](https://github.com/gitui-org/gitui/issues/747))\n- support inspecting annotation of tag ([#1076](https://github.com/gitui-org/gitui/issues/1076))\n- support deleting tag on remote ([#1074](https://github.com/gitui-org/gitui/issues/1074))\n- support git credentials helper (https) ([#800](https://github.com/gitui-org/gitui/issues/800))\n\n### Fixed\n- Keep commit message when pre-commit hook fails ([#1035](https://github.com/gitui-org/gitui/issues/1035))\n- honor `pushurl` when checking credentials for pushing ([#953](https://github.com/gitui-org/gitui/issues/953))\n- use git-path instead of workdir finding hooks ([#1046](https://github.com/gitui-org/gitui/issues/1046))\n- only enable remote actions (fetch/pull/push) if there are remote branches ([#1047](https://github.com/gitui-org/gitui/issues/1047))\n\n### Key binding notes\n- added `gg`/`G` vim bindings to `vim_style_key_config.ron` ([#1039](https://github.com/gitui-org/gitui/issues/1039))\n\n## [0.19] - 2021-12-08 - Bare Repo Support\n\n**finder highlighting matches**\n\n![fuzzy-find](assets/fuzzy-find-matches.gif)\n\n### Breaking Change\nHave you used `key_config.ron` for custom key bindings before?\nThe way this works got changed and simplified ([See docs](https://github.com/gitui-org/gitui/blob/master/KEY_CONFIG.md) for more info):\n* You only define the keys that should differ from the default.\n* The file is renamed to `key_bindings.ron`\n* Future addition of new keys will not break anymore\n\n### Added\n- add fetch/update command all remote branches ([#998](https://github.com/gitui-org/gitui/issues/998))\n- add `trace-libgit` feature to make git tracing optional [[@dm9pZCAq](https://github.com/dm9pZCAq)] ([#902](https://github.com/gitui-org/gitui/issues/902))\n- support merging and rebasing remote branches [[@R0nd](https://github.com/R0nd)] ([#920](https://github.com/gitui-org/gitui/issues/920))\n- add highlighting matches in fuzzy finder [[@Mifom](https://github.com/Mifom)] ([#893](https://github.com/gitui-org/gitui/issues/893))\n- support `home` and `end` keys in branchlist ([#957](https://github.com/gitui-org/gitui/issues/957))\n- add `ghemoji` feature to make gh-emoji (GitHub emoji) optional [[@jirutka](https://github.com/jirutka)] ([#954](https://github.com/gitui-org/gitui/pull/954))\n- allow customizing key symbols like `⏎` & `⇧` ([see docs](https://github.com/gitui-org/gitui/blob/master/KEY_CONFIG.md#key-symbols)) ([#465](https://github.com/gitui-org/gitui/issues/465))\n- simplify key overrides ([see docs](https://github.com/gitui-org/gitui/blob/master/KEY_CONFIG.md)) ([#946](https://github.com/gitui-org/gitui/issues/946))\n- dedicated fuzzy finder up/down keys to allow vim overrides ([#993](https://github.com/gitui-org/gitui/pull/993))\n- pull will also download tags ([#1013](https://github.com/gitui-org/gitui/pull/1013))\n- allow editing file from filetree ([#989](https://github.com/gitui-org/gitui/pull/989))\n- support bare repos (new `workdir` argument) ([#1026](https://github.com/gitui-org/gitui/pull/1026))\n\n### Fixed\n- honor options (for untracked files) in `stage_all` command ([#933](https://github.com/gitui-org/gitui/issues/933))\n- improved file diff speed dramatically ([#976](https://github.com/gitui-org/gitui/issues/976))\n- blaming files in sub-folders on windows ([#981](https://github.com/gitui-org/gitui/issues/981))\n- push failing due to tracing error in upstream ([#881](https://github.com/gitui-org/gitui/issues/881))\n\n## [0.18] - 2021-10-11\n\n**rebase merge with conflicts**\n\n![rebase-merge](assets/rebase.png)\n\n### Added\n- support rebasing branches with conflicts ([#895](https://github.com/gitui-org/gitui/issues/895))\n- add a key binding to stage / unstage items [[@alessandroasm](https://github.com/alessandroasm)] ([#909](https://github.com/gitui-org/gitui/issues/909))\n- switch to status tab after merging or rebasing with conflicts ([#926](https://github.com/gitui-org/gitui/issues/926))\n\n### Fixed\n- fix supported checkout of hierarchical branchnames ([#921](https://github.com/gitui-org/gitui/issues/921))\n- appropriate error message when pulling deleted remote branch ([#911](https://github.com/gitui-org/gitui/issues/911))\n- improved color contrast in branches popup for light themes  [[@Cottser](https://github.com/Cottser)] ([#922](https://github.com/gitui-org/gitui/issues/922))\n- use git_message_prettify for commit messages ([#917](https://github.com/gitui-org/gitui/issues/917))\n\n## [0.17.1] - 2021-09-10\n\n**fuzzy find files**\n\n![fuzzy-find](assets/fuzzy-find.gif)\n\n**emojified commit message**\n\n![emojified-commit-message](assets/emojified-commit-message.png)\n\n### Added\n- add supporting rebasing on branch (if conflict-free) ([#816](https://github.com/gitui-org/gitui/issues/816))\n- fuzzy find files ([#891](https://github.com/gitui-org/gitui/issues/891))\n- visualize progress during async syntax highlighting ([#889](https://github.com/gitui-org/gitui/issues/889))\n- added support for markdown emoji's in commits [[@andrewpollack](https://github.com/andrewpollack)] ([#768](https://github.com/gitui-org/gitui/issues/768))\n- added scrollbar to revlog [[@ashvin021](https://github.com/ashvin021)] ([#868](https://github.com/gitui-org/gitui/issues/868))\n\n### Fixed\n- fix build when system level libgit2 version was used ([#883](https://github.com/gitui-org/gitui/issues/883))\n- fix merging branch not closing branch window [[@andrewpollack](https://github.com/andrewpollack)] ([#876](https://github.com/gitui-org/gitui/issues/876))\n- fix commit msg being broken inside tag list ([#871](https://github.com/gitui-org/gitui/issues/871))\n- fix filetree file content not showing tabs correctly ([#874](https://github.com/gitui-org/gitui/issues/874))\n\n### Key binding notes\n- new keys: `rebase_branch` [`R`], `file_find` [`f`]\n\nsee `vim_style_key_config.ron` for their default vim binding\n\n## [0.17.0] - 2021-08-21\n\n**compare commits**\n\n![compare](assets/compare.gif)\n\n**options**\n\n![options](assets/options.gif)\n\n**drop multiple stashes**\n\n![drop-multiple-stashes](assets/drop-multiple-stashes.gif)\n\n**branch name validation**\n\n![name-validation](assets/branch-validation.gif)\n\n### Added\n- allow inspecting top commit of a branch from list\n- compare commits in revlog and head against branch ([#852](https://github.com/gitui-org/gitui/issues/852))\n- new options popup (show untracked files, diff settings) ([#849](https://github.com/gitui-org/gitui/issues/849))\n- mark and drop multiple stashes ([#854](https://github.com/gitui-org/gitui/issues/854))\n- check branch name validity while typing ([#559](https://github.com/gitui-org/gitui/issues/559))\n- support deleting remote branch [[@zcorniere](https://github.com/zcorniere)] ([#622](https://github.com/gitui-org/gitui/issues/622))\n- mark remote branches that have local tracking branch [[@jedel1043](https://github.com/jedel1043)] ([#861](https://github.com/gitui-org/gitui/issues/861))\n\n### Fixed\n- error viewing filetree in empty repo ([#859](https://github.com/gitui-org/gitui/issues/859))\n- do not allow to ignore .gitignore files ([#825](https://github.com/gitui-org/gitui/issues/825))\n- crash in shallow repo ([#836](https://github.com/gitui-org/gitui/issues/836))\n- fixed performance regression in revlog ([#850](https://github.com/gitui-org/gitui/issues/850))\n- fixed performance degradation when quitting on Windows ([#823](https://github.com/gitui-org/gitui/issues/823))\n\n## [0.16.2] - 2021-07-10\n\n**undo last commit**\n\n![undo-last-commit](assets/undo-last-commit.gif)\n\n**mark local tags**\n\n![tag-remote-marker](assets/tag-remote-marker.gif)\n\n### Added\n- taglist: show arrow-symbol on tags not present on origin [[@cruessler](https://github.com/cruessler)] ([#776](https://github.com/gitui-org/gitui/issues/776))\n- new `undo-last-commit` command [[@remique](https://github.com/remique)] ([#758](https://github.com/gitui-org/gitui/issues/758))\n- new quit key `[q]` ([#771](https://github.com/gitui-org/gitui/issues/771))\n- proper error message if remote rejects force push ([#801](https://github.com/gitui-org/gitui/issues/801))\n\n### Fixed\n- openssl vendoring broken on macos ([#772](https://github.com/gitui-org/gitui/issues/772))\n- amend and other commands not shown in help ([#778](https://github.com/gitui-org/gitui/issues/778))\n- focus locked on commit msg details in narrow term sizes ([#780](https://github.com/gitui-org/gitui/issues/780))\n- non-utf8 file/path names broke filetree ([#802](https://github.com/gitui-org/gitui/issues/802))\n\n## [0.16.1] - 2021-06-06\n\n### Added\n- honor `config.showUntrackedFiles` improving speed with a lot of untracked items ([#752](https://github.com/gitui-org/gitui/issues/752))\n- improve performance when opening filetree-tab ([#756](https://github.com/gitui-org/gitui/issues/756))\n- indicator for longer commit message than displayed ([#773](https://github.com/gitui-org/gitui/issues/773))\n\n![msg-len](assets/long-msg-indicator.gif)\n\n### Fixed\n- wrong file with same name shown in file tree ([#748](https://github.com/gitui-org/gitui/issues/748))\n- filetree collapsing broken on windows ([#761](https://github.com/gitui-org/gitui/issues/761))\n- unnecessary overdraw of the spinner on each redraw ([#764](https://github.com/gitui-org/gitui/issues/764))\n\n### Internal\n- use git_repository_message [[@kosayoda](https://github.com/kosayoda)] ([#751](https://github.com/gitui-org/gitui/issues/751))\n\n## [0.16.0] - 2021-05-28\n\n**merge branch, merge commit**\n\n![merge-commit](assets/merge-commit-abort.gif)\n\n**tag list popup**\n\n![tagslist](assets/tags-list-popup.gif)\n\n**revision file tree**\n\n![filetree](assets/revision-file-tree.gif)\n\n**commit subject length warning**\n\n![warning](assets/commit-msg-length-limit.gif)\n\n### Added\n- merging branches, pull-merge with conflicts, commit merges ([#485](https://github.com/gitui-org/gitui/issues/485))\n- tags-list-popup (delete-tag, go to tagged commit) [[@cruessler](https://github.com/cruessler)] ([#483](https://github.com/gitui-org/gitui/issues/483))\n- inspect file tree tab ([#743](https://github.com/gitui-org/gitui/issues/743))\n- file tree popup (for a specific revision) ([#714](https://github.com/gitui-org/gitui/issues/714))\n- warning if commit subject line gets too long ([#478](https://github.com/gitui-org/gitui/issues/478))\n- `--bugreport` cmd line arg to help diagnostics [[@zcorniere](https://github.com/zcorniere)] ([#695](https://github.com/gitui-org/gitui/issues/695))\n\n### Changed\n- smarter log timestamps ([#682](https://github.com/gitui-org/gitui/issues/682))\n- create-branch popup aligned with rename-branch [[@bruceCoelho](https://github.com/bruceCoelho)] ([#679](https://github.com/gitui-org/gitui/issues/679))\n- smart focus change after staging all files ([#706](https://github.com/gitui-org/gitui/issues/706))\n- do not allow to commit when `gpgsign` enabled ([#740](https://github.com/gitui-org/gitui/issues/740))\n\n### Fixed\n- selected-tab color broken in light theme [[@Cottser](https://github.com/Cottser)] ([#719](https://github.com/gitui-org/gitui/issues/719))\n- proper tmp file location to externally edit commit msg ([#518](https://github.com/gitui-org/gitui/issues/518))\n\n## [0.15.0] - 2021-04-27\n\n**file blame**\n\n![blame](assets/blame.gif)\n\n### Added\n- blame a file [[@cruessler](https://github.com/cruessler)] ([#484](https://github.com/gitui-org/gitui/issues/484))\n- support commit.template [[@wandernauta](https://github.com/wandernauta)] ([#546](https://github.com/gitui-org/gitui/issues/546))\n\n### Fixed\n- debug print when adding a file to ignore\n- fix scrolling long messages in commit details view ([#663](https://github.com/gitui-org/gitui/issues/663))\n- limit log messages in log tab ([#652](https://github.com/gitui-org/gitui/issues/652))\n- fetch crashed when no upstream of branch is set ([#637](https://github.com/gitui-org/gitui/issues/637))\n- `enter` key panics in empty remote branch list ([#643](https://github.com/gitui-org/gitui/issues/643))\n\n### Internal\n- cleanup some stringly typed code [[@wandernauta](https://github.com/wandernauta)] ([#655](https://github.com/gitui-org/gitui/issues/655))\n- introduce EventState enum (removing bool for even propagation) [[@tisorlawan](https://github.com/tisorlawan)] ([#665](https://github.com/gitui-org/gitui/issues/665))\n\n## [0.14.0] - 2021-04-11\n\n### Added\n- `[w]` key to toggle between staging/workdir [[@terhechte](https://github.com/terhechte)] ([#595](https://github.com/gitui-org/gitui/issues/595))\n- view/checkout remote branches ([#617](https://github.com/gitui-org/gitui/issues/617))\n\n![checkout-remote](assets/checkout-remote.gif)\n\n### Changed\n- ask to pop stash by default (*apply* using `[a]` now) [[@brunogouveia](https://github.com/brunogouveia)] ([#574](https://github.com/gitui-org/gitui/issues/574))\n\n![stash_pop](assets/stash_pop.gif)\n\n### Fixed\n- push branch to its tracking remote ([#597](https://github.com/gitui-org/gitui/issues/597))\n- fixed panic when staging lines involving missing newline eof ([#605](https://github.com/gitui-org/gitui/issues/605))\n- fixed pull/fetch deadlocking when it fails ([#624](https://github.com/gitui-org/gitui/issues/624))\n\n## [0.13.0] - 2021-03-15 - Happy Birthday GitUI 🥳\n\nThanks for your interest and support over this year! Read more about the 1 year anniversary reflections of this project on my [blog](https://blog.extrawurst.org/general/programming/rust/2021/03/15/gitui-a-year-in-opensource.html).\n\n**stage/unstage/discard by line**\n\n![by-line-ops](assets/by-line-ops.gif)\n\n**push tags**\n\n![push-tags](assets/push_tags.gif)\n\n### Changed\n- `[s]` key repurposed to trigger line based (un)stage\n- cleanup status/diff commands to be more context sensitive ([#572](https://github.com/gitui-org/gitui/issues/572))\n\n### Added\n- support pull via rebase (using config `pull.rebase`) ([#566](https://github.com/gitui-org/gitui/issues/566))\n- support stage/unstage selected lines ([#59](https://github.com/gitui-org/gitui/issues/59))\n- support discarding selected lines ([#59](https://github.com/gitui-org/gitui/issues/59))\n- support for pushing tags ([#568](https://github.com/gitui-org/gitui/issues/568))\n- visualize *conflicted* files differently ([#576](https://github.com/gitui-org/gitui/issues/576))\n\n### Fixed\n- keep diff line selection after staging/unstaging/discarding ([#583](https://github.com/gitui-org/gitui/issues/583))\n- fix pull deadlocking when aborting credentials input ([#586](https://github.com/gitui-org/gitui/issues/586))\n- error diagnostics for config loading ([#589](https://github.com/gitui-org/gitui/issues/589))\n\n## [0.12.0] - 2021-03-03\n\n**pull support (ff-merge or conflict-free merge-commit)**\n\n![pull](assets/pull.gif)\n\n**more info in commit popup**\n\n![chars-branch-name](assets/chars_and_branchname.gif)\n\n### Breaking Change\n- MacOS config directory now uses `~/.config/gitui` [[@remique](https://github.com/remique)] ([#317](https://github.com/gitui-org/gitui/issues/317))\n\n### Added\n- support for pull (fetch + simple merging) ([#319](https://github.com/gitui-org/gitui/issues/319))\n- show used char count in input texts ([#466](https://github.com/gitui-org/gitui/issues/466))\n- support smoother left/right toggle/keys for commit details ([#418](https://github.com/gitui-org/gitui/issues/418))\n- support *force push* command [[@WizardOhio24](https://github.com/WizardOhio24)] ([#274](https://github.com/gitui-org/gitui/issues/274))\n\n### Fixed\n- don't close branchlist every time ([#550](https://github.com/gitui-org/gitui/issues/550))\n- fixed key binding for *external exitor* in vim key bindings [[@yanganto](https://github.com/yanganto)] ([#549](https://github.com/gitui-org/gitui/issues/549))\n- fix some potential errors when deleting files while they are being diffed ([#490](https://github.com/gitui-org/gitui/issues/490))\n- push defaults to 'origin' remote if it exists ([#494](https://github.com/gitui-org/gitui/issues/494))\n- support missing pageUp/down support in branchlist ([#519](https://github.com/gitui-org/gitui/issues/519))\n- don't hide branch name while in commit dialog ([#529](https://github.com/gitui-org/gitui/issues/529))\n- don't discard commit message without confirmation ([#530](https://github.com/gitui-org/gitui/issues/530))\n- compilation broken on freebsd ([#461](https://github.com/gitui-org/gitui/issues/461))\n- don’t fail if `user.name` is not set [[@cruessler](https://github.com/cruessler)] ([#79](https://github.com/gitui-org/gitui/issues/79)) ([#228](https://github.com/gitui-org/gitui/issues/228))\n\n## [0.11.0] - 2021-12-20\n\n### Added\n- push to remote ([#265](https://github.com/gitui-org/gitui/issues/265)) ([#267](https://github.com/gitui-org/gitui/issues/267))\n\n![push](assets/push.gif)\n\n- number of incoming/outgoing commits to upstream ([#362](https://github.com/gitui-org/gitui/issues/362))\n- new branch list popup incl. checkout/delete/rename [[@WizardOhio24](https://github.com/WizardOhio24)] ([#303](https://github.com/gitui-org/gitui/issues/303)) ([#323](https://github.com/gitui-org/gitui/issues/323))\n\n![branches](assets/branches.gif)\n\n- compact treeview [[@WizardOhio24](https://github.com/WizardOhio24)] ([#192](https://github.com/gitui-org/gitui/issues/192))\n\n![tree](assets/compact-tree.png)\n\n- scrollbar in long commit messages [[@timaliberdov](https://github.com/timaliberdov)] ([#308](https://github.com/gitui-org/gitui/issues/308))\n- added windows scoop recipe ([#164](https://github.com/gitui-org/gitui/issues/164))\n- added gitui to [chocolatey](https://chocolatey.org/packages/gitui) on windows by [@nils-a](https://github.com/nils-a)\n- added gitui gentoo instructions to readme [[@dm9pZCAq](https://github.com/dm9pZCAq)] ([#430](https://github.com/gitui-org/gitui/pull/430))\n- added windows installer (msi) to release [[@pm100](https://github.com/pm100)] ([#360](https://github.com/gitui-org/gitui/issues/360))\n- command to copy commit hash [[@yanganto](https://github.com/yanganto)] ([#281](https://github.com/gitui-org/gitui/issues/281))\n\n### Changed\n- upgrade `dirs` to `dirs-next` / remove cfg migration code ([#351](https://github.com/gitui-org/gitui/issues/351)) ([#366](https://github.com/gitui-org/gitui/issues/366))\n- do not highlight selection in diff view when not focused ([#270](https://github.com/gitui-org/gitui/issues/270))\n- copy to clipboard using `xclip`(linux), `pbcopy`(mac) or `clip`(win) [[@cruessler](https://github.com/cruessler)] ([#262](https://github.com/gitui-org/gitui/issues/262))\n\n### Fixed\n- crash when changing git repo while gitui is open ([#271](https://github.com/gitui-org/gitui/issues/271))\n- remove workaround for color serialization [[@1wilkens](https://github.com/1wilkens)] ([#149](https://github.com/gitui-org/gitui/issues/149))\n- crash on small terminal size ([#307](https://github.com/gitui-org/gitui/issues/307))\n- fix vim keybindings uppercase handling [[@yanganto](https://github.com/yanganto)] ([#286](https://github.com/gitui-org/gitui/issues/286))\n- remove shift tab windows workaround [[@nils-a](https://github.com/nils-a)] ([#112](https://github.com/gitui-org/gitui/issues/112))\n- core.editor is ignored [[@pm100](https://github.com/pm100)] ([#414](https://github.com/gitui-org/gitui/issues/414))\n\n## [0.10.1] - 2020-09-01\n\n### Fixed\n- static linux binaries broke due to new clipboard feature which is disabled on linux for now ([#259](https://github.com/gitui-org/gitui/issues/259))\n\n## [0.10.0] - 2020-08-29\n\n### Added\n\n- fully **customizable key bindings** (see [KEY_CONFIG.md](KEY_CONFIG.md)) [[@yanganto](https://github.com/yanganto)] ([#109](https://github.com/gitui-org/gitui/issues/109)) ([#57](https://github.com/gitui-org/gitui/issues/57))\n- support scrolling in long commit messages [[@cruessler](https://github.com/cruessler)]([#208](https://github.com/gitui-org/gitui/issues/208))\n\n![scrolling](assets/msg-scrolling.gif)\n\n- copy lines from diffs to clipboard [[@cruessler](https://github.com/cruessler)]([#229](https://github.com/gitui-org/gitui/issues/229))\n\n![select-copy](assets/select-copy.gif)\n\n- scrollbar in long diffs ([#204](https://github.com/gitui-org/gitui/issues/204))\n\n![scrollbar](assets/scrollbar.gif)\n\n- allow creating new branch ([#253](https://github.com/gitui-org/gitui/issues/253))\n\n### Fixed\n\n- selection error in stashlist when deleting last element ([#223](https://github.com/gitui-org/gitui/issues/223))\n- git hooks broke ci build on windows [[@dr-BEat](https://github.com/dr-BEat)] ([#235](https://github.com/gitui-org/gitui/issues/235))\n\n## [0.9.1] - 2020-07-30\n\n### Added\n\n- move to (un)staged when the current selection is empty [[@jonstodle](https://github.com/jonstodle)]([#215](https://github.com/gitui-org/gitui/issues/215))\n- pending load of a diff/status is visualized ([#160](https://github.com/gitui-org/gitui/issues/160))\n- entry on [git-scm.com](https://git-scm.com/downloads/guis) in the list of GUI tools [[@Vidar314](https://github.com/Vidar314)] (see [PR](https://github.com/git/git-scm.com/pull/1485))\n- commits can be tagged in revlog [[@cruessler](https://github.com/cruessler)]([#103](https://github.com/gitui-org/gitui/issues/103))\n\n![](assets/tagging.gif)\n\n### Changed\n\n- async fetching tags to improve reactivity in giant repos ([#170](https://github.com/gitui-org/gitui/issues/170))\n\n### Fixed\n\n- removed unmaintained dependency `spin` ([#172](https://github.com/gitui-org/gitui/issues/172))\n- opening relative paths in external editor may fail in subpaths ([#184](https://github.com/gitui-org/gitui/issues/184))\n- crashes in revlog with utf8 commit messages ([#188](https://github.com/gitui-org/gitui/issues/188))\n- `add_to_ignore` failed on files without a newline at EOF ([#191](https://github.com/gitui-org/gitui/issues/191))\n- new tags were not picked up in revlog view ([#190](https://github.com/gitui-org/gitui/issues/190))\n- tags not shown in commit details popup ([#193](https://github.com/gitui-org/gitui/issues/193))\n- min size for relative popups on small terminals ([#179](https://github.com/gitui-org/gitui/issues/179))\n- fix crash on resizing terminal to very small width ([#198](https://github.com/gitui-org/gitui/issues/198))\n- fix broken tags when using a different internal representation ([#206](https://github.com/gitui-org/gitui/issues/206))\n- tags are not cleanly separated in details view ([#212](https://github.com/gitui-org/gitui/issues/212))\n\n## [0.8.1] - 2020-07-07\n\n### Added\n\n- open file in editor [[@jonstodle](https://github.com/jonstodle)]([#166](https://github.com/gitui-org/gitui/issues/166))\n\n### Fixed\n\n- switch deprecated transitive dependency `net2`->`socket2` [in `crossterm`->`mio`]([#66](https://github.com/gitui-org/gitui/issues/66))\n- crash diffing a stash that was created via cli ([#178](https://github.com/gitui-org/gitui/issues/178))\n- zero delta file size in diff of untracked binary file ([#171](https://github.com/gitui-org/gitui/issues/171))\n- newlines not visualized correctly in commit editor ([#169](https://github.com/gitui-org/gitui/issues/169))\n\n![](assets/newlines.gif)\n\n## [0.8.0] - 2020-07-06\n\n### Added\n\n- core homebrew [formulae](https://formulae.brew.sh/formula/gitui#default): `brew install gitui` [[@vladimyr](https://github.com/vladimyr)](<[#137](https://github.com/gitui-org/gitui/issues/137)>)\n- show file sizes and delta on binary diffs ([#141](https://github.com/gitui-org/gitui/issues/141))\n\n![](assets/binary_diff.png)\n\n- external editor support for commit messages [[@jonstodle](https://github.com/jonstodle)]([#46](https://github.com/gitui-org/gitui/issues/46))\n\n![](assets/vi_support.gif)\n\n### Changed\n\n- use terminal blue as default selection background ([#129](https://github.com/gitui-org/gitui/issues/129))\n- author column in revlog is now fixed width for better alignment ([#148](https://github.com/gitui-org/gitui/issues/148))\n- cleaner tab bar and background work indicating spinner:\n\n![](assets/spinner.gif)\n\n### Fixed\n\n- clearer help headers ([#131](https://github.com/gitui-org/gitui/issues/131))\n- display non-utf8 commit messages at least partially ([#150](https://github.com/gitui-org/gitui/issues/150))\n- hooks ignored when running `gitui` in subfolder of workdir ([#151](https://github.com/gitui-org/gitui/issues/151))\n- better scrolling in file-trees [[@tisorlawan](https://github.com/tisorlawan)]([#144](https://github.com/gitui-org/gitui/issues/144))\n- show untracked files in stash commit details [[@MCord](https://github.com/MCord)]([#130](https://github.com/gitui-org/gitui/issues/130))\n- in some repos looking up the branch name was a bottleneck ([#159](https://github.com/gitui-org/gitui/issues/159))\n- some optimizations in reflog\n- fix arrow utf8 encoding in help window [[@daober](https://github.com/daober)]([#142](https://github.com/gitui-org/gitui/issues/142))\n\n## [0.7.0] - 2020-06-15\n\n### Added\n\n- Inspect stash commit in detail ([#121](https://github.com/gitui-org/gitui/issues/121))\n- Support reset/revert individual hunks ([#11](https://github.com/gitui-org/gitui/issues/11))\n- Commit Amend (`ctrl+a`) when in commit popup ([#89](https://github.com/gitui-org/gitui/issues/89))\n\n![](assets/amend.gif)\n\n### Changed\n\n- file trees: `arrow-right` on expanded folder moves down into folder\n- better scrolling in diff ([#52](https://github.com/gitui-org/gitui/issues/52))\n- display current branch in status/log ([#115](https://github.com/gitui-org/gitui/issues/115))\n- commit msg popup: add cursor and more controls (`arrow-left/right`, `delete` & `backspace`) [[@alistaircarscadden](https://github.com/alistaircarscadden)]([#46](https://github.com/gitui-org/gitui/issues/46))\n- moved `theme.ron` from `XDG_CACHE_HOME` to `XDG_CONFIG_HOME` [[@jonstodle](https://github.com/jonstodle)](<[#98](https://github.com/gitui-org/gitui/issues/98)>)\n\n### Fixed\n\n- reset file inside folder failed when running `gitui` in a subfolder too ([#118](https://github.com/gitui-org/gitui/issues/118))\n- selection could disappear into collapsed folder ([#120](https://github.com/gitui-org/gitui/issues/120))\n- `Files: loading` sometimes wrong ([#119](https://github.com/gitui-org/gitui/issues/119))\n\n## [0.6.0] - 2020-06-09\n\n![](assets/commit-details.gif)\n\n### Changed\n\n- changed hotkeys for selecting stage/workdir (**Note:** use `[w]`/`[s]` to change between workdir and stage) and added hotkeys (`[1234]`) to switch to tabs directly ([#92](https://github.com/gitui-org/gitui/issues/92))\n- `arrow-up`/`down` on bottom/top of status file list switches focus ([#105](https://github.com/gitui-org/gitui/issues/105))\n- highlight tags in revlog better\n\n### Added\n\n- New `Stage all [a]`/`Unstage all [a]` in changes lists ([#82](https://github.com/gitui-org/gitui/issues/82))\n- add `-d`, `--directory` options to set working directory via program arg [[@alistaircarscadden](https://github.com/alistaircarscadden)]([#73](https://github.com/gitui-org/gitui/issues/73))\n- commit detail view in revlog ([#80](https://github.com/gitui-org/gitui/issues/80))\n\n### Fixed\n\n- app closes when staging invalid file/path ([#108](https://github.com/gitui-org/gitui/issues/108))\n- `shift+tab` not working on windows [[@MCord](https://github.com/MCord)]([#111](https://github.com/gitui-org/gitui/issues/111))\n\n## [0.5.0] - 2020-06-01\n\n### Changed\n\n- support more commands allowing optional multiline commandbar ([#83](https://github.com/gitui-org/gitui/issues/83))\n\n![](assets/cmdbar.gif)\n\n### Added\n\n- support adding untracked file/folder to `.gitignore` ([#44](https://github.com/gitui-org/gitui/issues/44))\n- support reverse tabbing using shift+tab ([#92](https://github.com/gitui-org/gitui/issues/92))\n- switch to using cmd line args instead of `ENV` (`-l` for logging and `--version`) **please convert your GITUI_LOGGING usage** [[@shenek](https://github.com/shenek)]([#88](https://github.com/gitui-org/gitui/issues/88))\n- added missing LICENSE.md files in sub-crates [[@ignatenkobrain](https://github.com/ignatenkobrain)]([#94](https://github.com/gitui-org/gitui/pull/94))\n\n### Fixed\n\n- error when diffing huge files ([#96](https://github.com/gitui-org/gitui/issues/96))\n- expressive error when run in bare repos ([#100](https://github.com/gitui-org/gitui/issues/100))\n\n## [0.4.0] - 2020-05-25\n\n### Added\n\n- stashing support (save,apply,drop) ([#3](https://github.com/gitui-org/gitui/issues/3))\n\n### Changed\n\n- log tab refreshes when head changes ([#78](https://github.com/gitui-org/gitui/issues/78))\n- performance optimization of the log tab in big repos\n- more readable default color for the commit hash in the log tab\n- more error/panic resilience (`unwrap`/`panic` denied by clippy now) [[@MCord](https://github.com/MCord)](<[#77](https://github.com/gitui-org/gitui/issues/77)>)\n\n### Fixes\n\n- panic on small terminal width ([#72](https://github.com/gitui-org/gitui/issues/72))\n\n![](assets/stashing.gif)\n\n## [0.3.0] - 2020-05-20\n\n### Added\n\n- support color themes and light mode [[@MCord](https://github.com/MCord)]([#28](https://github.com/gitui-org/gitui/issues/28))\n\n### Changed\n\n- more natural scrolling in log tab ([#52](https://github.com/gitui-org/gitui/issues/52))\n\n### Fixed\n\n- crash on commit when git name was not set ([#74](https://github.com/gitui-org/gitui/issues/74))\n- log tab shown empty in single commit repos ([#75](https://github.com/gitui-org/gitui/issues/75))\n\n![](assets/light-theme.png)\n\n## [0.2.6] - 2020-05-18\n\n### Fixed\n\n- fix crash help in small window size ([#63](https://github.com/gitui-org/gitui/issues/63))\n\n## [0.2.5] - 2020-05-16\n\n### Added\n\n- introduced proper changelog\n- hook support on windows [[@MCord](https://github.com/MCord)]([#14](https://github.com/gitui-org/gitui/issues/14))\n\n### Changed\n\n- show longer commit messages in log view\n- introduce proper error handling in `asyncgit` [[@MCord](https://github.com/MCord)]([#53](https://github.com/gitui-org/gitui/issues/53))\n- better error message when trying to run outside of a valid git repo ([#56](https://github.com/gitui-org/gitui/issues/56))\n- improve ctrl+c handling so it is checked first and no component needs to worry of blocking it\n\n### Fixed\n\n- support multiple tags per commit in log ([#61](https://github.com/gitui-org/gitui/issues/61))\n\n## [0.2.3] - 2020-05-12\n\n### Added\n\n- support more navigation keys: home/end/pageUp/pageDown ([#43](https://github.com/gitui-org/gitui/issues/43))\n- highlight current tab a bit better\n\n## [0.2.2] - 2020-05-10\n\n### Added\n\n- show tags in commit log ([#47](https://github.com/gitui-org/gitui/issues/47))\n- support home/end key in diff ([#43](https://github.com/gitui-org/gitui/issues/43))\n\n### Changed\n\n- close application shortcut is now the standard `ctrl+c`\n- some diff improvements ([#42](https://github.com/gitui-org/gitui/issues/42))\n\n### Fixed\n\n- document tab key to switch tabs ([#48](https://github.com/gitui-org/gitui/issues/48))\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nWe’re glad you found this document that is intended to make contributing to\nGitUI as easy as possible!\n\n## Building GitUI\n\nIn order to build GitUI on your machine, follow the instructions in the\n[“Build” section](./README.md#build).\n\n## Getting help\n\nThere’s a [Discord server][discord-server] you can join if you get stuck or\ndon’t know where to start. People are happy to answer any questions you might\nhave!\n\n## Getting started\n\nIf you are looking for something to work on, but don’t yet know what might be a\ngood first issue, you can take a look at [issues labelled with\n`good-first-issue`][good-first-issues]. They have been selected to not require\ntoo much context so that people not familiar with the codebase yet can still\nmake a contribution.\n\n[discord-server]: https://discord.gg/rZv4uxSQx3\n[good-first-issues]: https://github.com/gitui-org/gitui/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"gitui\"\nversion = \"0.28.0\"\nauthors = [\"extrawurst <mail@rusticorn.com>\"]\ndescription = \"blazing fast terminal-ui for git\"\nedition = \"2021\"\nrust-version = \"1.88\"\nexclude = [\".github/*\", \".vscode/*\", \"assets/*\"]\nhomepage = \"https://github.com/gitui-org/gitui\"\nrepository = \"https://github.com/gitui-org/gitui\"\nreadme = \"README.md\"\nlicense = \"MIT\"\ncategories = [\"command-line-utilities\"]\nkeywords = [\"cli\", \"git\", \"gui\", \"terminal\", \"ui\"]\nbuild = \"build.rs\"\n\n[workspace]\nmembers = [\n    \"asyncgit\",\n    \"filetreelist\",\n    \"git2-hooks\",\n    \"git2-testing\",\n    \"scopetime\",\n]\n\n[features]\ndefault = [\"ghemoji\", \"regex-fancy\", \"trace-libgit\", \"vendor-openssl\"]\nghemoji = [\"gh-emoji\"]\n# regex-* features are mutually exclusive.\nregex-fancy = [\"syntect/regex-fancy\", \"two-face/syntect-fancy\"]\nregex-onig = [\"syntect/regex-onig\", \"two-face/syntect-onig\"]\ntiming = [\"scopetime/enabled\"]\ntrace-libgit = [\"asyncgit/trace-libgit\"]\nvendor-openssl = [\"asyncgit/vendor-openssl\"]\n\n[dependencies]\nanyhow = \"1.0\"\nasyncgit = { path = \"./asyncgit\", version = \"0.28.0\", default-features = false }\nbacktrace = \"0.3\"\nbase64 = \"0.22\"\nbitflags = \"2.10\"\nbugreport = \"0.5.1\"\nbwrap = { version = \"1.3\", features = [\"use_std\"] }\nbytesize = { version = \"2.3\", default-features = false }\nchrono = { version = \"0.4\", default-features = false, features = [\"clock\"] }\nclap = { version = \"4.5\", features = [\"cargo\", \"env\"] }\ncrossbeam-channel = \"0.5\"\ncrossterm = { version = \"0.28\", features = [\"serde\"] }\ndirs = \"6.0\"\neasy-cast = \"0.5\"\nfiletreelist = { path = \"./filetreelist\", version = \"0.5\" }\nfuzzy-matcher = \"0.3\"\ngh-emoji = { version = \"1.0\", optional = true }\nindexmap = \"2\"\nitertools = \"0.14\"\nlog = \"0.4\"\nnotify = \"8\"\nnotify-debouncer-mini = \"0.7\"\nonce_cell = \"1\"\nparking_lot_core = \"0.9\"\nratatui = { version = \"0.29\", default-features = false, features = [\n  \"crossterm\",\n  \"serde\",\n] }\nrayon-core = \"1.13\"\nron = \"0.12\"\nscopeguard = \"1.2\"\nscopetime = { path = \"./scopetime\", version = \"0.1\" }\nserde = \"1.0\"\nshellexpand = \"3.1\"\nsimplelog = { version = \"0.12\", default-features = false }\nstruct-patch = \"0.10\"\nsyntect = { version = \"5.3\", default-features = false, features = [\n  \"default-syntaxes\",\n  \"default-themes\",\n  \"html\",\n  \"parsing\",\n  \"plist-load\",\n] }\ntui-textarea = \"0.7\"\ntwo-face = { version = \"0.4.4\", default-features = false }\nunicode-segmentation = \"1.12\"\nunicode-truncate = \"2.0\"\nunicode-width = \"0.2\"\nwhich = \"8.0\"\n\n[build-dependencies]\nchrono = { version = \"0.4\", default-features = false, features = [\"clock\"] }\n\n[dev-dependencies]\nenv_logger = \"0.11\"\npretty_assertions = \"1.4\"\ntempfile = \"3\"\n\n[badges]\nmaintenance = { status = \"actively-developed\" }\n\n# make debug build as fast as release\n# usage of utf8 encoding inside tui\n# makes their debug profile slow\n[profile.dev.package.\"ratatui\"]\nopt-level = 3\n\n[profile.release]\nopt-level = \"z\"  # Optimize for size.\nstrip = \"debuginfo\"\nlto = true\ncodegen-units = 1\n"
  },
  {
    "path": "FAQ.md",
    "content": "\n\n## <a name=\"table-of-contents\"></a> Table of Contents\n\n1. [\"Bad Credentials\" Error](#credentials)\n2. [Custom key bindings](#keybindings)\n2. [Watcher](#watcher)\n\n## 1. <a name=\"credentials\"></a> \"Bad Credentials\" Error <small><sup>[Top ▲](#table-of-contents)</sup></small>\n\nSome users have trouble pushing/pulling from remotes and adding their ssh-key to their ssh-agent solved the issue. The error they get is:\n![](./assets/bad-credentials.png)\n\nSee 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)\n\nNote 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.\n\n## 2. <a name=\"keybindings\"></a> Custom key bindings <small><sup>[Top ▲](#table-of-contents)</sup></small>\n\nIf 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)\n\n## 3. <a name=\"watcher\"></a> Watching for changes <small><sup>[Top ▲](#table-of-contents)</sup></small>\n\nBy 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.\n\nSee #1444 for details.\n"
  },
  {
    "path": "KEY_CONFIG.md",
    "content": "# Key Config\n\nThe default keys are based on arrow keys to navigate.\n\nHowever popular demand lead to fully customizability of the key bindings.\n\nCreate a `key_bindings.ron` file like this:\n```\n(\n    move_left: Some(( code: Char('h'), modifiers: \"\")),\n    move_right: Some(( code: Char('l'), modifiers: \"\")),\n    move_up: Some(( code: Char('k'), modifiers: \"\")),\n    move_down: Some(( code: Char('j'), modifiers: \"\")),\n\n    stash_open: Some(( code: Char('l'), modifiers: \"\")),\n    open_help: Some(( code: F(1), modifiers: \"\")),\n\n    status_reset_item: Some(( code: Char('U'), modifiers: \"SHIFT\")),\n)\n```\n\nThe config file format based on the [Ron file format](https://github.com/ron-rs/ron).\nThe location of the file depends on your OS:\n* `$HOME/.config/gitui/key_bindings.ron` (mac)\n* `$XDG_CONFIG_HOME/gitui/key_bindings.ron` (linux using XDG)\n* `$HOME/.config/gitui/key_bindings.ron` (linux)\n* `%APPDATA%/gitui/key_bindings.ron` (Windows)\n\nSee all possible keys to overwrite in gitui: [here](https://github.com/gitui-org/gitui/blob/master/src/keys/key_list.rs#L83)\n\nPossible values for:\n* `code` are defined by the type `KeyCode` in crossterm: [here](https://docs.rs/crossterm/latest/crossterm/event/enum.KeyCode.html)\n* `modifiers` are defined by the type `KeyModifiers` in crossterm: [here](https://docs.rs/crossterm/latest/crossterm/event/struct.KeyModifiers.html)\n\nHere 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.\n\n# Key Symbols\n\nSimilar to the above GitUI allows you to change the way the UI visualizes key combos containing special keys like `enter`(default: `⏎`) and `shift`(default: `⇧`).\n\nIf we can find a file `key_symbols.ron` in the above folders we apply the overwrites in it.\n\nExample content of this file looks like:\n\n```\n(\n    enter: Some(\"enter\"),\n    shift: Some(\"shift-\")\n)\n```\nThis 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))\n"
  },
  {
    "path": "LICENSE.md",
    "content": "MIT License\n\nCopyright (c) 2025 gitui-org\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "Makefile",
    "content": "\n.PHONY: debug build-release release-linux-musl test clippy clippy-pedantic install install-debug sort\n\nARGS=-l\n# ARGS=-l -d ~/code/extern/kubernetes\n# ARGS=-l -d ~/code/extern/linux\n# ARGS=-l -d ~/code/git-bare-test.git -w ~/code/git-bare-test\n\nprofile:\n\tCARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph --features timing -- ${ARGS}\n\nrun-timing:\n\tcargo run --features=timing --release -- ${ARGS}\n\ndebug:\n\tRUST_BACKTRACE=true cargo run --features=timing -- ${ARGS}\n\nbuild-release:\n\tcargo build --release --locked\n\nrelease-mac: build-release\n\tstrip target/release/gitui\n\totool -L target/release/gitui\n\tls -lisah target/release/gitui\n\tmkdir -p release\n\ttar -C ./target/release/ -czvf ./release/gitui-mac.tar.gz ./gitui\n\tls -lisah ./release/gitui-mac.tar.gz\n\nrelease-mac-x86: build-apple-x86-release\n\tstrip target/x86_64-apple-darwin/release/gitui\n\totool -L target/x86_64-apple-darwin/release/gitui\n\tls -lisah target/x86_64-apple-darwin/release/gitui\n\tmkdir -p release\n\ttar -C ./target/x86_64-apple-darwin/release/ -czvf ./release/gitui-mac-x86.tar.gz ./gitui\n\tls -lisah ./release/gitui-mac-x86.tar.gz\n\nrelease-win: build-release\n\tmkdir -p release\n\ttar -C ./target/release/ -czvf ./release/gitui-win.tar.gz ./gitui.exe\n\tcargo install cargo-wix --version 0.3.3 --locked\n\tcargo wix -p gitui --no-build --nocapture --output ./release/gitui-win.msi\n\tls -l ./release/gitui-win.msi\n\nrelease-linux-musl: build-linux-musl-release\n\tstrip target/x86_64-unknown-linux-musl/release/gitui\n\tmkdir -p release\n\ttar -C ./target/x86_64-unknown-linux-musl/release/ -czvf ./release/gitui-linux-x86_64.tar.gz ./gitui\n\nbuild-apple-x86-debug:\n\tcargo build --target=x86_64-apple-darwin\n\nbuild-apple-x86-release:\n\tcargo build --release --target=x86_64-apple-darwin --locked\n\nbuild-linux-musl-debug:\n\tcargo build --target=x86_64-unknown-linux-musl\n\nbuild-linux-musl-release:\n\tcargo build --release --target=x86_64-unknown-linux-musl --locked\n\ntest-linux-musl:\n\tcargo nextest run --workspace --target=x86_64-unknown-linux-musl\n\nrelease-linux-arm: build-linux-arm-release\n\tmkdir -p release\n\n\taarch64-linux-gnu-strip target/aarch64-unknown-linux-gnu/release/gitui\n\tarm-linux-gnueabihf-strip target/armv7-unknown-linux-gnueabihf/release/gitui\n\tarm-linux-gnueabihf-strip target/arm-unknown-linux-gnueabihf/release/gitui\n\n\ttar -C ./target/aarch64-unknown-linux-gnu/release/ -czvf ./release/gitui-linux-aarch64.tar.gz ./gitui\n\ttar -C ./target/armv7-unknown-linux-gnueabihf/release/ -czvf ./release/gitui-linux-armv7.tar.gz ./gitui\n\ttar -C ./target/arm-unknown-linux-gnueabihf/release/ -czvf ./release/gitui-linux-arm.tar.gz ./gitui\n\nbuild-linux-arm-debug:\n\tcargo build --target=aarch64-unknown-linux-gnu\n\tcargo build --target=armv7-unknown-linux-gnueabihf\n\tcargo build --target=arm-unknown-linux-gnueabihf\n\nbuild-linux-arm-release:\n\tcargo build --release --target=aarch64-unknown-linux-gnu --locked\n\tcargo build --release --target=armv7-unknown-linux-gnueabihf --locked\n\tcargo build --release --target=arm-unknown-linux-gnueabihf --locked\n\ntest:\n\tcargo nextest run --workspace\n\nfmt:\n\tcargo fmt -- --check\n\nclippy:\n\tcargo clippy --workspace --all-features\n\nclippy-nightly:\n\tcargo +nightly clippy --workspace --all-features\n\ncheck: fmt clippy test sort deny\n\ncheck-nightly:\n\tcargo +nightly c\n\tcargo +nightly clippy --workspace --all-features\n\tcargo +nightly t\n\ndeny:\n\tcargo deny check\n\nsort:\n\tcargo sort -c -w \".\"\n\ninstall:\n\tcargo install --path \".\" --offline --locked\n\ninstall-timing:\n\tcargo install --features=timing --path \".\" --offline --locked\n\nlicenses:\n\tcargo bundle-licenses --format toml --output THIRDPARTY.toml\n\nclean:\n\tcargo clean\n"
  },
  {
    "path": "NIGHTLIES.md",
    "content": "# Nightlies\n\n**Use with caution as these binaries are build nightly and might be broken**\n\nWhen you find problems please report them and always mention the version that you see in the `help popup` or when running `gitui -V`\n\n* [gitui-linux-aarch64.tar.gz](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-linux-aarch64.tar.gz)\n* [gitui-linux-arm.tar.gz](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-linux-arm.tar.gz)\n* [gitui-linux-armv7.tar.gz](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-linux-armv7.tar.gz)\n* [gitui-linux-x86_64.tar.gz](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-linux-x86_64.tar.gz)\n* [gitui-mac.tar.gz](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-mac.tar.gz)\n* [gitui-mac-x86.tar.gz](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-mac-x86.tar.gz)\n* [gitui-win.tar.gz](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-win.tar.gz)\n* [gitui-win.msi](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-win.msi)\n"
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\">\n<img width=\"300px\" src=\"assets/logo.png\" />\n\n[![CI][s0]][l0] [![crates][s1]][l1] ![MIT][s2] [![UNSAFE][s3]][l3] [![TWEET][s6]][l6] [![dep_status][s7]][l7] [![discord][s8]][l8]\n\n</h1>\n\n[s0]: https://github.com/gitui-org/gitui/workflows/CI/badge.svg\n[l0]: https://github.com/gitui-org/gitui/actions\n[s1]: https://img.shields.io/crates/v/gitui.svg\n[l1]: https://crates.io/crates/gitui\n[s2]: https://img.shields.io/badge/license-MIT-blue.svg\n[s3]: https://img.shields.io/badge/unsafe-forbidden-success.svg\n[l3]: https://github.com/rust-secure-code/safety-dance/\n[s6]: https://img.shields.io/twitter/follow/extrawurst?label=follow&style=social\n[l6]: https://twitter.com/intent/follow?screen_name=extrawurst\n[s7]: https://deps.rs/repo/github/gitui-org/gitui/status.svg\n[l7]: https://deps.rs/repo/github/gitui-org/gitui\n[s8]: https://img.shields.io/discord/1176858176897953872\n[l8]: https://discord.gg/rQNeEnMhus\n\n<h5 align=\"center\">GitUI provides you with the comfort of a git GUI but right in your terminal</h1>\n\n![](demo.gif)\n\n## <a name=\"table-of-contents\"></a> Table of Contents\n\n1. [Features](#features)\n2. [Motivation](#motivation)\n3. [Benchmarks](#bench)\n4. [Roadmap](#roadmap)\n5. [Limitations](#limitations)\n6. [Installation](#installation)\n7. [Build](#build)\n8. [FAQs](#faqs)\n9. [Diagnostics](#diagnostics)\n10. [Color Theme](#theme)\n11. [Key Bindings](#bindings)\n12. [Sponsoring](#sponsoring)\n13. [Inspiration](#inspiration)\n14. [Contributing](#contributing)\n15. [Contributors](#contributors)\n\n## 1. <a name=\"features\"></a> Features <small><sup>[Top ▲](#table-of-contents)</sup></small>\n\n- Fast and intuitive **keyboard only** control\n- Context based help (**no need to memorize** tons of hot-keys)\n- Inspect, commit, and amend changes (incl. hooks: *pre-commit*,*commit-msg*,*post-commit*,*prepare-commit-msg*)\n- Stage, unstage, revert and reset files, hunks and lines\n- Stashing (save, pop, apply, drop, and inspect)\n- Push / Fetch to / from remote\n- Branch List (create, rename, delete, checkout, remotes)\n- Browse / **Search** commit log, diff committed changes\n- Responsive terminal UI\n- Async git API for fluid control\n- Submodule support\n- gpg commit signing with shortcomings (see [#97](https://github.com/gitui-org/gitui/issues/97)))\n\n## 2. <a name=\"motivation\"></a> Motivation <small><sup>[Top ▲](#table-of-contents)</sup></small>\n\nI 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.\n\nUnfortunately popular git GUIs all fail on giant repositories or become unresponsive and unusable.\n\nGitUI provides you with the user experience and comfort of a git GUI but right in your terminal while being portable, fast, free and opensource.\n\n## 3. <a name=\"bench\"></a> Benchmarks <small><sup>[Top ▲](#table-of-contents)</sup></small>\n\nFor a [RustBerlin meetup presentation](https://youtu.be/rpilJV-eIVw?t=5334) ([slides](https://github.com/extrawurst/gitui-presentation)) I compared `lazygit`,`tig` and `gitui` by parsing the entire Linux git repository (which contains over 900k commits):\n\n|           | Time       | Memory (GB) | Binary (MB) | Freezes   | Crashes   |\n| --------- | ---------- | ----------- | ----------- | --------- | --------- |\n| `gitui`   | **24 s** ✅ | **0.17** ✅  | 10         | **No** ✅  | **No** ✅  |\n| `lazygit` | 57 s       | 2.6         | 25          | Yes       | Sometimes |\n| `tig`     | 4 m 20 s   | 1.3         | **0.6** ✅   | Sometimes | **No** ✅  |\n\n## 4. <a name=\"roadmap\"></a> Road(map) to 1.0 <small><sup>[Top ▲](#table-of-contents)</sup></small>\n\nThese are the high level goals before calling out `1.0`:\n\n* visualize branching structure in log tab ([#81](https://github.com/gitui-org/gitui/issues/81))\n* interactive rebase ([#32](https://github.com/gitui-org/gitui/issues/32))\n- no git-lfs support (see [#2812](https://github.com/gitui-org/gitui/issues/2812))\n\n## 5. <a name=\"limitations\"></a> Known Limitations <small><sup>[Top ▲](#table-of-contents)</sup></small>\n\n- no sparse repo support (see [#1226](https://github.com/gitui-org/gitui/issues/1226))\n- *credential.helper* for https needs to be **explicitly** configured (see [#800](https://github.com/gitui-org/gitui/issues/800))\n\nCurrently, this tool does not fully substitute the _git shell_, however both tools work well in tandem.\n\nThe priorities for `gitui` are on features that are making me mad when done on the _git shell_, like stashing, staging lines or hunks. Eventually, I will be able to work on making `gitui` a one stop solution - but for that I need help - this is just a spare time project for now.\n\nAll support is welcomed! Sponsors as well! ❤️\n\n## 6. <a name=\"installation\"></a> Installation <small><sup>[Top ▲](#table-of-contents)</sup></small>\n\nGitUI 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.\n\n<a href=\"https://repology.org/project/gitui/versions\">\n    <img src=\"https://repology.org/badge/vertical-allrepos/gitui.svg\" alt=\"Packaging status\" align=\"right\">\n</a>\n\n### Various Package Managers\n\n<details>\n  <summary>Install Instructions</summary>\n\n##### [Arch Linux](https://archlinux.org/packages/extra/x86_64/gitui/)\n\n```sh\npacman -S gitui\n```\n\n##### Fedora\n\n```sh\nsudo dnf install gitui\n```\n\n##### Gentoo\nAvailable in [dm9pZCAq overlay](https://github.com/gentoo-mirror/dm9pZCAq)\n\n```sh\nsudo eselect repository enable dm9pZCAq\nsudo emerge --sync dm9pZCAq\nsudo emerge dev-vcs/gitui::dm9pZCAq\n```\n\n##### [openSUSE](https://software.opensuse.org/package/gitui)\n\n```sh\nsudo zypper install gitui\n```\n\n##### Homebrew (macOS)\n\n```sh\nbrew install gitui\n```\n\n##### [MacPorts (macOS)](https://ports.macports.org/port/gitui/details/)\n\n```sh\nport install gitui\n```\n\n##### [Winget](https://github.com/microsoft/winget-pkgs/tree/master/manifests/s/StephanDilly/gitui) (Windows)\n\n```\nwinget install gitui\n```\n\n##### [Scoop](https://github.com/ScoopInstaller/Main/blob/master/bucket/gitui.json) (Windows)\n\n```\nscoop install gitui\n```\n\n##### [Chocolatey](https://chocolatey.org/packages/gitui) (Windows)\n\n```\nchoco install gitui\n```\n\n##### [Nix](https://search.nixos.org/packages?channel=unstable&show=gitui&from=0&size=50&sort=relevance&query=gitui) (Nix/NixOS)\n\nNixpkg\n```\nnix-env -iA nixpkgs.gitui\n```\nNixOS\n```\nnix-env -iA nixos.gitui\n```\n\n##### [Termux](https://github.com/termux/termux-packages/tree/master/packages/gitui) (Android)\n\n```\npkg install gitui\n```\n\n##### [Anaconda](https://anaconda.org/conda-forge/gitui)\n```\nconda install -c conda-forge gitui\n```\n\n</details>\n\n### Release Binaries\n\n[Available for download in releases](https://github.com/gitui-org/gitui/releases)\n\nBinaries available for:\n\n#### Linux\n\n- gitui-linux-x86_64.tar.gz (linux musl statically linked)\n- gitui-linux-aarch64.tar.gz (linux on 64 bit arm)\n- gitui-linux-arm.tar.gz\n- gitui-linux-armv7.tar.gz\n\nAll contain a single binary file\n\n#### macOS\n\n- gitui-mac.tar.gz (arm64)\n- gitui-mac-x86.tar.gz (intel x86)\n\n#### Windows\n\n- gitui-win.tar.gz (single 64bit binary)\n- gitui-win.msi (64bit Installer package)\n\n### Nightly Builds\n\nsee [NIGHTLIES.md](./NIGHTLIES.md)\n\n## 7. <a name=\"build\"></a> Build <small><sup>[Top ▲](#table-of-contents)</sup></small>\n\n### Requirements\n\n- Minimum supported `rust`/`cargo` version: `1.88`\n  - See [Install Rust](https://www.rust-lang.org/tools/install)\n\n- To build openssl dependency (see https://docs.rs/openssl/latest/openssl/)\n  - perl >= 5.12 (strawberry perl works for windows https://strawberryperl.com/)\n  - a c compiler (msvc, gcc or clang, cargo will find it)\n\n- To run the complete test suite python is required (and it must be invocable as `python`)\n\n### Cargo Install\n\nThe 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)\n\n### Cargo Features\n#### trace-libgit\nenable `libgit2` tracing\n\nworks if `libgit2` built with `-DENABLE_TRACE=ON`\n\nthis feature enabled by default, to disable: `cargo install --no-default-features`\n\n## 8. <a name=\"faqs\"></a> FAQs <small><sup>[Top ▲](#table-of-contents)</sup></small>\n\nsee [FAQs page](./FAQ.md)\n\n## 9. <a name=\"diagnostics\"></a> Diagnostics <small><sup>[Top ▲](#table-of-contents)</sup></small>\n\nTo run with logging enabled run `gitui -l`.\n\nThis will log to:\n\n- macOS: `$HOME/Library/Caches/gitui/gitui.log`\n- Linux using `XDG`: `$XDG_CACHE_HOME/gitui/gitui.log`\n- Linux: `$HOME/.cache/gitui/gitui.log`\n- Windows: `%LOCALAPPDATA%/gitui/gitui.log`\n\n## 10. <a name=\"theme\"></a> Color Theme <small><sup>[Top ▲](#table-of-contents)</sup></small>\n\n![](assets/light-theme.png)\n\n`gitui` should automatically work on both light and dark terminal themes.\n\nHowever, you can customize everything to your liking: See [Themes](THEMES.md).\n\n## 11. <a name=\"bindings\"></a> Key Bindings <small><sup>[Top ▲](#table-of-contents)</sup></small>\n\nThe key bindings can be customized: See [Key Config](KEY_CONFIG.md) on how to set them to `vim`-like bindings.\n\n## 12. <a name=\"sponsoring\"></a> Sponsoring <small><sup>[Top ▲](#table-of-contents)</sup></small>\n\n[![github](https://img.shields.io/badge/-GitHub%20Sponsors-fafbfc?logo=GitHub%20Sponsors)](https://github.com/sponsors/extrawurst)\n\n## 13. <a name=\"inspiration\"></a> Inspiration <small><sup>[Top ▲](#table-of-contents)</sup></small>\n\n- [lazygit](https://github.com/jesseduffield/lazygit)\n- [tig](https://github.com/jonas/tig)\n- [GitUp](https://github.com/git-up/GitUp)\n  - It would be nice to come up with a way to have the map view available in a terminal tool\n- [git-brunch](https://github.com/andys8/git-brunch)\n\n## 14. <a name=\"contributing\"></a> Contributing <small><sup>[Top ▲](#table-of-contents)</sup></small>\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md).\n\n## 15. <a name=\"contributors\"></a> Contributors <small><sup>[Top ▲](#table-of-contents)</sup></small>\n\nThanks goes to all the contributors that help make GitUI amazing! ❤️\n\nWanna become a co-maintainer? We are looking for [you](https://github.com/gitui-org/gitui/issues/2084)!\n\n<a href=\"https://github.com/gitui-org/gitui/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=gitui-org/gitui\" />\n</a>\n"
  },
  {
    "path": "THEMES.md",
    "content": "# Themes\n\ndefault on light terminal:\n![](assets/light-theme.png)\n\n## Configuration\n\nTo 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 don’t 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.\n\nThe 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:\n\n* `$HOME/.config/gitui/theme.ron` (mac)\n* `$XDG_CONFIG_HOME/gitui/theme.ron` (linux using XDG)\n* `$HOME/.config/gitui/theme.ron` (linux)\n* `%APPDATA%/gitui/theme.ron` (Windows)\n\nAlternatively, 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`.\n\nExample theme override:\n\n```ron\n(\n    selection_bg: Some(\"Blue\"),\n    selection_fg: Some(\"#ffffff\"),\n)\n```\n\nNote that you need to wrap values in `Some` due to the way the overrides work (as of 0.23).\n\nNotes:\n\n* rgb colors might not be supported in every terminal.\n* using a color like `yellow` might appear in whatever your terminal/theme defines for `yellow`\n* valid colors can be found in ratatui's [Color](https://docs.rs/ratatui/latest/ratatui/style/enum.Color.html) struct.\n* 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)\n\n## Preset Themes\n\nYou can find preset themes by Catppuccin [here](https://github.com/catppuccin/gitui.git).\n\n## Syntax Highlighting\n\nThe 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.\n\nExample syntax theme:\n```ron\n(\n    syntax: Some(\"InspiredGitHub\"),\n)\n```\n\nCustom 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:\n```ron\n(\n    syntax: Some(\"Blackboard\"),\n)\n```\n\n[filmgirl/TextMate-Themes](https://github.com/filmgirl/TextMate-Themes) offers many [beautiful](https://inkdeep.github.io/TextMate-Themes) TextMate themes to choose from.\n\n## Customizing line breaks\n\nIf you want to change how the line break is displayed in the diff, you can also specify `line_break` in your `theme.ron`:\n\n```ron\n(\n    line_break: Some(\"¶\"),\n)\n```\n\nNote that if you want to turn it off, you should use a blank string:\n\n```ron\n(\n    line_break: Some(\"\"),\n)\n```\n## Customizing selection\n\nBy default the `selection_fg` color is used to color the text of the selected line.\nDiff line, filename, commit hashes, time and author are re-colored with `selection_fg` color.\nThis can be changed by specifying the `use_selection_fg` boolean in your `theme.ron`:\n\n```\n(\n    use_selection_fg: Some(false),\n)\n```\n\nBy default, `use_selection_fg` is set to `true`.\n"
  },
  {
    "path": "assets/expandable-commands.drawio",
    "content": "<mxfile host=\"app.diagrams.net\" modified=\"2020-05-23T13:11:59.516Z\" agent=\"5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Safari/605.1.15\" etag=\"nKCWIzIC1T2-_TwD7yCE\" version=\"13.1.3\" type=\"device\"><diagram id=\"KDklGPkv8WkujI4HOg4Q\" name=\"Page-1\">5Zhdr5MwAIZ/DZcnGe2Acek+1GiciYsx8a6HdtBYKJZOxvn1tqOMsXLiNGESdrPRtx/Q9wH6Fgeu0uM7gfLkE8eEOWCGjw5cOwAEC1/9aqGqBQ+GtRALimvJbYUdfSFGnBn1QDEpOg0l50zSvCtGPMtIJDsaEoKX3WZ7zrpnzVFMLGEXIWar3yiWSa0uQNDq7wmNk+bMrm/ml6KmsZlJkSDMywsJbhy4EpzL+ig9rgjT3jW+1P3evlJ7vjBBMnlLh+rj9oO/3e23n+HL5vv8iX3FX57MKL8QO5gJm4uVVeOA4IcMEz3IzIHLMqGS7HIU6dpSIVdaIlOmSq46NMMRIcnx1et0z7NXdw3hKZGiUk2aDg366qpctv5Dz2jJhfdgZkRkmMfnsVtb1IFx5i9cAiN0CVy5BHpcgj0uucFQLkHLpYinKZVKc7xl5HhryzU1f9m1ppCC/yArzrhQSsYz1XK5p4xdSYjROFPFSPlFlL7UblL13L4xFSnFWJ+ml0WX1gA4QGjjCHrv2YFgzC0YgugZ1jDW04YRjgyGZ8EopF53Tixqz6bMA/gj4+FbPFIuDI7iZMaUccDgzzgW98QRjG95nfujCyGLEboU/GsI8YZyKXzgEHKN4/xS/V/v2eahecgU4s3HRsPe7D1UDPHdsQGx95WMFMWj5JDghj3TXXOIa+d0hLGjvzjprnF2Cok+SrUd2XOh/xQpOmlK1qLSQym8KyU7vSeE5fVTk0yahXfDRqo/lA7Fwo7uPw9N2HK8zbTzlrWgDLevVcX2S/Gp7uJzO9z8Bg==</diagram></mxfile>"
  },
  {
    "path": "assets/log-commit-info.drawio",
    "content": "<mxfile host=\"app.diagrams.net\" modified=\"2020-06-02T16:58:00.925Z\" agent=\"5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Safari/605.1.15\" etag=\"XbmHwEoPIuusTyLd3JQZ\" version=\"13.1.13\" type=\"device\"><diagram id=\"t1-bsFE1bwoXy9pVcyMy\" name=\"Page-1\">7VzbcuI4EP0aV+0+QNmyxeUxQJjULsluLTNJzb6kFCzAE9vy2CKQfP1KvttSKmQLYQ+Qh2BaQrLPabVa3Q2aOfZ2X0IUrG+JjV0N6PZOMycaAEA3DfbCJa+JpG8ME8EqdOxEZBSCufOGU6GeSjeOjaNKR0qIS52gKlwQ38cLWpGhMCTbarclcauzBmiFBcF8gVxR+uDYdJ1IB6BfyG+ws1pnMxu99Pk8lHVOnyRaI5tsSyLzWjPHISE0ufJ2Y+xy8DJcks9N32nNbyzEPt3nA4PHR3ezfQBvsEP/Ivff/ny5HnZ6ySgvyN2kD5zeLH3NEAjJxrcxH0TXzNF27VA8D9CCt24Z50y2pp7L3hnsUryp9D5fcEjxriRKb/ILJh6m4SvrkumMlQKWaoyVqcK2wN/op7J1GftBKkQp56t87AIWdpEi8wmUjBaiZFRRMiUomQMJSmCoCiWzhSiBGkqWRJekKFmqUIICSlcbuiahZl4x8ZQQjd9zD3kcDZfdzugJhZrFLBfpLoiXN61oDFEv6RKyq1gyQRQnQwGLveo6/2eI/eY3V0k33UBP5vIHeo5s1B9E9lAgjWFNq8xENCTPeExcftsTn/is52jpuG5NhFxn5bO3Ll7yEThvDjOtV6mYkuAdLajqScQaHX81i0eZwELylQ/QuK5AVarSb/+CsvSmQRoIIN3iKOK7+gH0WAPmMv77WJk9x7b5THvo8yF4MGvKOhR5gDK7poqGoUDDPY5v2CX+ik/o2+w/3gUhY8d5wbHL5nkO5a5SRpje7Xbjj4TY4xNGwYa/2pwN5gS2zjApWVBwvwWlbIMygEDlOGWqmQWlfvGY4uKxjrl4DNEpGLsk4itCg6PraKHBiQLtXzAIcdi8LastATgQ6RgelQ5xT2nfxgv33HgNUxlMos2fOu5h7PQvs/FaEi/RPKayZgPXWDBO2V7AKgW9hs03EE++nALRaJwuBRKTrYqCO++Ph1v9bvbPj+nu+/zf2f3wedQRfZbGLbZVj9DIzpOyONZBXDspSqKeThxmYk/Fr7PqJ3iJXweOqZVW+7Sy7kYcNbgqBUn0fRsHibk2TUZXpTC1MFLfyZWikfCqFKY2xczag4p4oEnCvlo9TKzPKQ7WyGdXE2be+VNMs+BxFMTyAsnezw1PXo0WySbAo8nh6gn9pvNzK2B3qkuvfueXHFl9SXzaWSLPYRPFH/eIT6KYi0qXKE4LxtHqYFfMm8WyNQDrGUjIwOHSOL2Wv8vAgjFcTDLh1/zGIMcHMow/6mvkfTOl+F/DgGKYhJS8pWhIgM4bMrXlAj5VRXW5MFZeLo/Vl0uM+G1pqkRn93mYvCl/jkKxIbcAec9h8dz6ayGGJXGi+MXYg1JbugAKZKxSI1sIecOqNH8d5fhtDnVZWFWAtJ+gKYmCs6WV6HimVgd331txhBXN9r6xBGWOkhhJuCTFmk2KNZEEk6pG5nm1aUeXBX3kwTdlO7oh1iScQ96rdqa2JCc8VWkvOQ1i4OGS9zroElK3gsRoyInlueoBqCOmueSIi/GQBPHGUgYKfKv+x6Af10KJ8RWIlj08uJz9Lme/y9nv7M9+/T33YXUGSoxszphjpMW1D0m665SrHwRGjln+ICekhTFUuG+dobI0haQo5PSrHerHLUmVoapiBzkJ8pKTUyx2yBRfknk6bmZdXl8imoTTgbzpcxIQYzvNGeBsm4LtS4lmsJTrGTBFjns+zgNsm/MgqYVqge7ukac2ezKYlEWfwBnEQmDbYiFA9GxTH46bi04tjXTShsPQPzYc0uI0ZdyI/vWcIrpJyTFiOlJ+/KcoUHNIbws9gsXKotUleqQGSxk9oued50+zoMmMrBKyQEJWFlh5KqIq50NgvRaxaQJN0Y8/4/UlhMEap0fm81/W1/sENri+kD79ubu9ebv7vvW+RnT+2Pu2lf5GQDl3kjC0f+rlyid0zZAG+hT5Cy7JspN6nuh/J3R9SdlcUjYnnLJBEbJtZP96mRvBvEmM4CdcQknmRn6IVWXyxKP+J6qKPzBqF+t1sV4nab3O3mxJio2Pa7bEyFviWM+c6CC1Xwo8Zm6Y0t9G42AfhBirV/0OJ5B8jdaQZfcO8YVyKTFipVKZGDYrt9F5hP9siBr2uzWmIOhKfiVkCEWuLKiIK7FoI9/J/ybBJjgffkwdHJUfLd1nirbSDmNe/wc=</diagram></mxfile>"
  },
  {
    "path": "assets/options.drawio",
    "content": "<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>"
  },
  {
    "path": "assets/stashing.drawio",
    "content": "<mxfile host=\"app.diagrams.net\" modified=\"2020-05-19T12:41:55.023Z\" agent=\"5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Safari/605.1.15\" etag=\"G_7kN2nuUboj_f0S3bPh\" version=\"13.1.3\" type=\"device\" pages=\"2\"><diagram id=\"hAseTAaTBIQ-p2lVXsqS\" name=\"Page-1\">7Vlbk5owFP41zLQPO0NAUB+97LZ92Je1l+coEegGQiGu2l/f3CBEcNzuiLK2L5p8JyTk+87JSYLlzpLdpxxm0SMJELYcO9hZ7txynOHIZ78c2EvAc8cSCPM4kBDQwCL+jRRoK3QTB6gwGlJCMI0zE1yRNEUramAwz8nWbLYm2Bw1gyFqAIsVxE30RxzQSKIjZ6jxzygOo3Jk4Kv5JbBsrGZSRDAg2xrk3lvuLCeEylKymyHMuSt5kc89HLFWL5ajlL7mgadvOMToyV8+foFPYA6+D4Y/71QvLxBv1ITVy9J9yUBONmmAeCe25U63UUzRIoMrbt0yyRkW0QSzGmBF1R3KKdodfU9QzZ55DSIJovmeNSkfsBVhpceo6lbT7/oKi2rUOyUIleRh1bVmhRUUMX9BktNDkoBJ0qhJUuV4BkmgK5LcBkkLCummaFDFJk1NPgqak2c0I5jkDElJylpO1zHGBxDEcZiy6oqRhBg+5RTGLFYnypDEQcCHaRXAlKgDDVocddAmQVcKDNoUKKI4DXuqwZqkdKFeCpxDEO+kIK0LR1eC+E1B4AvqqRgdBIQzvnJEjPu3cA/8Yc+yWylar1gamiy1pDe31ZM6S2+guVO69fx2KMK18xto24f5mJNdZDBl5ZCKmUtsWQIiC7IzhMLZ2MvDtgwzu+ilpvV8aZ9BYO+0wBfNl6C5h4yYdJbDurO/xgmSpcmGRoxiURbapjDpIqtitKa9C8K2jf5lo7C5zZxkGd6boVhTwv+14Ufc6UqyPGHGPFzCD+wFbSlia+kjL3IKbe72d2uYxGwQ8XhCUlII3o0mhbg84A3sbKfH1UHtHV5TeGzeHBWH8KpW8uAJJhgy52X+Yh6fucfoO9UWVG1Lqd/UjaO7kXRXljL3lGZJd2UWzs9q0v05DES1HgIcFR7PDToMDFiGAofU+sVBMyC4UYUEN9aCgltEWHC8DAwO2gKpzU0GyGvYq0wVcTpkPB40uiXQTNt7jXs1WAZRZRrUTCqWtBQ1Gyw0HtaGP1RVVCtp66DpcKpdwzPfU27qYKm7+vEBeI2lbp6T7HYV8MBpBbyLKtA8Qj/EGIFbzfavuda7bAgMWwVonhBvVIDqMvp6Coz+b4pPZYrrq1R2fLZLlAO+z0BadWFyfG2pvL/O2bgzzlo+PnnTneXNBW/Cye1NSnO4euY7x9v05bfK0p0rt1yzNGRh/+G/o0nb+nJhUVo/r8lrrVvdjTqnv69ddDPqvOsPbGe5MASXujC01BlZ22qnY/f+Dw==</diagram><diagram id=\"o9nFayAMEWxc9pClOnIG\" name=\"Page-2\">7Vxbm5rKEv01edzn46KT4dEr4BZmvKK8nI+LERDEM6ICv/6s4qbOOMkkMdl7f5skRm2a7urqqlWrurE/8Z0gFl+MnaOE9sr/xDF2/InvfuK4z48P+J8KkrygyQt5wfrFtfMi9lwwcdNVUcgUpQfXXu2vKkZh6Efu7rrQCrfblRVdlRkvL+HputqX0L/udWesV28KJpbhvy3VXDty8tJH7vO5XFq5a6fsmX0oxhcYZeViJHvHsMPTRRHf+8R3XsIwyj8FcWflk+5KvSi7l9Pmj6cvf3Y1o20vV8J/48MfeWP977mlGsLLahvdt2kub/po+IdCX8VYo6RUIIa9o49ukGm6fVy9RC70OzTMlf8c7t3IDbe4boZRFAao4NOFtmFt1i/hYWt3Qj98yZriv2R/Ltpo+e6a7o3CHUqN/S63gC9uvILU7azLVlnKlCX4bBuR8Ylv5V+5/v64/sS14wDj7TxLKqcn7YapxQcrZVxDGjNWNzwOeZu3kyavJM2jFVhHxWudlI6Q2oHlypITmWIzfdo6e0NrvjxPBqEtjU9P7uMRd/HDrZUOAyHRk8f4abppDvm8nuy2OX0xSA1NODxP5Hjo9dCWvdOlcYjvvOI5p1VHXhvifKdzDoOyRPX6WZkd+L7NDI6rLuMOp62DOmlFSqfByl0reeq0UI/aUpmVFvuQx11q6ovOD4621twMNfVoikLy5LYTnZNDxZuxQ7pnOtvLXZlVJ5vPstd41MV5YCWP0M2zaNH/0sBf8qM19alAhtU0f7c5f2OLa0H2FE55I2/LlUV9Z4onQXbVQM/+LjPZrEB40dGGnEKXXdKnvLZEYWdux77lsr4dzA+2pAhy0N+bHfn47MWJrqmMLFFfvRP010Qf6bUMcvJaBmX6y2Wgd97QxowB3ahd6s/xDc0ObdLVpQ6C5tEMZld9m4Fw0Cff7HcrS+NkqTVTHbZk4j5bGjjmVt0vF2P/zw5zGnqzdW4LGxf1U+iEWXLrbP50WC1ppPm8DHbZLFr8uGmKM/QgkBUGsAgeFsEZC3W3DGL/KWkz+sJhhlof1mofcG2vazrzlLwnaTmyyoJjaNSHZkJd87eGNMo0pnSozsC3FvMdZNqMg7ljS/NEX1AbDmNLrYdhIsBvrIOdKgeTH2yH7HgyZFVtnPbiIaf7GDFrBTNvDPl0aMTQBtCuz6wmcjba8c52P+DD8MWnidDA9cikea80Mt5ZfBt63Txg3vb6Yv1ZFnMtDDUfPbIptMFC944ezNZLDhoQ5wk09j97oTKGpjczH4OGzO3oQZbmB4vrB/qk7UFaaFX+XGrpyW2lT1K7gXtLLT3IXfh4dw8t9ze66B/0hGVMvoXy3smSSBbWsQJ7Z7psPiNuK1YW1MbF6FNb8vdkXctAaAwxp6sALaXheklIBGmHGqHHfg1rOtgdNjUWOx8tnW7M+1nqzq0eIGNqibGzmoZr4Bphm4/W9yanvqC1wOSaTC7BjMaQ5GPQoZ9RpAf9yNDiJnqRdS2GJY53S66H6yVGtbzML7uZZEXbpW+Fa2XSIL2fTM7P5mCozTmS1qKRavEe13gDlqFPWMjY93TIaJL/YHR0Dda1GS7sZLmAfkWyC9YxoRXZa05tsc/YC+VxyGG8wMO372+8KkPJsb+SCpRM4fnShuyegR8EGOkeflGNiMoy7Wi6Y2r+nhB1Is738ClYkepU13PtUTukPbSZ9REBDTwDWgLCQ4vjDazXffZVJvOP9HSERhp2F5LyKm8sxp5Bn7nXWK5OSylbl977Bsvpux74e7N7gVn8ODG5iGLMAZKSt9+YC1ghRi67RSxZF8gVQPsR7gQS9uEhsWOLs/Aymv2Z+7N0lp6wj/zE3AiuEcw9u3O2Q5PXfSvQ9yZvwef0ncX5LjTy+aytGGNpYJ5jX9+OPr8nazm2c0wpUOsS06et04DGH8xjW/Mh01jUNbIXINtXMGfel48zVpeUae+ol8g3UfsribCCkNffY6zNPP76XMktPsApQltkoePbfqkD0cnbCx1E6MM3F8ptbBJ3LOZ1p2830ZITSD8H8jVwh3Sp6T5808/Q39uXOoJfzxq5X1/4ZoZJ7dMSlgm/3Q4X46NF5dN2hm/wQfAPH5iazQe1EWdtVGM/8xeTX0eYzwdqB1jhkt9C1ojQ7S3GtG7M+VnmG+1vEPVOptgnHNqZQYzXDG3HxyW3r7CK+if8UKaD0wVyBkuN3ZkSYkXP8U1J923R95ZT6JoTYIvyg9xRToXHo6287dKvCLuHHsWD9tHI9R/pC51kpRjjgDNU/dtceQ/Gz8Wbn8S21KDYApQgXTx7N95f+xr8L0OUTrsBPGhUHHA6uuBX1Vjd1xgPPV9ivHutV2pnkHPbrA/IGqgh9Id4zO50cbwnnyx0zleMIhggTowK1FNDso/njpBFMBqDxds7W1TDbDxp49HKMaORsZNSfm9U87eav/0G/iazSl43Wi4GW0NrANvnQYaV0+wemndg0fJiLGBpOTsDbiCuiQOMPZs5wsS32IR+K/8R4bteeDG+1o32L3XlH0ySd4K2RcEz0jfsMFa27TPeVqwlXF+yFlz3gE9HC31XeRXhfd72hZ56B8Xb37LGB5ovwiqbc+C/swgyOktudoVjNgd8AhaAPSAutsnSgPengqXOuD6nT38Y3dIzOsx+J3dz7GAGvpbVIV+gNYYzDnSE7XUGqnBq8ldn4gpfyZBeMcj0LZJmGXOlS72QImctg6PBza4jyI2oCH1RVHSH12ieqql1i8UyNYutWezHWaxyBxar8HlEqLPxv1s2vuGL9bpYvUCBS9R7xekFeXPB6c/8M5ufjCEhHqLNco4OS85B3IP3aT5if+xfSf5hVB/xt1F9ebpmY/IFmlbIdUbSitF9HUV12FiOXaRBRpC3OU9FPoSZWufryjk/3ZFHnzl1TNhHs3+YcoP/5fJXOdURUSOBxx5tzL4d+MTyYKFv+UnOgfbvyVmOq+LnJWpdoHrJVMFC+6wB9NJ7JcucfwV1hNnMFabzjRUrEyEokU9mIJ9IiOK70O3OljYVb1oBncztV5hu2msCyY4r0WFM7eS+k2OCrc8JVXIduCz0OY/IS26hk7Gdp7C/YDVhPfJe8qh3+Guho3CtTp0MnS7zzMzTpbaDXIq4ZGSLwinjdO9wUrVrNep8/EY+LtkhRR6MD1FB9a+4p2TDh5Cr8wOHosQZ3TLuTah3wHuVr+oZoskFJuRXf5izskrX+ovWG4uRfJy3vscZkzr7rrPvd7LvDDVhm7C3AXOJdor3cd6mdr/C237lLkox//+mHNygVcbOaT0X/UifnNajBaQDfgw77WIFsgnpZz/F6axmwemYu3I6rxkSqkHSmaGxPt7b9mJM65F7uZ8jngm2TCsMH9tlgaQl+8wxrs5R6xz1V+eo3uy9HeNUTZ16xfGuaDdawMqlzSPdaV7GjpJ3SoPtMuiFSqe1BydeDztgZEBHpbNZ3xqhqQnoIeeY8o0YQ8zs2Z2lI45tjIlvio5qcrC2ziwdT1DOD44mP/IVZoaXGq16sCbx5Ctee6dTnc4sAbvCd9lXNstv1Ovl9TxYGTwPFoC6Iyo7DaZyMpgOxMFU7ePVNRdOVwcKofxkaFW9JK8nn2ywMlubFeWDMj8u28/1Nin63WaIgrqDzOvQfk+hsW0LBjjLVyaoPEcTXAuILcbdzCKgn7GYcStqo8i3qKzwLNLfFmi2pZWO/n6Uey3qjkuGS/flqxbzfC6or4zfd+je/JkqPR8ndKF2zuOcJ+QZVJ/KIHeiuHghB7PI6l7rttK/7ZscLFMaNKmd1blerHTKNjalXtlzWR5Ryv4QZUqdlh5QzUV1D6JwWV/n5kdTmzPWdvPKJjA3G/1sF+cyRCSBKeedbABzL5F3GD0WWcuc5qt/h8wmfZqWmY3M/sa1m7YltferyewwdBvfs34TV9nN673FaZ3d1NnNh/cWmXvsLardXr2Oc891nA2YON/6iaxF7Y6SKhf4fU9PQIqxBDbX/XDOUj0ZplyhWIFq9Up0vRJ9tRKd5T6IsGB0id29zFnk73hKYvT+U67ben3mnhnLOCDt9X5m/WUqFwgxuuuK8/OGEHEWf++zXPmzrPVqcs23ftNqsvfeanKxs/5LOFexqvZv2v8XIRey0h/OIGPFLRDCuyvfqlBU/qHdrxqvarz6e+BVvfv1j9794v7mu195xnjj95q9+NXuV/Ny90vmimdE3csdsCt++a1dsB+Tot6Dq/fgivw3Q3H4/9ixSI/n1TnEqA8/ZcW8++S/N6qZ4j2ZornqM+tstrg1zSL5m2dw8w14GEWenJHd+vRmDQmxKotjOWuTmYpDdu/NITXesWnmCVnJSgkzlkAa/eNrdWdsY2tsq7HtN2IbV2PbX4Ft9K7fF9funhtf4xreNft7MG36zgkb9S5qnSX/kl3Ur5ywUf9C896/0MxwzPGtH/8Neqyks3/IE+8Xz4TUv+mpd1L/st/01M+C3PVZkOtzJ36Kh42YCiF+6zMhBQ6T/XwDz4p91zNWbH8hzyBG5nyVkXVb++EU+EVMBJHLSHch5AhKbIDvHYcc7JHbH3TcB2ayMxOMip7T5QcMuOfRlCgOCAcrEZz8GdDYt5CJqV6mcQYZTINmhZ5oANcGLg2+wGahyYwJFLP3fqx4OyPtbEaKc5f88veoGdZ5Tc3Q5jzsc62LAuQgGyQeOT8hAieIeYFN2Xcf+kvIpnvX8+W83iW3eH37vKbjE+lfuziucfUSreJX51t+43BItjqxMojFVRisopcE9xWtlGdcFod8fi6+ns4nZjaFosy5OC2TY9n/MI3isM7ioM511fj5MEp8KM6jLL+ej83Mrl2cPcr3/g8=</diagram></mxfile>"
  },
  {
    "path": "asyncgit/Cargo.toml",
    "content": "[package]\nname = \"asyncgit\"\nversion = \"0.28.0\"\nauthors = [\"extrawurst <mail@rusticorn.com>\"]\nedition = \"2021\"\ndescription = \"allow using git2 in a asynchronous context\"\nhomepage = \"https://github.com/gitui-org/gitui\"\nrepository = \"https://github.com/gitui-org/gitui\"\nreadme = \"README.md\"\nlicense = \"MIT\"\ncategories = [\"concurrency\", \"asynchronous\"]\nkeywords = [\"git\"]\n\n[features]\ndefault = [\"trace-libgit\"]\ntrace-libgit = []\nvendor-openssl = [\"openssl-sys\"]\n\n[dependencies]\nbitflags = \"2\"\ncrossbeam-channel = \"0.5\"\ndirs = \"6.0\"\neasy-cast = \"0.5\"\nfuzzy-matcher = \"0.3\"\ngit2 = \"0.20\"\ngit2-hooks = { path = \"../git2-hooks\", version = \">=0.6\" }\ngix = { version = \"0.78.0\", default-features = false, features = [\n    \"max-performance\",\n    \"revision\",\n    \"mailmap\",\n    \"status\",\n] }\nlog = \"0.4\"\n# git2 = { path = \"../../extern/git2-rs\", features = [\"vendored-openssl\"]}\n# git2 = { git=\"https://github.com/extrawurst/git2-rs.git\", rev=\"fc13dcc\", features = [\"vendored-openssl\"]}\n# pinning to vendored openssl, using the git2 feature this gets lost with new resolver\nopenssl-sys = { version = '0.9', features = [\"vendored\"], optional = true }\nrayon = \"1.11\"\nrayon-core = \"1.13\"\nscopetime = { path = \"../scopetime\", version = \"0.1\" }\nserde = { version = \"1.0\", features = [\"derive\"] }\nssh-key = { version = \"0.6.7\", features = [\"crypto\", \"encryption\"] }\nthiserror = \"2.0\"\nunicode-truncate = \"2.0\"\nurl = \"2.5\"\n\n[dev-dependencies]\nenv_logger = \"0.11\"\ninvalidstring = { path = \"../invalidstring\", version = \"0.1\" }\npretty_assertions = \"1.4\"\nserial_test = \"3.3\"\ntempfile = \"3\"\n"
  },
  {
    "path": "asyncgit/README.md",
    "content": "# asyncgit\n\n*allow using git2 in an asynchronous context*\n\nThis crate is designed as part of the [gitui](http://gitui.org) project.\n\n`asyncgit` provides the primary interface to interact with *git* repositories. It is split into the main module and a `sync` part. The latter provides convenience wrapper for typical usage patterns against git repositories.\n\nThe primary goal however is to allow putting certain (potentially) long running [git2](https://github.com/rust-lang/git2-rs) calls onto a thread pool.[crossbeam-channel](https://github.com/crossbeam-rs/crossbeam) is then used to wait for a notification confirming the result. \n\nIn `gitui` this allows the main-thread and therefore the *ui* to stay responsive.\n\n"
  },
  {
    "path": "asyncgit/src/asyncjob/mod.rs",
    "content": "//! provides `AsyncJob` trait and `AsyncSingleJob` struct\n\n#![deny(clippy::expect_used)]\n\nuse crate::error::Result;\nuse crossbeam_channel::Sender;\nuse std::sync::{Arc, Mutex, RwLock};\n\n/// Passed to `AsyncJob::run` allowing sending intermediate progress notifications\npub struct RunParams<\n\tT: Copy + Send,\n\tP: Clone + Send + Sync + PartialEq,\n> {\n\tsender: Sender<T>,\n\tprogress: Arc<RwLock<P>>,\n}\n\nimpl<T: Copy + Send, P: Clone + Send + Sync + PartialEq>\n\tRunParams<T, P>\n{\n\t/// send an intermediate update notification.\n\t/// do not confuse this with the return value of `run`.\n\t/// `send` should only be used about progress notifications\n\t/// and not for the final notification indicating the end of the async job.\n\t/// see `run` for more info\n\tpub fn send(&self, notification: T) -> Result<()> {\n\t\tself.sender.send(notification)?;\n\t\tOk(())\n\t}\n\n\t/// set the current progress\n\tpub fn set_progress(&self, p: P) -> Result<bool> {\n\t\tOk(if *self.progress.read()? == p {\n\t\t\tfalse\n\t\t} else {\n\t\t\t*(self.progress.write()?) = p;\n\t\t\ttrue\n\t\t})\n\t}\n}\n\n/// trait that defines an async task we can run on a threadpool\npub trait AsyncJob: Send + Sync + Clone {\n\t/// defines what notification type is used to communicate outside\n\ttype Notification: Copy + Send;\n\t/// type of progress\n\ttype Progress: Clone + Default + Send + Sync + PartialEq;\n\n\t/// can run a synchronous time intensive task.\n\t/// the returned notification is used to tell interested parties\n\t/// that the job finished and the job can be access via `take_last`.\n\t/// prior to this final notification it is not safe to assume `take_last`\n\t/// will already return the correct job\n\tfn run(\n\t\t&mut self,\n\t\tparams: RunParams<Self::Notification, Self::Progress>,\n\t) -> Result<Self::Notification>;\n\n\t/// allows observers to get intermediate progress status if the job customizes it\n\t/// by default this will be returning `Self::Progress::default()`\n\tfn get_progress(&self) -> Self::Progress {\n\t\tSelf::Progress::default()\n\t}\n}\n\n/// Abstraction for a FIFO task queue that will only queue up **one** `next` job.\n/// It keeps overwriting the next job until it is actually taken to be processed\n#[derive(Debug, Clone)]\npub struct AsyncSingleJob<J: AsyncJob> {\n\tnext: Arc<Mutex<Option<J>>>,\n\tlast: Arc<Mutex<Option<J>>>,\n\tprogress: Arc<RwLock<J::Progress>>,\n\tsender: Sender<J::Notification>,\n\tpending: Arc<Mutex<()>>,\n}\n\nimpl<J: 'static + AsyncJob> AsyncSingleJob<J> {\n\t///\n\tpub fn new(sender: Sender<J::Notification>) -> Self {\n\t\tSelf {\n\t\t\tnext: Arc::new(Mutex::new(None)),\n\t\t\tlast: Arc::new(Mutex::new(None)),\n\t\t\tpending: Arc::new(Mutex::new(())),\n\t\t\tprogress: Arc::new(RwLock::new(J::Progress::default())),\n\t\t\tsender,\n\t\t}\n\t}\n\n\t///\n\tpub fn is_pending(&self) -> bool {\n\t\tself.pending.try_lock().is_err()\n\t}\n\n\t/// makes sure `next` is cleared and returns `true` if it actually canceled something\n\tpub fn cancel(&self) -> bool {\n\t\tif let Ok(mut next) = self.next.lock() {\n\t\t\tif next.is_some() {\n\t\t\t\t*next = None;\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\tfalse\n\t}\n\n\t/// take out last finished job\n\tpub fn take_last(&self) -> Option<J> {\n\t\tself.last.lock().map_or(None, |mut last| last.take())\n\t}\n\n\t/// spawns `task` if nothing is running currently,\n\t/// otherwise schedules as `next` overwriting if `next` was set before.\n\t/// return `true` if the new task gets started right away.\n\tpub fn spawn(&self, task: J) -> bool {\n\t\tself.schedule_next(task);\n\t\tself.check_for_job()\n\t}\n\n\t///\n\tpub fn progress(&self) -> Option<J::Progress> {\n\t\tself.progress.read().ok().map(|d| (*d).clone())\n\t}\n\n\tfn check_for_job(&self) -> bool {\n\t\tif self.is_pending() {\n\t\t\treturn false;\n\t\t}\n\n\t\tif let Some(task) = self.take_next() {\n\t\t\tlet self_clone = (*self).clone();\n\t\t\trayon_core::spawn(move || {\n\t\t\t\tif let Err(e) = self_clone.run_job(task) {\n\t\t\t\t\tlog::error!(\"async job error: {e}\");\n\t\t\t\t}\n\t\t\t});\n\n\t\t\treturn true;\n\t\t}\n\n\t\tfalse\n\t}\n\n\tfn run_job(&self, mut task: J) -> Result<()> {\n\t\t//limit the pending scope\n\t\t{\n\t\t\tlet _pending = self.pending.lock()?;\n\n\t\t\tlet notification = task.run(RunParams {\n\t\t\t\tprogress: self.progress.clone(),\n\t\t\t\tsender: self.sender.clone(),\n\t\t\t})?;\n\n\t\t\tif let Ok(mut last) = self.last.lock() {\n\t\t\t\t*last = Some(task);\n\t\t\t}\n\n\t\t\tself.sender.send(notification)?;\n\t\t}\n\n\t\tself.check_for_job();\n\n\t\tOk(())\n\t}\n\n\tfn schedule_next(&self, task: J) {\n\t\tif let Ok(mut next) = self.next.lock() {\n\t\t\t*next = Some(task);\n\t\t}\n\t}\n\n\tfn take_next(&self) -> Option<J> {\n\t\tself.next.lock().map_or(None, |mut next| next.take())\n\t}\n}\n\n#[cfg(test)]\nmod test {\n\tuse super::*;\n\tuse crossbeam_channel::unbounded;\n\tuse pretty_assertions::assert_eq;\n\tuse std::{\n\t\tsync::atomic::{AtomicBool, AtomicU32, Ordering},\n\t\tthread,\n\t\ttime::Duration,\n\t};\n\n\t#[derive(Clone)]\n\tstruct TestJob {\n\t\tv: Arc<AtomicU32>,\n\t\tfinish: Arc<AtomicBool>,\n\t\tvalue_to_add: u32,\n\t}\n\n\ttype TestNotification = ();\n\n\timpl AsyncJob for TestJob {\n\t\ttype Notification = TestNotification;\n\t\ttype Progress = ();\n\n\t\tfn run(\n\t\t\t&mut self,\n\t\t\t_params: RunParams<Self::Notification, Self::Progress>,\n\t\t) -> Result<Self::Notification> {\n\t\t\tprintln!(\"[job] wait\");\n\n\t\t\twhile !self.finish.load(Ordering::SeqCst) {\n\t\t\t\tstd::thread::yield_now();\n\t\t\t}\n\n\t\t\tprintln!(\"[job] sleep\");\n\n\t\t\tthread::sleep(Duration::from_millis(100));\n\n\t\t\tprintln!(\"[job] done sleeping\");\n\n\t\t\tlet res =\n\t\t\t\tself.v.fetch_add(self.value_to_add, Ordering::SeqCst);\n\n\t\t\tprintln!(\"[job] value: {res}\");\n\n\t\t\tOk(())\n\t\t}\n\t}\n\n\t#[test]\n\tfn test_overwrite() {\n\t\tlet (sender, receiver) = unbounded();\n\n\t\tlet job: AsyncSingleJob<TestJob> =\n\t\t\tAsyncSingleJob::new(sender);\n\n\t\tlet task = TestJob {\n\t\t\tv: Arc::new(AtomicU32::new(1)),\n\t\t\tfinish: Arc::new(AtomicBool::new(false)),\n\t\t\tvalue_to_add: 1,\n\t\t};\n\n\t\tassert!(job.spawn(task.clone()));\n\t\ttask.finish.store(true, Ordering::SeqCst);\n\t\tthread::sleep(Duration::from_millis(10));\n\n\t\tfor _ in 0..5 {\n\t\t\tprintln!(\"spawn\");\n\t\t\tassert!(!job.spawn(task.clone()));\n\t\t}\n\n\t\tprintln!(\"recv\");\n\t\treceiver.recv().unwrap();\n\t\treceiver.recv().unwrap();\n\t\tassert!(receiver.is_empty());\n\n\t\tassert_eq!(\n\t\t\ttask.v.load(std::sync::atomic::Ordering::SeqCst),\n\t\t\t3\n\t\t);\n\t}\n\n\tfn wait_for_job(job: &AsyncSingleJob<TestJob>) {\n\t\twhile job.is_pending() {\n\t\t\tthread::sleep(Duration::from_millis(10));\n\t\t}\n\t}\n\n\t#[test]\n\tfn test_cancel() {\n\t\tlet (sender, receiver) = unbounded();\n\n\t\tlet job: AsyncSingleJob<TestJob> =\n\t\t\tAsyncSingleJob::new(sender);\n\n\t\tlet task = TestJob {\n\t\t\tv: Arc::new(AtomicU32::new(1)),\n\t\t\tfinish: Arc::new(AtomicBool::new(false)),\n\t\t\tvalue_to_add: 1,\n\t\t};\n\n\t\tassert!(job.spawn(task.clone()));\n\t\ttask.finish.store(true, Ordering::SeqCst);\n\t\tthread::sleep(Duration::from_millis(10));\n\n\t\tfor _ in 0..5 {\n\t\t\tprintln!(\"spawn\");\n\t\t\tassert!(!job.spawn(task.clone()));\n\t\t}\n\n\t\tprintln!(\"cancel\");\n\t\tassert!(job.cancel());\n\n\t\ttask.finish.store(true, Ordering::SeqCst);\n\n\t\twait_for_job(&job);\n\n\t\tprintln!(\"recv\");\n\t\treceiver.recv().unwrap();\n\t\tprintln!(\"received\");\n\n\t\tassert_eq!(\n\t\t\ttask.v.load(std::sync::atomic::Ordering::SeqCst),\n\t\t\t2\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/blame.rs",
    "content": "use crate::{\n\terror::Result,\n\thash,\n\tsync::{self, CommitId, FileBlame, RepoPath},\n\tAsyncGitNotification,\n};\nuse crossbeam_channel::Sender;\nuse std::{\n\thash::Hash,\n\tsync::{\n\t\tatomic::{AtomicUsize, Ordering},\n\t\tArc, Mutex,\n\t},\n};\n\n///\n#[derive(Hash, Clone, PartialEq, Eq)]\npub struct BlameParams {\n\t/// path to the file to blame\n\tpub file_path: String,\n\t/// blame at a specific revision\n\tpub commit_id: Option<CommitId>,\n}\n\nstruct Request<R, A>(R, Option<A>);\n\n#[derive(Default, Clone)]\nstruct LastResult<P, R> {\n\tparams: P,\n\tresult: R,\n}\n\n///\npub struct AsyncBlame {\n\tcurrent: Arc<Mutex<Request<u64, FileBlame>>>,\n\tlast: Arc<Mutex<Option<LastResult<BlameParams, FileBlame>>>>,\n\tsender: Sender<AsyncGitNotification>,\n\tpending: Arc<AtomicUsize>,\n\trepo: RepoPath,\n}\n\nimpl AsyncBlame {\n\t///\n\tpub fn new(\n\t\trepo: RepoPath,\n\t\tsender: &Sender<AsyncGitNotification>,\n\t) -> Self {\n\t\tSelf {\n\t\t\trepo,\n\t\t\tcurrent: Arc::new(Mutex::new(Request(0, None))),\n\t\t\tlast: Arc::new(Mutex::new(None)),\n\t\t\tsender: sender.clone(),\n\t\t\tpending: Arc::new(AtomicUsize::new(0)),\n\t\t}\n\t}\n\n\t///\n\tpub fn last(&self) -> Result<Option<(BlameParams, FileBlame)>> {\n\t\tlet last = self.last.lock()?;\n\n\t\tOk(last.clone().map(|last_result| {\n\t\t\t(last_result.params, last_result.result)\n\t\t}))\n\t}\n\n\t///\n\tpub fn refresh(&self) -> Result<()> {\n\t\tif let Ok(Some(param)) = self.get_last_param() {\n\t\t\tself.clear_current()?;\n\t\t\tself.request(param)?;\n\t\t}\n\t\tOk(())\n\t}\n\n\t///\n\tpub fn is_pending(&self) -> bool {\n\t\tself.pending.load(Ordering::Relaxed) > 0\n\t}\n\n\t///\n\tpub fn request(\n\t\t&self,\n\t\tparams: BlameParams,\n\t) -> Result<Option<FileBlame>> {\n\t\tlog::trace!(\"request\");\n\n\t\tlet hash = hash(&params);\n\n\t\t{\n\t\t\tlet mut current = self.current.lock()?;\n\n\t\t\tif current.0 == hash {\n\t\t\t\treturn Ok(current.1.clone());\n\t\t\t}\n\n\t\t\tcurrent.0 = hash;\n\t\t\tcurrent.1 = None;\n\t\t}\n\n\t\tlet arc_current = Arc::clone(&self.current);\n\t\tlet arc_last = Arc::clone(&self.last);\n\t\tlet sender = self.sender.clone();\n\t\tlet arc_pending = Arc::clone(&self.pending);\n\t\tlet repo = self.repo.clone();\n\n\t\tself.pending.fetch_add(1, Ordering::Relaxed);\n\n\t\trayon_core::spawn(move || {\n\t\t\tlet notify = Self::get_blame_helper(\n\t\t\t\t&repo,\n\t\t\t\tparams,\n\t\t\t\t&arc_last,\n\t\t\t\t&arc_current,\n\t\t\t\thash,\n\t\t\t);\n\n\t\t\tlet notify = match notify {\n\t\t\t\tErr(err) => {\n\t\t\t\t\tlog::error!(\"get_blame_helper error: {err}\");\n\t\t\t\t\ttrue\n\t\t\t\t}\n\t\t\t\tOk(notify) => notify,\n\t\t\t};\n\n\t\t\tarc_pending.fetch_sub(1, Ordering::Relaxed);\n\n\t\t\tsender\n\t\t\t\t.send(if notify {\n\t\t\t\t\tAsyncGitNotification::Blame\n\t\t\t\t} else {\n\t\t\t\t\tAsyncGitNotification::FinishUnchanged\n\t\t\t\t})\n\t\t\t\t.expect(\"error sending blame\");\n\t\t});\n\n\t\tOk(None)\n\t}\n\n\tfn get_blame_helper(\n\t\trepo_path: &RepoPath,\n\t\tparams: BlameParams,\n\t\tarc_last: &Arc<\n\t\t\tMutex<Option<LastResult<BlameParams, FileBlame>>>,\n\t\t>,\n\t\tarc_current: &Arc<Mutex<Request<u64, FileBlame>>>,\n\t\thash: u64,\n\t) -> Result<bool> {\n\t\tlet file_blame = sync::blame::blame_file(\n\t\t\trepo_path,\n\t\t\t&params.file_path,\n\t\t\tparams.commit_id,\n\t\t)?;\n\n\t\tlet mut notify = false;\n\t\t{\n\t\t\tlet mut current = arc_current.lock()?;\n\t\t\tif current.0 == hash {\n\t\t\t\tcurrent.1 = Some(file_blame.clone());\n\t\t\t\tnotify = true;\n\t\t\t}\n\t\t}\n\n\t\t{\n\t\t\tlet mut last = arc_last.lock()?;\n\t\t\t*last = Some(LastResult {\n\t\t\t\tresult: file_blame,\n\t\t\t\tparams,\n\t\t\t});\n\t\t}\n\n\t\tOk(notify)\n\t}\n\n\tfn get_last_param(&self) -> Result<Option<BlameParams>> {\n\t\tOk(self\n\t\t\t.last\n\t\t\t.lock()?\n\t\t\t.clone()\n\t\t\t.map(|last_result| last_result.params))\n\t}\n\n\tfn clear_current(&self) -> Result<()> {\n\t\tlet mut current = self.current.lock()?;\n\t\tcurrent.0 = 0;\n\t\tcurrent.1 = None;\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/branches.rs",
    "content": "use crate::{\n\tasyncjob::{AsyncJob, RunParams},\n\terror::Result,\n\tsync::{branch::get_branches_info, BranchInfo, RepoPath},\n\tAsyncGitNotification,\n};\nuse std::sync::{Arc, Mutex};\n\nenum JobState {\n\tRequest {\n\t\tlocal_branches: bool,\n\t\trepo: RepoPath,\n\t},\n\tResponse(Result<Vec<BranchInfo>>),\n}\n\n///\n#[derive(Clone, Default)]\npub struct AsyncBranchesJob {\n\tstate: Arc<Mutex<Option<JobState>>>,\n}\n\n///\nimpl AsyncBranchesJob {\n\t///\n\tpub fn new(repo: RepoPath, local_branches: bool) -> Self {\n\t\tSelf {\n\t\t\tstate: Arc::new(Mutex::new(Some(JobState::Request {\n\t\t\t\trepo,\n\t\t\t\tlocal_branches,\n\t\t\t}))),\n\t\t}\n\t}\n\n\t///\n\tpub fn result(&self) -> Option<Result<Vec<BranchInfo>>> {\n\t\tif let Ok(mut state) = self.state.lock() {\n\t\t\tif let Some(state) = state.take() {\n\t\t\t\treturn match state {\n\t\t\t\t\tJobState::Request { .. } => None,\n\t\t\t\t\tJobState::Response(result) => Some(result),\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tNone\n\t}\n}\n\nimpl AsyncJob for AsyncBranchesJob {\n\ttype Notification = AsyncGitNotification;\n\ttype Progress = ();\n\n\tfn run(\n\t\t&mut self,\n\t\t_params: RunParams<Self::Notification, Self::Progress>,\n\t) -> Result<Self::Notification> {\n\t\tif let Ok(mut state) = self.state.lock() {\n\t\t\t*state = state.take().map(|state| match state {\n\t\t\t\tJobState::Request {\n\t\t\t\t\tlocal_branches,\n\t\t\t\t\trepo,\n\t\t\t\t} => {\n\t\t\t\t\tlet branches =\n\t\t\t\t\t\tget_branches_info(&repo, local_branches);\n\n\t\t\t\t\tJobState::Response(branches)\n\t\t\t\t}\n\t\t\t\tJobState::Response(result) => {\n\t\t\t\t\tJobState::Response(result)\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\tOk(AsyncGitNotification::Branches)\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/cached/branchname.rs",
    "content": "use crate::{\n\terror::Result,\n\tsync::{self, branch::get_branch_name, RepoPathRef},\n};\nuse sync::Head;\n\n///\npub struct BranchName {\n\tlast_result: Option<(Head, String)>,\n\trepo: RepoPathRef,\n}\n\nimpl BranchName {\n\t///\n\tpub const fn new(repo: RepoPathRef) -> Self {\n\t\tSelf {\n\t\t\trepo,\n\t\t\tlast_result: None,\n\t\t}\n\t}\n\n\t///\n\tpub fn lookup(&mut self) -> Result<String> {\n\t\tlet current_head = sync::get_head_tuple(&self.repo.borrow())?;\n\n\t\tif let Some((last_head, branch_name)) =\n\t\t\tself.last_result.as_ref()\n\t\t{\n\t\t\tif *last_head == current_head {\n\t\t\t\treturn Ok(branch_name.clone());\n\t\t\t}\n\t\t}\n\n\t\tself.fetch(current_head)\n\t}\n\n\t///\n\tpub fn last(&self) -> Option<String> {\n\t\tself.last_result.as_ref().map(|last| last.1.clone())\n\t}\n\n\tfn fetch(&mut self, head: Head) -> Result<String> {\n\t\tlet name = get_branch_name(&self.repo.borrow())?;\n\t\tself.last_result = Some((head, name.clone()));\n\t\tOk(name)\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/cached/mod.rs",
    "content": "//! cached lookups:\n//! parts of the sync api that might take longer\n//! to compute but change seldom so doing them async might be overkill\n\nmod branchname;\n\npub use branchname::BranchName;\n"
  },
  {
    "path": "asyncgit/src/commit_files.rs",
    "content": "use crate::{\n\terror::Result,\n\tsync::{self, commit_files::OldNew, CommitId, RepoPath},\n\tAsyncGitNotification, StatusItem,\n};\nuse crossbeam_channel::Sender;\nuse std::sync::{\n\tatomic::{AtomicUsize, Ordering},\n\tArc, Mutex,\n};\n\ntype ResultType = Vec<StatusItem>;\nstruct Request<R, A>(R, A);\n\n///\n#[derive(Debug, Copy, Clone, PartialEq, Eq)]\npub struct CommitFilesParams {\n\t///\n\tpub id: CommitId,\n\t///\n\tpub other: Option<CommitId>,\n}\n\nimpl From<CommitId> for CommitFilesParams {\n\tfn from(id: CommitId) -> Self {\n\t\tSelf { id, other: None }\n\t}\n}\n\nimpl From<(CommitId, CommitId)> for CommitFilesParams {\n\tfn from((id, other): (CommitId, CommitId)) -> Self {\n\t\tSelf {\n\t\t\tid,\n\t\t\tother: Some(other),\n\t\t}\n\t}\n}\n\nimpl From<OldNew<CommitId>> for CommitFilesParams {\n\tfn from(old_new: OldNew<CommitId>) -> Self {\n\t\tSelf {\n\t\t\tid: old_new.new,\n\t\t\tother: Some(old_new.old),\n\t\t}\n\t}\n}\n\n///\npub struct AsyncCommitFiles {\n\tcurrent:\n\t\tArc<Mutex<Option<Request<CommitFilesParams, ResultType>>>>,\n\tsender: Sender<AsyncGitNotification>,\n\tpending: Arc<AtomicUsize>,\n\trepo: RepoPath,\n}\n\nimpl AsyncCommitFiles {\n\t///\n\tpub fn new(\n\t\trepo: RepoPath,\n\t\tsender: &Sender<AsyncGitNotification>,\n\t) -> Self {\n\t\tSelf {\n\t\t\trepo,\n\t\t\tcurrent: Arc::new(Mutex::new(None)),\n\t\t\tsender: sender.clone(),\n\t\t\tpending: Arc::new(AtomicUsize::new(0)),\n\t\t}\n\t}\n\n\t///\n\tpub fn current(\n\t\t&self,\n\t) -> Result<Option<(CommitFilesParams, ResultType)>> {\n\t\tlet c = self.current.lock()?;\n\n\t\tc.as_ref()\n\t\t\t.map_or(Ok(None), |c| Ok(Some((c.0, c.1.clone()))))\n\t}\n\n\t///\n\tpub fn is_pending(&self) -> bool {\n\t\tself.pending.load(Ordering::Relaxed) > 0\n\t}\n\n\t///\n\tpub fn fetch(&self, params: CommitFilesParams) -> Result<()> {\n\t\tif self.is_pending() {\n\t\t\treturn Ok(());\n\t\t}\n\n\t\tlog::trace!(\"request: {params:?}\");\n\n\t\t{\n\t\t\tlet current = self.current.lock()?;\n\t\t\tif let Some(c) = &*current {\n\t\t\t\tif c.0 == params {\n\t\t\t\t\treturn Ok(());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tlet arc_current = Arc::clone(&self.current);\n\t\tlet sender = self.sender.clone();\n\t\tlet arc_pending = Arc::clone(&self.pending);\n\t\tlet repo = self.repo.clone();\n\n\t\tself.pending.fetch_add(1, Ordering::Relaxed);\n\n\t\trayon_core::spawn(move || {\n\t\t\tSelf::fetch_helper(&repo, params, &arc_current)\n\t\t\t\t.expect(\"failed to fetch\");\n\n\t\t\tarc_pending.fetch_sub(1, Ordering::Relaxed);\n\n\t\t\tsender\n\t\t\t\t.send(AsyncGitNotification::CommitFiles)\n\t\t\t\t.expect(\"error sending\");\n\t\t});\n\n\t\tOk(())\n\t}\n\n\tfn fetch_helper(\n\t\trepo_path: &RepoPath,\n\t\tparams: CommitFilesParams,\n\t\tarc_current: &Arc<\n\t\t\tMutex<Option<Request<CommitFilesParams, ResultType>>>,\n\t\t>,\n\t) -> Result<()> {\n\t\tlet res = sync::get_commit_files(\n\t\t\trepo_path,\n\t\t\tparams.id,\n\t\t\tparams.other,\n\t\t)?;\n\n\t\tlog::trace!(\"get_commit_files: {:?} ({})\", params, res.len());\n\n\t\t{\n\t\t\tlet mut current = arc_current.lock()?;\n\t\t\t*current = Some(Request(params, res));\n\t\t}\n\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/diff.rs",
    "content": "use crate::{\n\terror::Result,\n\thash,\n\tsync::{\n\t\tself, commit_files::OldNew, diff::DiffOptions, CommitId,\n\t\tRepoPath,\n\t},\n\tAsyncGitNotification, FileDiff,\n};\nuse crossbeam_channel::Sender;\nuse std::{\n\thash::Hash,\n\tsync::{\n\t\tatomic::{AtomicUsize, Ordering},\n\t\tArc, Mutex,\n\t},\n};\n\n///\n#[derive(Debug, Hash, Clone, PartialEq, Eq)]\npub enum DiffType {\n\t/// diff two commits\n\tCommits(OldNew<CommitId>),\n\t/// diff in a given commit\n\tCommit(CommitId),\n\t/// diff against staged file\n\tStage,\n\t/// diff against file in workdir\n\tWorkDir,\n}\n\n///\n#[derive(Debug, Hash, Clone, PartialEq, Eq)]\npub struct DiffParams {\n\t/// path to the file to diff\n\tpub path: String,\n\t/// what kind of diff\n\tpub diff_type: DiffType,\n\t/// diff options\n\tpub options: DiffOptions,\n}\n\nstruct Request<R, A>(R, Option<A>);\n\n#[derive(Default, Clone)]\nstruct LastResult<P, R> {\n\tparams: P,\n\tresult: R,\n}\n\n///\npub struct AsyncDiff {\n\tcurrent: Arc<Mutex<Request<u64, FileDiff>>>,\n\tlast: Arc<Mutex<Option<LastResult<DiffParams, FileDiff>>>>,\n\tsender: Sender<AsyncGitNotification>,\n\tpending: Arc<AtomicUsize>,\n\trepo: RepoPath,\n}\n\nimpl AsyncDiff {\n\t///\n\tpub fn new(\n\t\trepo: RepoPath,\n\t\tsender: &Sender<AsyncGitNotification>,\n\t) -> Self {\n\t\tSelf {\n\t\t\trepo,\n\t\t\tcurrent: Arc::new(Mutex::new(Request(0, None))),\n\t\t\tlast: Arc::new(Mutex::new(None)),\n\t\t\tsender: sender.clone(),\n\t\t\tpending: Arc::new(AtomicUsize::new(0)),\n\t\t}\n\t}\n\n\t///\n\tpub fn last(&self) -> Result<Option<(DiffParams, FileDiff)>> {\n\t\tlet last = self.last.lock()?;\n\n\t\tOk(last.clone().map(|res| (res.params, res.result)))\n\t}\n\n\t///\n\tpub fn refresh(&self) -> Result<()> {\n\t\tif let Ok(Some(param)) = self.get_last_param() {\n\t\t\tself.clear_current()?;\n\t\t\tself.request(param)?;\n\t\t}\n\t\tOk(())\n\t}\n\n\t///\n\tpub fn is_pending(&self) -> bool {\n\t\tself.pending.load(Ordering::Relaxed) > 0\n\t}\n\n\t///\n\tpub fn request(\n\t\t&self,\n\t\tparams: DiffParams,\n\t) -> Result<Option<FileDiff>> {\n\t\tlog::trace!(\"request {params:?}\");\n\n\t\tlet hash = hash(&params);\n\n\t\t{\n\t\t\tlet mut current = self.current.lock()?;\n\n\t\t\tif current.0 == hash {\n\t\t\t\treturn Ok(current.1.clone());\n\t\t\t}\n\n\t\t\tcurrent.0 = hash;\n\t\t\tcurrent.1 = None;\n\t\t}\n\n\t\tlet arc_current = Arc::clone(&self.current);\n\t\tlet arc_last = Arc::clone(&self.last);\n\t\tlet sender = self.sender.clone();\n\t\tlet arc_pending = Arc::clone(&self.pending);\n\t\tlet repo = self.repo.clone();\n\n\t\tself.pending.fetch_add(1, Ordering::Relaxed);\n\n\t\trayon_core::spawn(move || {\n\t\t\tlet notify = Self::get_diff_helper(\n\t\t\t\t&repo,\n\t\t\t\tparams,\n\t\t\t\t&arc_last,\n\t\t\t\t&arc_current,\n\t\t\t\thash,\n\t\t\t);\n\n\t\t\tlet notify = match notify {\n\t\t\t\tErr(e) => {\n\t\t\t\t\tlog::error!(\"get_diff_helper error: {e}\");\n\t\t\t\t\ttrue\n\t\t\t\t}\n\t\t\t\tOk(notify) => notify,\n\t\t\t};\n\n\t\t\tarc_pending.fetch_sub(1, Ordering::Relaxed);\n\n\t\t\tsender\n\t\t\t\t.send(if notify {\n\t\t\t\t\tAsyncGitNotification::Diff\n\t\t\t\t} else {\n\t\t\t\t\tAsyncGitNotification::FinishUnchanged\n\t\t\t\t})\n\t\t\t\t.expect(\"error sending diff\");\n\t\t});\n\n\t\tOk(None)\n\t}\n\n\tfn get_diff_helper(\n\t\trepo_path: &RepoPath,\n\t\tparams: DiffParams,\n\t\tarc_last: &Arc<\n\t\t\tMutex<Option<LastResult<DiffParams, FileDiff>>>,\n\t\t>,\n\t\tarc_current: &Arc<Mutex<Request<u64, FileDiff>>>,\n\t\thash: u64,\n\t) -> Result<bool> {\n\t\tlet res = match params.diff_type {\n\t\t\tDiffType::Stage => sync::diff::get_diff(\n\t\t\t\trepo_path,\n\t\t\t\t&params.path,\n\t\t\t\ttrue,\n\t\t\t\tSome(params.options),\n\t\t\t)?,\n\t\t\tDiffType::WorkDir => sync::diff::get_diff(\n\t\t\t\trepo_path,\n\t\t\t\t&params.path,\n\t\t\t\tfalse,\n\t\t\t\tSome(params.options),\n\t\t\t)?,\n\t\t\tDiffType::Commit(id) => sync::diff::get_diff_commit(\n\t\t\t\trepo_path,\n\t\t\t\tid,\n\t\t\t\tparams.path.clone(),\n\t\t\t\tSome(params.options),\n\t\t\t)?,\n\t\t\tDiffType::Commits(ids) => sync::diff::get_diff_commits(\n\t\t\t\trepo_path,\n\t\t\t\tids,\n\t\t\t\tparams.path.clone(),\n\t\t\t\tSome(params.options),\n\t\t\t)?,\n\t\t};\n\n\t\tlet mut notify = false;\n\t\t{\n\t\t\tlet mut current = arc_current.lock()?;\n\t\t\tif current.0 == hash {\n\t\t\t\tcurrent.1 = Some(res.clone());\n\t\t\t\tnotify = true;\n\t\t\t}\n\t\t}\n\n\t\t{\n\t\t\tlet mut last = arc_last.lock()?;\n\t\t\t*last = Some(LastResult {\n\t\t\t\tresult: res,\n\t\t\t\tparams,\n\t\t\t});\n\t\t}\n\n\t\tOk(notify)\n\t}\n\n\tfn get_last_param(&self) -> Result<Option<DiffParams>> {\n\t\tOk(self.last.lock()?.clone().map(|e| e.params))\n\t}\n\n\tfn clear_current(&self) -> Result<()> {\n\t\tlet mut current = self.current.lock()?;\n\t\tcurrent.0 = 0;\n\t\tcurrent.1 = None;\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/error.rs",
    "content": "use std::{\n\tnum::TryFromIntError, path::StripPrefixError,\n\tstring::FromUtf8Error,\n};\nuse thiserror::Error;\n\n///\n#[derive(Error, Debug)]\npub enum GixError {\n\t///\n\t#[error(\"gix::discover error: {0}\")]\n\tDiscover(#[from] Box<gix::discover::Error>),\n\n\t///\n\t#[error(\"gix::head::peel::to_commit error: {0}\")]\n\tHeadPeelToCommit(#[from] gix::head::peel::to_commit::Error),\n\n\t///\n\t#[error(\"gix::object::find::existing::with_conversion::Error error: {0}\")]\n\tObjectFindExistingWithConversion(\n\t\t#[from] gix::object::find::existing::with_conversion::Error,\n\t),\n\n\t///\n\t#[error(\"gix::objs::decode::Error error: {0}\")]\n\tObjsDecode(#[from] gix::objs::decode::Error),\n\n\t///\n\t#[error(\"gix::pathspec::init::Error error: {0}\")]\n\tPathspecInit(#[from] Box<gix::pathspec::init::Error>),\n\n\t///\n\t#[error(\"gix::reference::find::existing error: {0}\")]\n\tReferenceFindExisting(\n\t\t#[from] gix::reference::find::existing::Error,\n\t),\n\n\t///\n\t#[error(\"gix::reference::head_tree_id::Error error: {0}\")]\n\tReferenceHeadTreeId(#[from] gix::reference::head_tree_id::Error),\n\n\t///\n\t#[error(\"gix::reference::iter::Error error: {0}\")]\n\tReferenceIter(#[from] gix::reference::iter::Error),\n\n\t///\n\t#[error(\"gix::reference::iter::init::Error error: {0}\")]\n\tReferenceIterInit(#[from] gix::reference::iter::init::Error),\n\n\t///\n\t#[error(\"gix::revision::walk error: {0}\")]\n\tRevisionWalk(#[from] gix::revision::walk::Error),\n\n\t///\n\t#[error(\"gix::status::Error error: {0}\")]\n\tStatus(#[from] Box<gix::status::Error>),\n\n\t///\n\t#[error(\"gix::status::index_worktree::Error error: {0}\")]\n\tStatusIndexWorktree(\n\t\t#[from] Box<gix::status::index_worktree::Error>,\n\t),\n\n\t///\n\t#[error(\"gix::status::into_iter::Error error: {0}\")]\n\tStatusIntoIter(#[from] Box<gix::status::into_iter::Error>),\n\n\t///\n\t#[error(\"gix::status::iter::Error error: {0}\")]\n\tStatusIter(#[from] Box<gix::status::iter::Error>),\n\n\t///\n\t#[error(\"gix::status::tree_index::Error error: {0}\")]\n\tStatusTreeIndex(#[from] Box<gix::status::tree_index::Error>),\n\n\t///\n\t#[error(\"gix::worktree::open_index::Error error: {0}\")]\n\tWorktreeOpenIndex(#[from] Box<gix::worktree::open_index::Error>),\n}\n\n///\n#[derive(Error, Debug)]\npub enum Error {\n\t///\n\t#[error(\"`{0}`\")]\n\tGeneric(String),\n\n\t///\n\t#[error(\"git: no head found\")]\n\tNoHead,\n\n\t///\n\t#[error(\"git: conflict during rebase\")]\n\tRebaseConflict,\n\n\t///\n\t#[error(\"git: remote url not found\")]\n\tUnknownRemote,\n\n\t///\n\t#[error(\"git: inconclusive remotes\")]\n\tNoDefaultRemoteFound,\n\n\t///\n\t#[error(\"git: work dir error\")]\n\tNoWorkDir,\n\n\t///\n\t#[error(\"git: uncommitted changes\")]\n\tUncommittedChanges,\n\n\t///\n\t#[error(\"git: can\\u{2019}t run blame on a binary file\")]\n\tNoBlameOnBinaryFile,\n\n\t///\n\t#[error(\"binary file\")]\n\tBinaryFile,\n\n\t///\n\t#[error(\"io error:{0}\")]\n\tIo(#[from] std::io::Error),\n\n\t///\n\t#[error(\"git error:{0}\")]\n\tGit(#[from] git2::Error),\n\n\t///\n\t#[error(\"git config error: {0}\")]\n\tGitConfig(String),\n\n\t///\n\t#[error(\"strip prefix error: {0}\")]\n\tStripPrefix(#[from] StripPrefixError),\n\n\t///\n\t#[error(\"utf8 error:{0}\")]\n\tUtf8Conversion(#[from] FromUtf8Error),\n\n\t///\n\t#[error(\"TryFromInt error:{0}\")]\n\tIntConversion(#[from] TryFromIntError),\n\n\t///\n\t#[error(\"EasyCast error:{0}\")]\n\tEasyCast(#[from] easy_cast::Error),\n\n\t///\n\t#[error(\"no parent of commit found\")]\n\tNoParent,\n\n\t///\n\t#[error(\"not on a branch\")]\n\tNoBranch,\n\n\t///\n\t#[error(\"rayon error: {0}\")]\n\tThreadPool(#[from] rayon_core::ThreadPoolBuildError),\n\n\t///\n\t#[error(\"git hook error: {0}\")]\n\tHooks(#[from] git2_hooks::HooksError),\n\n\t///\n\t#[error(\"sign builder error: {0}\")]\n\tSignBuilder(#[from] crate::sync::sign::SignBuilderError),\n\n\t///\n\t#[error(\"sign error: {0}\")]\n\tSign(#[from] crate::sync::sign::SignError),\n\n\t///\n\t#[error(\"gix error:{0}\")]\n\tGix(#[from] GixError),\n\n\t///\n\t#[error(\"amend error: config commit.gpgsign=true detected.\\ngpg signing is not supported for amending non-last commits\")]\n\tSignAmendNonLastCommit,\n\n\t///\n\t#[error(\"reword error: config commit.gpgsign=true detected.\\ngpg signing is not supported for rewording non-last commits\")]\n\tSignRewordNonLastCommit,\n\n\t///\n\t#[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\")]\n\tSignRewordLastCommitStaged,\n}\n\n///\npub type Result<T> = std::result::Result<T, Error>;\n\nimpl<T> From<std::sync::PoisonError<T>> for Error {\n\tfn from(error: std::sync::PoisonError<T>) -> Self {\n\t\tSelf::Generic(format!(\"poison error: {error}\"))\n\t}\n}\n\nimpl<T> From<crossbeam_channel::SendError<T>> for Error {\n\tfn from(error: crossbeam_channel::SendError<T>) -> Self {\n\t\tSelf::Generic(format!(\"send error: {error}\"))\n\t}\n}\n\nimpl From<gix::discover::Error> for GixError {\n\tfn from(error: gix::discover::Error) -> Self {\n\t\tSelf::Discover(Box::new(error))\n\t}\n}\n\nimpl From<gix::discover::Error> for Error {\n\tfn from(error: gix::discover::Error) -> Self {\n\t\tSelf::Gix(GixError::from(error))\n\t}\n}\n\nimpl From<gix::head::peel::to_commit::Error> for Error {\n\tfn from(error: gix::head::peel::to_commit::Error) -> Self {\n\t\tSelf::Gix(GixError::from(error))\n\t}\n}\n\nimpl From<gix::object::find::existing::with_conversion::Error>\n\tfor Error\n{\n\tfn from(\n\t\terror: gix::object::find::existing::with_conversion::Error,\n\t) -> Self {\n\t\tSelf::Gix(GixError::from(error))\n\t}\n}\n\nimpl From<gix::objs::decode::Error> for Error {\n\tfn from(error: gix::objs::decode::Error) -> Self {\n\t\tSelf::Gix(GixError::from(error))\n\t}\n}\n\nimpl From<gix::pathspec::init::Error> for GixError {\n\tfn from(error: gix::pathspec::init::Error) -> Self {\n\t\tSelf::PathspecInit(Box::new(error))\n\t}\n}\n\nimpl From<gix::pathspec::init::Error> for Error {\n\tfn from(error: gix::pathspec::init::Error) -> Self {\n\t\tSelf::Gix(GixError::from(error))\n\t}\n}\n\nimpl From<gix::reference::find::existing::Error> for Error {\n\tfn from(error: gix::reference::find::existing::Error) -> Self {\n\t\tSelf::Gix(GixError::from(error))\n\t}\n}\n\nimpl From<gix::reference::head_tree_id::Error> for Error {\n\tfn from(error: gix::reference::head_tree_id::Error) -> Self {\n\t\tSelf::Gix(GixError::from(error))\n\t}\n}\n\nimpl From<gix::reference::iter::Error> for Error {\n\tfn from(error: gix::reference::iter::Error) -> Self {\n\t\tSelf::Gix(GixError::from(error))\n\t}\n}\n\nimpl From<gix::reference::iter::init::Error> for Error {\n\tfn from(error: gix::reference::iter::init::Error) -> Self {\n\t\tSelf::Gix(GixError::from(error))\n\t}\n}\n\nimpl From<gix::revision::walk::Error> for Error {\n\tfn from(error: gix::revision::walk::Error) -> Self {\n\t\tSelf::Gix(GixError::from(error))\n\t}\n}\n\nimpl From<gix::status::Error> for GixError {\n\tfn from(error: gix::status::Error) -> Self {\n\t\tSelf::Status(Box::new(error))\n\t}\n}\n\nimpl From<gix::status::Error> for Error {\n\tfn from(error: gix::status::Error) -> Self {\n\t\tSelf::Gix(GixError::from(error))\n\t}\n}\n\nimpl From<gix::status::iter::Error> for GixError {\n\tfn from(error: gix::status::iter::Error) -> Self {\n\t\tSelf::StatusIter(Box::new(error))\n\t}\n}\n\nimpl From<gix::status::iter::Error> for Error {\n\tfn from(error: gix::status::iter::Error) -> Self {\n\t\tSelf::Gix(GixError::from(error))\n\t}\n}\n\nimpl From<gix::status::into_iter::Error> for GixError {\n\tfn from(error: gix::status::into_iter::Error) -> Self {\n\t\tSelf::StatusIntoIter(Box::new(error))\n\t}\n}\n\nimpl From<gix::status::into_iter::Error> for Error {\n\tfn from(error: gix::status::into_iter::Error) -> Self {\n\t\tSelf::Gix(GixError::from(error))\n\t}\n}\n\nimpl From<gix::status::index_worktree::Error> for GixError {\n\tfn from(error: gix::status::index_worktree::Error) -> Self {\n\t\tSelf::StatusIndexWorktree(Box::new(error))\n\t}\n}\n\nimpl From<gix::status::index_worktree::Error> for Error {\n\tfn from(error: gix::status::index_worktree::Error) -> Self {\n\t\tSelf::Gix(GixError::from(error))\n\t}\n}\n\nimpl From<gix::status::tree_index::Error> for GixError {\n\tfn from(error: gix::status::tree_index::Error) -> Self {\n\t\tSelf::StatusTreeIndex(Box::new(error))\n\t}\n}\n\nimpl From<gix::status::tree_index::Error> for Error {\n\tfn from(error: gix::status::tree_index::Error) -> Self {\n\t\tSelf::Gix(GixError::from(error))\n\t}\n}\n\nimpl From<gix::worktree::open_index::Error> for GixError {\n\tfn from(error: gix::worktree::open_index::Error) -> Self {\n\t\tSelf::WorktreeOpenIndex(Box::new(error))\n\t}\n}\n\nimpl From<gix::worktree::open_index::Error> for Error {\n\tfn from(error: gix::worktree::open_index::Error) -> Self {\n\t\tSelf::Gix(GixError::from(error))\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/fetch_job.rs",
    "content": "//!\n\nuse crate::{\n\tasyncjob::{AsyncJob, RunParams},\n\terror::Result,\n\tsync::remotes::fetch_all,\n\tsync::{cred::BasicAuthCredential, RepoPath},\n\tAsyncGitNotification, ProgressPercent,\n};\n\nuse std::sync::{Arc, Mutex};\n\nenum JobState {\n\tRequest(Option<BasicAuthCredential>),\n\tResponse(Result<()>),\n}\n\n///\n#[derive(Clone)]\npub struct AsyncFetchJob {\n\tstate: Arc<Mutex<Option<JobState>>>,\n\trepo: RepoPath,\n}\n\n///\nimpl AsyncFetchJob {\n\t///\n\tpub fn new(\n\t\trepo: RepoPath,\n\t\tbasic_credential: Option<BasicAuthCredential>,\n\t) -> Self {\n\t\tSelf {\n\t\t\trepo,\n\t\t\tstate: Arc::new(Mutex::new(Some(JobState::Request(\n\t\t\t\tbasic_credential,\n\t\t\t)))),\n\t\t}\n\t}\n}\n\nimpl AsyncJob for AsyncFetchJob {\n\ttype Notification = AsyncGitNotification;\n\ttype Progress = ProgressPercent;\n\n\tfn run(\n\t\t&mut self,\n\t\t_params: RunParams<Self::Notification, Self::Progress>,\n\t) -> Result<Self::Notification> {\n\t\tif let Ok(mut state) = self.state.lock() {\n\t\t\t*state = state.take().map(|state| match state {\n\t\t\t\tJobState::Request(basic_credentials) => {\n\t\t\t\t\t//TODO: support progress\n\t\t\t\t\tlet result = fetch_all(\n\t\t\t\t\t\t&self.repo,\n\t\t\t\t\t\t&basic_credentials,\n\t\t\t\t\t\t&None,\n\t\t\t\t\t);\n\n\t\t\t\t\tJobState::Response(result)\n\t\t\t\t}\n\t\t\t\tJobState::Response(result) => {\n\t\t\t\t\tJobState::Response(result)\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\tOk(AsyncGitNotification::Fetch)\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/filter_commits.rs",
    "content": "use rayon::{\n\tprelude::ParallelIterator,\n\tslice::{ParallelSlice, ParallelSliceMut},\n};\n\nuse crate::{\n\tasyncjob::{AsyncJob, RunParams},\n\terror::Result,\n\tsync::{self, CommitId, RepoPath, SharedCommitFilterFn},\n\tAsyncGitNotification, ProgressPercent,\n};\nuse std::{\n\tsync::{\n\t\tatomic::{AtomicBool, AtomicUsize, Ordering},\n\t\tArc, Mutex,\n\t},\n\ttime::{Duration, Instant},\n};\n\n///\npub struct CommitFilterResult {\n\t///\n\tpub result: Vec<CommitId>,\n\t///\n\tpub duration: Duration,\n}\n\nenum JobState {\n\tRequest {\n\t\tcommits: Vec<CommitId>,\n\t\trepo_path: RepoPath,\n\t},\n\tResponse(Result<CommitFilterResult>),\n}\n\n///\n#[derive(Clone)]\npub struct AsyncCommitFilterJob {\n\tstate: Arc<Mutex<Option<JobState>>>,\n\tfilter: SharedCommitFilterFn,\n\tcancellation_flag: Arc<AtomicBool>,\n}\n\n///\nimpl AsyncCommitFilterJob {\n\t///\n\tpub fn new(\n\t\trepo_path: RepoPath,\n\t\tcommits: Vec<CommitId>,\n\t\tfilter: SharedCommitFilterFn,\n\t\tcancellation_flag: Arc<AtomicBool>,\n\t) -> Self {\n\t\tSelf {\n\t\t\tstate: Arc::new(Mutex::new(Some(JobState::Request {\n\t\t\t\trepo_path,\n\t\t\t\tcommits,\n\t\t\t}))),\n\t\t\tfilter,\n\t\t\tcancellation_flag,\n\t\t}\n\t}\n\n\t///\n\tpub fn result(&self) -> Option<Result<CommitFilterResult>> {\n\t\tif let Ok(mut state) = self.state.lock() {\n\t\t\tif let Some(state) = state.take() {\n\t\t\t\treturn match state {\n\t\t\t\t\tJobState::Request { .. } => None,\n\t\t\t\t\tJobState::Response(result) => Some(result),\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tNone\n\t}\n\n\tfn run_request(\n\t\t&self,\n\t\trepo_path: &RepoPath,\n\t\tcommits: Vec<CommitId>,\n\t\tparams: &RunParams<AsyncGitNotification, ProgressPercent>,\n\t) -> JobState {\n\t\tlet result = self\n\t\t\t.filter_commits(repo_path, commits, params)\n\t\t\t.map(|(start, result)| CommitFilterResult {\n\t\t\t\tresult,\n\t\t\t\tduration: start.elapsed(),\n\t\t\t});\n\n\t\tJobState::Response(result)\n\t}\n\n\tfn filter_commits(\n\t\t&self,\n\t\trepo_path: &RepoPath,\n\t\tcommits: Vec<CommitId>,\n\t\tparams: &RunParams<AsyncGitNotification, ProgressPercent>,\n\t) -> Result<(Instant, Vec<CommitId>)> {\n\t\tscopetime::scope_time!(\"filter_commits\");\n\n\t\tlet total_amount = commits.len();\n\t\tlet start = Instant::now();\n\n\t\t//note: for some reason >4 threads degrades search performance\n\t\tlet pool =\n\t\t\trayon::ThreadPoolBuilder::new().num_threads(4).build()?;\n\n\t\tlet idx = AtomicUsize::new(0);\n\n\t\tlet mut result = pool.install(|| {\n\t\t\tcommits\n\t\t\t\t.into_iter()\n\t\t\t\t.enumerate()\n\t\t\t\t.collect::<Vec<(usize, CommitId)>>()\n\t\t\t\t.par_chunks(1000)\n\t\t\t\t.filter_map(|c| {\n\t\t\t\t\t//TODO: error log repo open errors\n\t\t\t\t\tsync::repo(repo_path).ok().map(|repo| {\n\t\t\t\t\t\tc.iter()\n\t\t\t\t\t\t\t.filter_map(|(e, c)| {\n\t\t\t\t\t\t\t\tlet idx = idx.fetch_add(\n\t\t\t\t\t\t\t\t1,\n\t\t\t\t\t\t\t\tstd::sync::atomic::Ordering::Relaxed,\n\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t\t\tif self\n\t\t\t\t\t\t\t\t\t.cancellation_flag\n\t\t\t\t\t\t\t\t\t.load(Ordering::Relaxed)\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\treturn None;\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tSelf::update_progress(\n\t\t\t\t\t\t\t\t\tparams,\n\t\t\t\t\t\t\t\t\tProgressPercent::new(\n\t\t\t\t\t\t\t\t\t\tidx,\n\t\t\t\t\t\t\t\t\t\ttotal_amount,\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t\t\t(*self.filter)(&repo, c)\n\t\t\t\t\t\t\t\t\t.ok()\n\t\t\t\t\t\t\t\t\t.and_then(|res| {\n\t\t\t\t\t\t\t\t\t\tres.then_some((*e, *c))\n\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t.collect::<Vec<_>>()\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t\t.flatten()\n\t\t\t\t.collect::<Vec<_>>()\n\t\t});\n\n\t\tresult.par_sort_by(|a, b| a.0.cmp(&b.0));\n\n\t\tlet result = result.into_iter().map(|c| c.1).collect();\n\n\t\tOk((start, result))\n\t}\n\n\tfn update_progress(\n\t\tparams: &RunParams<AsyncGitNotification, ProgressPercent>,\n\t\tnew_progress: ProgressPercent,\n\t) {\n\t\tmatch params.set_progress(new_progress) {\n\t\t\tErr(e) => log::error!(\"progress error: {e}\"),\n\t\t\tOk(result) if result => {\n\t\t\t\tif let Err(e) =\n\t\t\t\t\tparams.send(AsyncGitNotification::CommitFilter)\n\t\t\t\t{\n\t\t\t\t\tlog::error!(\"send error: {e}\");\n\t\t\t\t}\n\t\t\t}\n\t\t\t_ => (),\n\t\t}\n\t}\n}\n\nimpl AsyncJob for AsyncCommitFilterJob {\n\ttype Notification = AsyncGitNotification;\n\ttype Progress = ProgressPercent;\n\n\tfn run(\n\t\t&mut self,\n\t\tparams: RunParams<Self::Notification, Self::Progress>,\n\t) -> Result<Self::Notification> {\n\t\tif let Ok(mut state) = self.state.lock() {\n\t\t\t*state = state.take().map(|state| match state {\n\t\t\t\tJobState::Request { commits, repo_path } => {\n\t\t\t\t\tself.run_request(&repo_path, commits, &params)\n\t\t\t\t}\n\t\t\t\tJobState::Response(result) => {\n\t\t\t\t\tJobState::Response(result)\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\tOk(AsyncGitNotification::CommitFilter)\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/lib.rs",
    "content": "/*!\n`AsyncGit` is a library that provides non-blocking access to Git\noperations, enabling `GitUI` to perform potentially slow Git operations\nin the background while keeping the user interface responsive.\n\nIt also provides synchronous Git operations.\n\nIt wraps libraries like git2 and gix.\n*/\n\n#![forbid(missing_docs)]\n#![deny(\n\tmismatched_lifetime_syntaxes,\n\tunused_imports,\n\tunused_must_use,\n\tdead_code,\n\tunstable_name_collisions,\n\tunused_assignments,\n\tdeprecated\n)]\n#![deny(clippy::all, clippy::perf, clippy::nursery, clippy::pedantic)]\n#![deny(\n\tclippy::filetype_is_file,\n\tclippy::cargo,\n\tclippy::unwrap_used,\n\tclippy::panic,\n\tclippy::match_like_matches_macro,\n\tclippy::needless_update\n\t//TODO: get this in someday since expect still leads us to crashes sometimes\n\t// clippy::expect_used\n)]\n#![allow(\n\tclippy::module_name_repetitions,\n\tclippy::must_use_candidate,\n\tclippy::missing_errors_doc,\n\tclippy::empty_docs,\n\tclippy::unnecessary_debug_formatting\n)]\n//TODO:\n#![allow(\n\tclippy::significant_drop_tightening,\n\tclippy::missing_panics_doc,\n\tclippy::multiple_crate_versions\n)]\n\npub mod asyncjob;\nmod blame;\nmod branches;\npub mod cached;\nmod commit_files;\nmod diff;\nmod error;\nmod fetch_job;\nmod filter_commits;\nmod progress;\nmod pull;\nmod push;\nmod push_tags;\npub mod remote_progress;\npub mod remote_tags;\nmod revlog;\nmod status;\npub mod sync;\nmod tags;\nmod treefiles;\n\npub use crate::{\n\tblame::{AsyncBlame, BlameParams},\n\tbranches::AsyncBranchesJob,\n\tcommit_files::{AsyncCommitFiles, CommitFilesParams},\n\tdiff::{AsyncDiff, DiffParams, DiffType},\n\terror::{Error, Result},\n\tfetch_job::AsyncFetchJob,\n\tfilter_commits::{AsyncCommitFilterJob, CommitFilterResult},\n\tprogress::ProgressPercent,\n\tpull::{AsyncPull, FetchRequest},\n\tpush::{AsyncPush, PushRequest},\n\tpush_tags::{AsyncPushTags, PushTagsRequest},\n\tremote_progress::{RemoteProgress, RemoteProgressState},\n\trevlog::{AsyncLog, FetchStatus},\n\tstatus::{AsyncStatus, StatusParams},\n\tsync::{\n\t\tdiff::{DiffLine, DiffLineType, FileDiff},\n\t\tremotes::push::PushType,\n\t\tstatus::{StatusItem, StatusItemType},\n\t},\n\ttags::AsyncTags,\n\ttreefiles::AsyncTreeFilesJob,\n};\npub use git2::message_prettify;\nuse std::{\n\tcollections::hash_map::DefaultHasher,\n\thash::{Hash, Hasher},\n};\n\n/// this type is used to communicate events back through the channel\n#[derive(Copy, Clone, Debug, PartialEq, Eq)]\npub enum AsyncGitNotification {\n\t/// this indicates that no new state was fetched but that a async process finished\n\tFinishUnchanged,\n\t///\n\tStatus,\n\t///\n\tDiff,\n\t///\n\tLog,\n\t///\n\tFileLog,\n\t///\n\tCommitFiles,\n\t///\n\tTags,\n\t///\n\tPush,\n\t///\n\tPushTags,\n\t///\n\tPull,\n\t///\n\tBlame,\n\t///\n\tRemoteTags,\n\t///\n\tFetch,\n\t///\n\tBranches,\n\t///\n\tTreeFiles,\n\t///\n\tCommitFilter,\n}\n\n/// helper function to calculate the hash of an arbitrary type that implements the `Hash` trait\npub fn hash<T: Hash + ?Sized>(v: &T) -> u64 {\n\tlet mut hasher = DefaultHasher::new();\n\tv.hash(&mut hasher);\n\thasher.finish()\n}\n\n///\n#[cfg(feature = \"trace-libgit\")]\npub fn register_tracing_logging() -> bool {\n\tfn git_trace(level: git2::TraceLevel, msg: &[u8]) {\n\t\tlog::info!(\"[{:?}]: {}\", level, String::from_utf8_lossy(msg));\n\t}\n\tgit2::trace_set(git2::TraceLevel::Trace, git_trace).is_ok()\n}\n\n///\n#[cfg(not(feature = \"trace-libgit\"))]\npub fn register_tracing_logging() -> bool {\n\ttrue\n}\n"
  },
  {
    "path": "asyncgit/src/progress.rs",
    "content": "//!\n\nuse easy_cast::{Conv, ConvFloat};\nuse std::cmp;\n\n///\n#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]\npub struct ProgressPercent {\n\t/// percent 0..100\n\tpub progress: u8,\n}\n\nimpl ProgressPercent {\n\t///\n\tpub fn new(current: usize, total: usize) -> Self {\n\t\tlet total = f64::conv(cmp::max(current, total));\n\t\tlet progress = f64::conv(current) / total * 100.0;\n\t\tlet progress = u8::try_conv_nearest(progress).unwrap_or(100);\n\t\tSelf { progress }\n\t}\n\t///\n\tpub const fn empty() -> Self {\n\t\tSelf { progress: 0 }\n\t}\n\t///\n\tpub const fn full() -> Self {\n\t\tSelf { progress: 100 }\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\n\t#[test]\n\tfn test_progress_zero_total() {\n\t\tlet prog = ProgressPercent::new(1, 0);\n\n\t\tassert_eq!(prog.progress, 100);\n\t}\n\n\t#[test]\n\tfn test_progress_zero_all() {\n\t\tlet prog = ProgressPercent::new(0, 0);\n\t\tassert_eq!(prog.progress, 100);\n\t}\n\n\t#[test]\n\tfn test_progress_rounding() {\n\t\tlet prog = ProgressPercent::new(2, 10);\n\n\t\tassert_eq!(prog.progress, 20);\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/pull.rs",
    "content": "use crate::{\n\terror::{Error, Result},\n\tsync::{\n\t\tcred::BasicAuthCredential,\n\t\tremotes::{fetch, push::ProgressNotification},\n\t\tRepoPath,\n\t},\n\tAsyncGitNotification, RemoteProgress,\n};\nuse crossbeam_channel::{unbounded, Sender};\nuse std::{\n\tsync::{Arc, Mutex},\n\tthread,\n};\n\n///\n#[derive(Default, Clone, Debug)]\npub struct FetchRequest {\n\t///\n\tpub remote: String,\n\t///\n\tpub branch: String,\n\t///\n\tpub basic_credential: Option<BasicAuthCredential>,\n}\n\n//TODO: since this is empty we can go with a simple AtomicBool to mark that we are fetching or not\n#[derive(Default, Clone, Debug)]\nstruct FetchState {}\n\n///\npub struct AsyncPull {\n\tstate: Arc<Mutex<Option<FetchState>>>,\n\tlast_result: Arc<Mutex<Option<(usize, String)>>>,\n\tprogress: Arc<Mutex<Option<ProgressNotification>>>,\n\tsender: Sender<AsyncGitNotification>,\n\trepo: RepoPath,\n}\n\nimpl AsyncPull {\n\t///\n\tpub fn new(\n\t\trepo: RepoPath,\n\t\tsender: &Sender<AsyncGitNotification>,\n\t) -> Self {\n\t\tSelf {\n\t\t\trepo,\n\t\t\tstate: Arc::new(Mutex::new(None)),\n\t\t\tlast_result: Arc::new(Mutex::new(None)),\n\t\t\tprogress: Arc::new(Mutex::new(None)),\n\t\t\tsender: sender.clone(),\n\t\t}\n\t}\n\n\t///\n\tpub fn is_pending(&self) -> Result<bool> {\n\t\tlet state = self.state.lock()?;\n\t\tOk(state.is_some())\n\t}\n\n\t///\n\tpub fn last_result(&self) -> Result<Option<(usize, String)>> {\n\t\tlet res = self.last_result.lock()?;\n\t\tOk(res.clone())\n\t}\n\n\t///\n\tpub fn progress(&self) -> Result<Option<RemoteProgress>> {\n\t\tlet res = self.progress.lock()?;\n\t\tOk(res.as_ref().map(|progress| progress.clone().into()))\n\t}\n\n\t///\n\tpub fn request(&self, params: FetchRequest) -> Result<()> {\n\t\tlog::trace!(\"request\");\n\n\t\tif self.is_pending()? {\n\t\t\treturn Ok(());\n\t\t}\n\n\t\tself.set_request(&params)?;\n\t\tRemoteProgress::set_progress(&self.progress, None)?;\n\n\t\tlet arc_state = Arc::clone(&self.state);\n\t\tlet arc_res = Arc::clone(&self.last_result);\n\t\tlet arc_progress = Arc::clone(&self.progress);\n\t\tlet sender = self.sender.clone();\n\t\tlet repo = self.repo.clone();\n\n\t\tthread::spawn(move || {\n\t\t\tlet (progress_sender, receiver) = unbounded();\n\n\t\t\tlet handle = RemoteProgress::spawn_receiver_thread(\n\t\t\t\tAsyncGitNotification::Pull,\n\t\t\t\tsender.clone(),\n\t\t\t\treceiver,\n\t\t\t\tarc_progress,\n\t\t\t);\n\n\t\t\tlet res = fetch(\n\t\t\t\t&repo,\n\t\t\t\t&params.branch,\n\t\t\t\tparams.basic_credential,\n\t\t\t\tSome(progress_sender.clone()),\n\t\t\t);\n\n\t\t\tprogress_sender\n\t\t\t\t.send(ProgressNotification::Done)\n\t\t\t\t.expect(\"closing send failed\");\n\n\t\t\thandle.join().expect(\"joining thread failed\");\n\n\t\t\tSelf::set_result(&arc_res, res).expect(\"result error\");\n\n\t\t\tSelf::clear_request(&arc_state).expect(\"clear error\");\n\n\t\t\tsender\n\t\t\t\t.send(AsyncGitNotification::Pull)\n\t\t\t\t.expect(\"AsyncNotification error\");\n\t\t});\n\n\t\tOk(())\n\t}\n\n\tfn set_request(&self, _params: &FetchRequest) -> Result<()> {\n\t\tlet mut state = self.state.lock()?;\n\n\t\tif state.is_some() {\n\t\t\treturn Err(Error::Generic(\"pending request\".into()));\n\t\t}\n\n\t\t*state = Some(FetchState {});\n\n\t\tOk(())\n\t}\n\n\tfn clear_request(\n\t\tstate: &Arc<Mutex<Option<FetchState>>>,\n\t) -> Result<()> {\n\t\tlet mut state = state.lock()?;\n\n\t\t*state = None;\n\n\t\tOk(())\n\t}\n\n\tfn set_result(\n\t\tarc_result: &Arc<Mutex<Option<(usize, String)>>>,\n\t\tres: Result<usize>,\n\t) -> Result<()> {\n\t\tlet mut last_res = arc_result.lock()?;\n\n\t\t*last_res = match res {\n\t\t\tOk(bytes) => Some((bytes, String::new())),\n\t\t\tErr(e) => {\n\t\t\t\tlog::error!(\"fetch error: {e}\");\n\t\t\t\tSome((0, e.to_string()))\n\t\t\t}\n\t\t};\n\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/push.rs",
    "content": "use crate::{\n\terror::{Error, Result},\n\tsync::{\n\t\tcred::BasicAuthCredential,\n\t\tremotes::push::push_raw,\n\t\tremotes::push::{ProgressNotification, PushType},\n\t\tRepoPath,\n\t},\n\tAsyncGitNotification, RemoteProgress,\n};\nuse crossbeam_channel::{unbounded, Sender};\nuse std::{\n\tsync::{Arc, Mutex},\n\tthread,\n};\n\n///\n#[derive(Default, Clone, Debug)]\npub struct PushRequest {\n\t///\n\tpub remote: String,\n\t///\n\tpub branch: String,\n\t///\n\tpub push_type: PushType,\n\t///\n\tpub force: bool,\n\t///\n\tpub delete: bool,\n\t///\n\tpub basic_credential: Option<BasicAuthCredential>,\n}\n\n//TODO: since this is empty we can go with a simple AtomicBool to mark that we are fetching or not\n#[derive(Default, Clone, Debug)]\nstruct PushState {}\n\n///\npub struct AsyncPush {\n\tstate: Arc<Mutex<Option<PushState>>>,\n\tlast_result: Arc<Mutex<Option<String>>>,\n\tprogress: Arc<Mutex<Option<ProgressNotification>>>,\n\tsender: Sender<AsyncGitNotification>,\n\trepo: RepoPath,\n}\n\nimpl AsyncPush {\n\t///\n\tpub fn new(\n\t\trepo: RepoPath,\n\t\tsender: &Sender<AsyncGitNotification>,\n\t) -> Self {\n\t\tSelf {\n\t\t\trepo,\n\t\t\tstate: Arc::new(Mutex::new(None)),\n\t\t\tlast_result: Arc::new(Mutex::new(None)),\n\t\t\tprogress: Arc::new(Mutex::new(None)),\n\t\t\tsender: sender.clone(),\n\t\t}\n\t}\n\n\t///\n\tpub fn is_pending(&self) -> Result<bool> {\n\t\tlet state = self.state.lock()?;\n\t\tOk(state.is_some())\n\t}\n\n\t///\n\tpub fn last_result(&self) -> Result<Option<String>> {\n\t\tlet res = self.last_result.lock()?;\n\t\tOk(res.clone())\n\t}\n\n\t///\n\tpub fn progress(&self) -> Result<Option<RemoteProgress>> {\n\t\tlet res = self.progress.lock()?;\n\t\tOk(res.as_ref().map(|progress| progress.clone().into()))\n\t}\n\n\t///\n\tpub fn request(&self, params: PushRequest) -> Result<()> {\n\t\tlog::trace!(\"request\");\n\n\t\tif self.is_pending()? {\n\t\t\treturn Ok(());\n\t\t}\n\n\t\tself.set_request(&params)?;\n\t\tRemoteProgress::set_progress(&self.progress, None)?;\n\n\t\tlet arc_state = Arc::clone(&self.state);\n\t\tlet arc_res = Arc::clone(&self.last_result);\n\t\tlet arc_progress = Arc::clone(&self.progress);\n\t\tlet sender = self.sender.clone();\n\t\tlet repo = self.repo.clone();\n\n\t\tthread::spawn(move || {\n\t\t\tlet (progress_sender, receiver) = unbounded();\n\n\t\t\tlet handle = RemoteProgress::spawn_receiver_thread(\n\t\t\t\tAsyncGitNotification::Push,\n\t\t\t\tsender.clone(),\n\t\t\t\treceiver,\n\t\t\t\tarc_progress,\n\t\t\t);\n\n\t\t\tlet res = push_raw(\n\t\t\t\t&repo,\n\t\t\t\tparams.remote.as_str(),\n\t\t\t\tparams.branch.as_str(),\n\t\t\t\tparams.push_type,\n\t\t\t\tparams.force,\n\t\t\t\tparams.delete,\n\t\t\t\tparams.basic_credential.clone(),\n\t\t\t\tSome(progress_sender.clone()),\n\t\t\t);\n\n\t\t\tprogress_sender\n\t\t\t\t.send(ProgressNotification::Done)\n\t\t\t\t.expect(\"closing send failed\");\n\n\t\t\thandle.join().expect(\"joining thread failed\");\n\n\t\t\tSelf::set_result(&arc_res, res).expect(\"result error\");\n\n\t\t\tSelf::clear_request(&arc_state).expect(\"clear error\");\n\n\t\t\tsender\n\t\t\t\t.send(AsyncGitNotification::Push)\n\t\t\t\t.expect(\"error sending push\");\n\t\t});\n\n\t\tOk(())\n\t}\n\n\tfn set_request(&self, _params: &PushRequest) -> Result<()> {\n\t\tlet mut state = self.state.lock()?;\n\n\t\tif state.is_some() {\n\t\t\treturn Err(Error::Generic(\"pending request\".into()));\n\t\t}\n\n\t\t*state = Some(PushState {});\n\n\t\tOk(())\n\t}\n\n\tfn clear_request(\n\t\tstate: &Arc<Mutex<Option<PushState>>>,\n\t) -> Result<()> {\n\t\tlet mut state = state.lock()?;\n\n\t\t*state = None;\n\n\t\tOk(())\n\t}\n\n\tfn set_result(\n\t\tarc_result: &Arc<Mutex<Option<String>>>,\n\t\tres: Result<()>,\n\t) -> Result<()> {\n\t\tlet mut last_res = arc_result.lock()?;\n\n\t\t*last_res = match res {\n\t\t\tOk(()) => None,\n\t\t\tErr(e) => {\n\t\t\t\tlog::error!(\"push error: {e}\");\n\t\t\t\tSome(e.to_string())\n\t\t\t}\n\t\t};\n\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/push_tags.rs",
    "content": "use crate::{\n\terror::{Error, Result},\n\tsync::{\n\t\tcred::BasicAuthCredential,\n\t\tremotes::tags::{push_tags, PushTagsProgress},\n\t\tRepoPath,\n\t},\n\tAsyncGitNotification, RemoteProgress,\n};\nuse crossbeam_channel::{unbounded, Sender};\nuse std::{\n\tsync::{Arc, Mutex},\n\tthread,\n};\n\n///\n#[derive(Default, Clone, Debug)]\npub struct PushTagsRequest {\n\t///\n\tpub remote: String,\n\t///\n\tpub basic_credential: Option<BasicAuthCredential>,\n}\n\n//TODO: since this is empty we can go with a simple AtomicBool to mark that we are fetching or not\n#[derive(Default, Clone, Debug)]\nstruct PushState {}\n\n///\npub struct AsyncPushTags {\n\tstate: Arc<Mutex<Option<PushState>>>,\n\tlast_result: Arc<Mutex<Option<String>>>,\n\tprogress: Arc<Mutex<Option<PushTagsProgress>>>,\n\tsender: Sender<AsyncGitNotification>,\n\trepo: RepoPath,\n}\n\nimpl AsyncPushTags {\n\t///\n\tpub fn new(\n\t\trepo: RepoPath,\n\t\tsender: &Sender<AsyncGitNotification>,\n\t) -> Self {\n\t\tSelf {\n\t\t\trepo,\n\t\t\tstate: Arc::new(Mutex::new(None)),\n\t\t\tlast_result: Arc::new(Mutex::new(None)),\n\t\t\tprogress: Arc::new(Mutex::new(None)),\n\t\t\tsender: sender.clone(),\n\t\t}\n\t}\n\n\t///\n\tpub fn is_pending(&self) -> Result<bool> {\n\t\tlet state = self.state.lock()?;\n\t\tOk(state.is_some())\n\t}\n\n\t///\n\tpub fn last_result(&self) -> Result<Option<String>> {\n\t\tlet res = self.last_result.lock()?;\n\t\tOk(res.clone())\n\t}\n\n\t///\n\tpub fn progress(&self) -> Result<Option<PushTagsProgress>> {\n\t\tlet res = self.progress.lock()?;\n\t\tOk(*res)\n\t}\n\n\t///\n\tpub fn request(&self, params: PushTagsRequest) -> Result<()> {\n\t\tlog::trace!(\"request\");\n\n\t\tif self.is_pending()? {\n\t\t\treturn Ok(());\n\t\t}\n\n\t\tself.set_request(&params)?;\n\t\tRemoteProgress::set_progress(&self.progress, None)?;\n\n\t\tlet arc_state = Arc::clone(&self.state);\n\t\tlet arc_res = Arc::clone(&self.last_result);\n\t\tlet arc_progress = Arc::clone(&self.progress);\n\t\tlet sender = self.sender.clone();\n\t\tlet repo = self.repo.clone();\n\n\t\tthread::spawn(move || {\n\t\t\tlet (progress_sender, receiver) = unbounded();\n\n\t\t\tlet handle = RemoteProgress::spawn_receiver_thread(\n\t\t\t\tAsyncGitNotification::PushTags,\n\t\t\t\tsender.clone(),\n\t\t\t\treceiver,\n\t\t\t\tarc_progress,\n\t\t\t);\n\n\t\t\tlet res = push_tags(\n\t\t\t\t&repo,\n\t\t\t\tparams.remote.as_str(),\n\t\t\t\tparams.basic_credential.clone(),\n\t\t\t\tSome(progress_sender),\n\t\t\t);\n\n\t\t\thandle.join().expect(\"joining thread failed\");\n\n\t\t\tSelf::set_result(&arc_res, res).expect(\"result error\");\n\n\t\t\tSelf::clear_request(&arc_state).expect(\"clear error\");\n\n\t\t\tsender\n\t\t\t\t.send(AsyncGitNotification::PushTags)\n\t\t\t\t.expect(\"error sending push\");\n\t\t});\n\n\t\tOk(())\n\t}\n\n\tfn set_request(&self, _params: &PushTagsRequest) -> Result<()> {\n\t\tlet mut state = self.state.lock()?;\n\n\t\tif state.is_some() {\n\t\t\treturn Err(Error::Generic(\"pending request\".into()));\n\t\t}\n\n\t\t*state = Some(PushState {});\n\n\t\tOk(())\n\t}\n\n\tfn clear_request(\n\t\tstate: &Arc<Mutex<Option<PushState>>>,\n\t) -> Result<()> {\n\t\tlet mut state = state.lock()?;\n\n\t\t*state = None;\n\n\t\tOk(())\n\t}\n\n\tfn set_result(\n\t\tarc_result: &Arc<Mutex<Option<String>>>,\n\t\tres: Result<()>,\n\t) -> Result<()> {\n\t\tlet mut last_res = arc_result.lock()?;\n\n\t\t*last_res = match res {\n\t\t\tOk(()) => None,\n\t\t\tErr(e) => {\n\t\t\t\tlog::error!(\"push error: {e}\");\n\t\t\t\tSome(e.to_string())\n\t\t\t}\n\t\t};\n\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/remote_progress.rs",
    "content": "//!\n\nuse crate::{\n\terror::Result,\n\tprogress::ProgressPercent,\n\tsync::remotes::push::{AsyncProgress, ProgressNotification},\n\tAsyncGitNotification,\n};\nuse crossbeam_channel::{Receiver, Sender};\nuse git2::PackBuilderStage;\nuse std::{\n\tsync::{Arc, Mutex},\n\tthread::{self, JoinHandle},\n};\n\n/// used for push/pull\n#[derive(Clone, Debug)]\npub enum RemoteProgressState {\n\t///\n\tPackingAddingObject,\n\t///\n\tPackingDeltafiction,\n\t///\n\tPushing,\n\t/// fetch progress\n\tTransfer,\n\t/// remote progress done\n\tDone,\n}\n\n///\n#[derive(Clone, Debug)]\npub struct RemoteProgress {\n\t///\n\tpub state: RemoteProgressState,\n\t///\n\tpub progress: ProgressPercent,\n}\n\nimpl RemoteProgress {\n\t///\n\tpub fn new(\n\t\tstate: RemoteProgressState,\n\t\tcurrent: usize,\n\t\ttotal: usize,\n\t) -> Self {\n\t\tSelf {\n\t\t\tstate,\n\t\t\tprogress: ProgressPercent::new(current, total),\n\t\t}\n\t}\n\n\t///\n\tpub const fn get_progress_percent(&self) -> u8 {\n\t\tself.progress.progress\n\t}\n\n\tpub(crate) fn set_progress<T>(\n\t\tprogress: &Arc<Mutex<Option<T>>>,\n\t\tstate: Option<T>,\n\t) -> Result<()> {\n\t\tlet mut progress = progress.lock()?;\n\n\t\t*progress = state;\n\n\t\tOk(())\n\t}\n\n\t/// spawn thread to listen to progress notifications coming in from blocking remote git method (fetch/push)\n\tpub(crate) fn spawn_receiver_thread<\n\t\tT: 'static + AsyncProgress,\n\t>(\n\t\tnotification_type: AsyncGitNotification,\n\t\tsender: Sender<AsyncGitNotification>,\n\t\treceiver: Receiver<T>,\n\t\tprogress: Arc<Mutex<Option<T>>>,\n\t) -> JoinHandle<()> {\n\t\tthread::spawn(move || loop {\n\t\t\tlet incoming = receiver.recv();\n\t\t\tmatch incoming {\n\t\t\t\tOk(update) => {\n\t\t\t\t\tSelf::set_progress(\n\t\t\t\t\t\t&progress,\n\t\t\t\t\t\tSome(update.clone()),\n\t\t\t\t\t)\n\t\t\t\t\t.expect(\"set progress failed\");\n\t\t\t\t\tsender\n\t\t\t\t\t\t.send(notification_type)\n\t\t\t\t\t\t.expect(\"Notification error\");\n\n\t\t\t\t\tthread::yield_now();\n\n\t\t\t\t\tif update.is_done() {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tErr(e) => {\n\t\t\t\t\tlog::error!(\n\t\t\t\t\t\t\"remote progress receiver error: {e}\",\n\t\t\t\t\t);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nimpl From<ProgressNotification> for RemoteProgress {\n\tfn from(progress: ProgressNotification) -> Self {\n\t\tmatch progress {\n\t\t\tProgressNotification::Packing {\n\t\t\t\tstage,\n\t\t\t\tcurrent,\n\t\t\t\ttotal,\n\t\t\t} => match stage {\n\t\t\t\tPackBuilderStage::AddingObjects => Self::new(\n\t\t\t\t\tRemoteProgressState::PackingAddingObject,\n\t\t\t\t\tcurrent,\n\t\t\t\t\ttotal,\n\t\t\t\t),\n\t\t\t\tPackBuilderStage::Deltafication => Self::new(\n\t\t\t\t\tRemoteProgressState::PackingDeltafiction,\n\t\t\t\t\tcurrent,\n\t\t\t\t\ttotal,\n\t\t\t\t),\n\t\t\t},\n\t\t\tProgressNotification::PushTransfer {\n\t\t\t\tcurrent,\n\t\t\t\ttotal,\n\t\t\t\t..\n\t\t\t} => Self::new(\n\t\t\t\tRemoteProgressState::Pushing,\n\t\t\t\tcurrent,\n\t\t\t\ttotal,\n\t\t\t),\n\t\t\tProgressNotification::Transfer {\n\t\t\t\tobjects,\n\t\t\t\ttotal_objects,\n\t\t\t\t..\n\t\t\t} => Self::new(\n\t\t\t\tRemoteProgressState::Transfer,\n\t\t\t\tobjects,\n\t\t\t\ttotal_objects,\n\t\t\t),\n\t\t\t_ => Self::new(RemoteProgressState::Done, 1, 1),\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/remote_tags.rs",
    "content": "//!\n\nuse crate::{\n\tasyncjob::{AsyncJob, RunParams},\n\terror::Result,\n\tsync::cred::BasicAuthCredential,\n\tsync::{\n\t\tremotes::{get_default_remote, tags_missing_remote},\n\t\tRepoPath,\n\t},\n\tAsyncGitNotification,\n};\n\nuse std::sync::{Arc, Mutex};\n\nenum JobState {\n\tRequest(Option<BasicAuthCredential>),\n\tResponse(Result<Vec<String>>),\n}\n\n///\n#[derive(Clone)]\npub struct AsyncRemoteTagsJob {\n\tstate: Arc<Mutex<Option<JobState>>>,\n\trepo: RepoPath,\n}\n\n///\nimpl AsyncRemoteTagsJob {\n\t///\n\tpub fn new(\n\t\trepo: RepoPath,\n\t\tbasic_credential: Option<BasicAuthCredential>,\n\t) -> Self {\n\t\tSelf {\n\t\t\trepo,\n\t\t\tstate: Arc::new(Mutex::new(Some(JobState::Request(\n\t\t\t\tbasic_credential,\n\t\t\t)))),\n\t\t}\n\t}\n\n\t///\n\tpub fn result(&self) -> Option<Result<Vec<String>>> {\n\t\tif let Ok(mut state) = self.state.lock() {\n\t\t\tif let Some(state) = state.take() {\n\t\t\t\treturn match state {\n\t\t\t\t\tJobState::Request(_) => None,\n\t\t\t\t\tJobState::Response(result) => Some(result),\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tNone\n\t}\n}\n\nimpl AsyncJob for AsyncRemoteTagsJob {\n\ttype Notification = AsyncGitNotification;\n\ttype Progress = ();\n\n\tfn run(\n\t\t&mut self,\n\t\t_params: RunParams<Self::Notification, Self::Progress>,\n\t) -> Result<Self::Notification> {\n\t\tif let Ok(mut state) = self.state.lock() {\n\t\t\t*state = state.take().map(|state| match state {\n\t\t\t\tJobState::Request(basic_credential) => {\n\t\t\t\t\tlet result = get_default_remote(&self.repo)\n\t\t\t\t\t\t.and_then(|remote| {\n\t\t\t\t\t\t\ttags_missing_remote(\n\t\t\t\t\t\t\t\t&self.repo,\n\t\t\t\t\t\t\t\t&remote,\n\t\t\t\t\t\t\t\tbasic_credential,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t});\n\n\t\t\t\t\tJobState::Response(result)\n\t\t\t\t}\n\t\t\t\tJobState::Response(result) => {\n\t\t\t\t\tJobState::Response(result)\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\tOk(AsyncGitNotification::RemoteTags)\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/revlog.rs",
    "content": "use crate::{\n\terror::Result,\n\tsync::{\n\t\tgix_repo, repo, CommitId, LogWalker, LogWalkerWithoutFilter,\n\t\tRepoPath, SharedCommitFilterFn,\n\t},\n\tAsyncGitNotification, Error,\n};\nuse crossbeam_channel::Sender;\nuse scopetime::scope_time;\nuse std::{\n\tsync::{\n\t\tatomic::{AtomicBool, Ordering},\n\t\tArc, Mutex,\n\t},\n\tthread,\n\ttime::{Duration, Instant},\n};\n\n///\n#[derive(PartialEq, Eq, Debug)]\npub enum FetchStatus {\n\t/// previous fetch still running\n\tPending,\n\t/// no change expected\n\tNoChange,\n\t/// new walk was started\n\tStarted,\n}\n\n///\npub struct AsyncLogResult {\n\t///\n\tpub commits: Vec<CommitId>,\n\t///\n\tpub duration: Duration,\n}\n///\npub struct AsyncLog {\n\tcurrent: Arc<Mutex<AsyncLogResult>>,\n\tcurrent_head: Arc<Mutex<Option<CommitId>>>,\n\tsender: Sender<AsyncGitNotification>,\n\tpending: Arc<AtomicBool>,\n\tbackground: Arc<AtomicBool>,\n\tfilter: Option<SharedCommitFilterFn>,\n\tpartial_extract: AtomicBool,\n\trepo: RepoPath,\n}\n\nstatic LIMIT_COUNT: usize = 3000;\nstatic SLEEP_FOREGROUND: Duration = Duration::from_millis(2);\nstatic SLEEP_BACKGROUND: Duration = Duration::from_secs(1);\n\nimpl AsyncLog {\n\t///\n\tpub fn new(\n\t\trepo: RepoPath,\n\t\tsender: &Sender<AsyncGitNotification>,\n\t\tfilter: Option<SharedCommitFilterFn>,\n\t) -> Self {\n\t\tSelf {\n\t\t\trepo,\n\t\t\tcurrent: Arc::new(Mutex::new(AsyncLogResult {\n\t\t\t\tcommits: Vec::new(),\n\t\t\t\tduration: Duration::default(),\n\t\t\t})),\n\t\t\tcurrent_head: Arc::new(Mutex::new(None)),\n\t\t\tsender: sender.clone(),\n\t\t\tpending: Arc::new(AtomicBool::new(false)),\n\t\t\tbackground: Arc::new(AtomicBool::new(false)),\n\t\t\tfilter,\n\t\t\tpartial_extract: AtomicBool::new(false),\n\t\t}\n\t}\n\n\t///\n\tpub fn count(&self) -> Result<usize> {\n\t\tOk(self.current.lock()?.commits.len())\n\t}\n\n\t///\n\tpub fn get_slice(\n\t\t&self,\n\t\tstart_index: usize,\n\t\tamount: usize,\n\t) -> Result<Vec<CommitId>> {\n\t\tif self.partial_extract.load(Ordering::Relaxed) {\n\t\t\treturn Err(Error::Generic(String::from(\"Faulty usage of AsyncLog: Cannot partially extract items and rely on get_items slice to still work!\")));\n\t\t}\n\n\t\tlet list = &self.current.lock()?.commits;\n\t\tlet list_len = list.len();\n\t\tlet min = start_index.min(list_len);\n\t\tlet max = min + amount;\n\t\tlet max = max.min(list_len);\n\t\tOk(list[min..max].to_vec())\n\t}\n\n\t///\n\tpub fn get_items(&self) -> Result<Vec<CommitId>> {\n\t\tif self.partial_extract.load(Ordering::Relaxed) {\n\t\t\treturn Err(Error::Generic(String::from(\"Faulty usage of AsyncLog: Cannot partially extract items and rely on get_items slice to still work!\")));\n\t\t}\n\n\t\tlet list = &self.current.lock()?.commits;\n\t\tOk(list.clone())\n\t}\n\n\t///\n\tpub fn extract_items(&self) -> Result<Vec<CommitId>> {\n\t\tself.partial_extract.store(true, Ordering::Relaxed);\n\t\tlet list = &mut self.current.lock()?.commits;\n\t\tlet result = list.clone();\n\t\tlist.clear();\n\t\tOk(result)\n\t}\n\n\t///\n\tpub fn get_last_duration(&self) -> Result<Duration> {\n\t\tOk(self.current.lock()?.duration)\n\t}\n\n\t///\n\tpub fn is_pending(&self) -> bool {\n\t\tself.pending.load(Ordering::Relaxed)\n\t}\n\n\t///\n\tpub fn set_background(&self) {\n\t\tself.background.store(true, Ordering::Relaxed);\n\t}\n\n\t///\n\tfn current_head(&self) -> Result<Option<CommitId>> {\n\t\tOk(*self.current_head.lock()?)\n\t}\n\n\t///\n\tfn head_changed(&self) -> Result<bool> {\n\t\tif let Ok(head) = repo(&self.repo)?.head() {\n\t\t\treturn Ok(\n\t\t\t\thead.target() != self.current_head()?.map(Into::into)\n\t\t\t);\n\t\t}\n\t\tOk(false)\n\t}\n\n\t///\n\tpub fn fetch(&self) -> Result<FetchStatus> {\n\t\tself.background.store(false, Ordering::Relaxed);\n\n\t\tif self.is_pending() {\n\t\t\treturn Ok(FetchStatus::Pending);\n\t\t}\n\n\t\tif !self.head_changed()? {\n\t\t\treturn Ok(FetchStatus::NoChange);\n\t\t}\n\n\t\tself.pending.store(true, Ordering::Relaxed);\n\n\t\tself.clear()?;\n\n\t\tlet arc_current = Arc::clone(&self.current);\n\t\tlet sender = self.sender.clone();\n\t\tlet arc_pending = Arc::clone(&self.pending);\n\t\tlet arc_background = Arc::clone(&self.background);\n\t\tlet filter = self.filter.clone();\n\t\tlet repo_path = self.repo.clone();\n\n\t\tif let Ok(head) = repo(&self.repo)?.head() {\n\t\t\t*self.current_head.lock()? =\n\t\t\t\thead.target().map(CommitId::new);\n\t\t}\n\n\t\trayon_core::spawn(move || {\n\t\t\tscope_time!(\"async::revlog\");\n\n\t\t\tSelf::fetch_helper(\n\t\t\t\t&repo_path,\n\t\t\t\t&arc_current,\n\t\t\t\t&arc_background,\n\t\t\t\t&sender,\n\t\t\t\tfilter,\n\t\t\t)\n\t\t\t.expect(\"failed to fetch\");\n\n\t\t\tarc_pending.store(false, Ordering::Relaxed);\n\n\t\t\tSelf::notify(&sender);\n\t\t});\n\n\t\tOk(FetchStatus::Started)\n\t}\n\n\tfn fetch_helper(\n\t\trepo_path: &RepoPath,\n\t\tarc_current: &Arc<Mutex<AsyncLogResult>>,\n\t\tarc_background: &Arc<AtomicBool>,\n\t\tsender: &Sender<AsyncGitNotification>,\n\t\tfilter: Option<SharedCommitFilterFn>,\n\t) -> Result<()> {\n\t\tfilter.map_or_else(\n\t\t\t|| {\n\t\t\t\tSelf::fetch_helper_without_filter(\n\t\t\t\t\trepo_path,\n\t\t\t\t\tarc_current,\n\t\t\t\t\tarc_background,\n\t\t\t\t\tsender,\n\t\t\t\t)\n\t\t\t},\n\t\t\t|filter| {\n\t\t\t\tSelf::fetch_helper_with_filter(\n\t\t\t\t\trepo_path,\n\t\t\t\t\tarc_current,\n\t\t\t\t\tarc_background,\n\t\t\t\t\tsender,\n\t\t\t\t\tfilter,\n\t\t\t\t)\n\t\t\t},\n\t\t)\n\t}\n\n\tfn fetch_helper_with_filter(\n\t\trepo_path: &RepoPath,\n\t\tarc_current: &Arc<Mutex<AsyncLogResult>>,\n\t\tarc_background: &Arc<AtomicBool>,\n\t\tsender: &Sender<AsyncGitNotification>,\n\t\tfilter: SharedCommitFilterFn,\n\t) -> Result<()> {\n\t\tlet start_time = Instant::now();\n\n\t\tlet mut entries = vec![CommitId::default(); LIMIT_COUNT];\n\t\tentries.resize(0, CommitId::default());\n\n\t\tlet r = repo(repo_path)?;\n\t\tlet mut walker =\n\t\t\tLogWalker::new(&r, LIMIT_COUNT)?.filter(Some(filter));\n\n\t\tloop {\n\t\t\tentries.clear();\n\t\t\tlet read = walker.read(&mut entries)?;\n\n\t\t\tlet mut current = arc_current.lock()?;\n\t\t\tcurrent.commits.extend(entries.iter());\n\t\t\tcurrent.duration = start_time.elapsed();\n\n\t\t\tif read == 0 {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tSelf::notify(sender);\n\n\t\t\tlet sleep_duration =\n\t\t\t\tif arc_background.load(Ordering::Relaxed) {\n\t\t\t\t\tSLEEP_BACKGROUND\n\t\t\t\t} else {\n\t\t\t\t\tSLEEP_FOREGROUND\n\t\t\t\t};\n\n\t\t\tthread::sleep(sleep_duration);\n\t\t}\n\n\t\tlog::trace!(\"revlog visited: {}\", walker.visited());\n\n\t\tOk(())\n\t}\n\n\tfn fetch_helper_without_filter(\n\t\trepo_path: &RepoPath,\n\t\tarc_current: &Arc<Mutex<AsyncLogResult>>,\n\t\tarc_background: &Arc<AtomicBool>,\n\t\tsender: &Sender<AsyncGitNotification>,\n\t) -> Result<()> {\n\t\tlet start_time = Instant::now();\n\n\t\tlet mut entries = vec![CommitId::default(); LIMIT_COUNT];\n\t\tentries.resize(0, CommitId::default());\n\n\t\tlet mut repo: gix::Repository = gix_repo(repo_path)?;\n\t\tlet mut walker =\n\t\t\tLogWalkerWithoutFilter::new(&mut repo, LIMIT_COUNT)?;\n\n\t\tloop {\n\t\t\tentries.clear();\n\t\t\tlet read = walker.read(&mut entries)?;\n\n\t\t\tlet mut current = arc_current.lock()?;\n\t\t\tcurrent.commits.extend(entries.iter());\n\t\t\tcurrent.duration = start_time.elapsed();\n\n\t\t\tif read == 0 {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tSelf::notify(sender);\n\n\t\t\tlet sleep_duration =\n\t\t\t\tif arc_background.load(Ordering::Relaxed) {\n\t\t\t\t\tSLEEP_BACKGROUND\n\t\t\t\t} else {\n\t\t\t\t\tSLEEP_FOREGROUND\n\t\t\t\t};\n\n\t\t\tthread::sleep(sleep_duration);\n\t\t}\n\n\t\tlog::trace!(\"revlog visited: {}\", walker.visited());\n\n\t\tOk(())\n\t}\n\n\tfn clear(&self) -> Result<()> {\n\t\tself.current.lock()?.commits.clear();\n\t\t*self.current_head.lock()? = None;\n\t\tself.partial_extract.store(false, Ordering::Relaxed);\n\t\tOk(())\n\t}\n\n\tfn notify(sender: &Sender<AsyncGitNotification>) {\n\t\tsender\n\t\t\t.send(AsyncGitNotification::Log)\n\t\t\t.expect(\"error sending\");\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse std::sync::atomic::AtomicBool;\n\tuse std::sync::{Arc, Mutex};\n\tuse std::time::Duration;\n\n\tuse crossbeam_channel::unbounded;\n\tuse serial_test::serial;\n\tuse tempfile::TempDir;\n\n\tuse crate::sync::tests::{debug_cmd_print, repo_init};\n\tuse crate::sync::RepoPath;\n\tuse crate::AsyncLog;\n\n\tuse super::AsyncLogResult;\n\n\t#[test]\n\t#[serial]\n\tfn test_smoke_in_subdir() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: RepoPath =\n\t\t\troot.as_os_str().to_str().unwrap().into();\n\n\t\tlet (tx_git, _rx_git) = unbounded();\n\n\t\tdebug_cmd_print(&repo_path, \"mkdir subdir\");\n\n\t\tlet subdir = repo.path().parent().unwrap().join(\"subdir\");\n\t\tlet subdir_path: RepoPath =\n\t\t\tsubdir.as_os_str().to_str().unwrap().into();\n\n\t\tlet arc_current = Arc::new(Mutex::new(AsyncLogResult {\n\t\t\tcommits: Vec::new(),\n\t\t\tduration: Duration::default(),\n\t\t}));\n\t\tlet arc_background = Arc::new(AtomicBool::new(false));\n\n\t\tlet result = AsyncLog::fetch_helper_without_filter(\n\t\t\t&subdir_path,\n\t\t\t&arc_current,\n\t\t\t&arc_background,\n\t\t\t&tx_git,\n\t\t);\n\n\t\tassert_eq!(result.unwrap(), ());\n\t}\n\n\t#[test]\n\t#[serial]\n\tfn test_env_variables() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet git_dir = repo.path();\n\n\t\tlet (tx_git, _rx_git) = unbounded();\n\n\t\tlet empty_dir = TempDir::new().unwrap();\n\t\tlet empty_path: RepoPath =\n\t\t\tempty_dir.path().to_str().unwrap().into();\n\n\t\tlet arc_current = Arc::new(Mutex::new(AsyncLogResult {\n\t\t\tcommits: Vec::new(),\n\t\t\tduration: Duration::default(),\n\t\t}));\n\t\tlet arc_background = Arc::new(AtomicBool::new(false));\n\n\t\tstd::env::set_var(\"GIT_DIR\", git_dir);\n\n\t\tlet result = AsyncLog::fetch_helper_without_filter(\n\t\t\t// We pass an empty path, thus testing whether `GIT_DIR`, set above, is taken into account.\n\t\t\t&empty_path,\n\t\t\t&arc_current,\n\t\t\t&arc_background,\n\t\t\t&tx_git,\n\t\t);\n\n\t\tstd::env::remove_var(\"GIT_DIR\");\n\n\t\tassert_eq!(result.unwrap(), ());\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/status.rs",
    "content": "use crate::{\n\terror::Result,\n\thash,\n\tsync::{\n\t\tself, status::StatusType, RepoPath, ShowUntrackedFilesConfig,\n\t},\n\tAsyncGitNotification, StatusItem,\n};\nuse crossbeam_channel::Sender;\nuse std::{\n\thash::Hash,\n\tsync::{\n\t\tatomic::{AtomicU64, AtomicUsize, Ordering},\n\t\tArc, Mutex,\n\t},\n};\n\n#[derive(Default, Hash, Clone)]\npub struct Status {\n\tpub items: Vec<StatusItem>,\n}\n\n///\n#[derive(Default, Hash, Copy, Clone, PartialEq, Eq)]\npub struct StatusParams {\n\tstatus_type: StatusType,\n\tconfig: Option<ShowUntrackedFilesConfig>,\n}\n\nimpl StatusParams {\n\t///\n\tpub const fn new(\n\t\tstatus_type: StatusType,\n\t\tconfig: Option<ShowUntrackedFilesConfig>,\n\t) -> Self {\n\t\tSelf {\n\t\t\tstatus_type,\n\t\t\tconfig,\n\t\t}\n\t}\n}\n\nstruct Request<R, A>(R, Option<A>);\n\n///\npub struct AsyncStatus {\n\tcurrent: Arc<Mutex<Request<u64, Status>>>,\n\tlast: Arc<Mutex<Status>>,\n\tsender: Sender<AsyncGitNotification>,\n\tpending: Arc<AtomicUsize>,\n\trepo: RepoPath,\n\t/// Counter that increments after each completed fetch.\n\tgeneration: Arc<AtomicU64>,\n}\n\nimpl AsyncStatus {\n\t///\n\tpub fn new(\n\t\trepo: RepoPath,\n\t\tsender: Sender<AsyncGitNotification>,\n\t) -> Self {\n\t\tSelf {\n\t\t\trepo,\n\t\t\tcurrent: Arc::new(Mutex::new(Request(0, None))),\n\t\t\tlast: Arc::new(Mutex::new(Status::default())),\n\t\t\tsender,\n\t\t\tpending: Arc::new(AtomicUsize::new(0)),\n\t\t\tgeneration: Arc::new(AtomicU64::new(0)),\n\t\t}\n\t}\n\n\t///\n\tpub fn last(&self) -> Result<Status> {\n\t\tlet last = self.last.lock()?;\n\t\tOk(last.clone())\n\t}\n\n\t///\n\tpub fn is_pending(&self) -> bool {\n\t\tself.pending.load(Ordering::Relaxed) > 0\n\t}\n\n\t///\n\tpub fn fetch(\n\t\t&self,\n\t\tparams: &StatusParams,\n\t) -> Result<Option<Status>> {\n\t\tif self.is_pending() {\n\t\t\tlog::trace!(\"request blocked, still pending\");\n\t\t\treturn Ok(None);\n\t\t}\n\n\t\tlet generation = self.generation.load(Ordering::Relaxed);\n\t\tlet hash_request = hash(&(params, generation));\n\n\t\tlog::trace!(\n\t\t\t\"request: [hash: {}] (type: {:?}, gen: {})\",\n\t\t\thash_request,\n\t\t\tparams.status_type,\n\t\t\tgeneration,\n\t\t);\n\n\t\t{\n\t\t\tlet mut current = self.current.lock()?;\n\n\t\t\tif current.0 == hash_request {\n\t\t\t\treturn Ok(current.1.clone());\n\t\t\t}\n\n\t\t\tcurrent.0 = hash_request;\n\t\t\tcurrent.1 = None;\n\t\t}\n\n\t\tlet arc_current = Arc::clone(&self.current);\n\t\tlet arc_last = Arc::clone(&self.last);\n\t\tlet arc_generation = Arc::clone(&self.generation);\n\t\tlet sender = self.sender.clone();\n\t\tlet arc_pending = Arc::clone(&self.pending);\n\t\tlet status_type = params.status_type;\n\t\tlet config = params.config;\n\t\tlet repo = self.repo.clone();\n\n\t\tself.pending.fetch_add(1, Ordering::Relaxed);\n\n\t\trayon_core::spawn(move || {\n\t\t\tif let Err(e) = Self::fetch_helper(\n\t\t\t\t&repo,\n\t\t\t\tstatus_type,\n\t\t\t\tconfig,\n\t\t\t\thash_request,\n\t\t\t\t&arc_current,\n\t\t\t\t&arc_last,\n\t\t\t) {\n\t\t\t\tlog::error!(\"fetch_helper: {e}\");\n\t\t\t}\n\n\t\t\t// Increment generation to invalidate cache for next request\n\t\t\tarc_generation.fetch_add(1, Ordering::Relaxed);\n\t\t\tarc_pending.fetch_sub(1, Ordering::Relaxed);\n\n\t\t\tsender\n\t\t\t\t.send(AsyncGitNotification::Status)\n\t\t\t\t.expect(\"error sending status\");\n\t\t});\n\n\t\tOk(None)\n\t}\n\n\tfn fetch_helper(\n\t\trepo: &RepoPath,\n\t\tstatus_type: StatusType,\n\t\tconfig: Option<ShowUntrackedFilesConfig>,\n\t\thash_request: u64,\n\t\tarc_current: &Arc<Mutex<Request<u64, Status>>>,\n\t\tarc_last: &Arc<Mutex<Status>>,\n\t) -> Result<()> {\n\t\tlet res = Self::get_status(repo, status_type, config)?;\n\t\tlog::trace!(\n\t\t\t\"status fetched: {hash_request} (type: {status_type:?})\",\n\t\t);\n\n\t\t{\n\t\t\tlet mut current = arc_current.lock()?;\n\t\t\tif current.0 == hash_request {\n\t\t\t\tcurrent.1 = Some(res.clone());\n\t\t\t}\n\t\t}\n\n\t\t{\n\t\t\tlet mut last = arc_last.lock()?;\n\t\t\t*last = res;\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn get_status(\n\t\trepo: &RepoPath,\n\t\tstatus_type: StatusType,\n\t\tconfig: Option<ShowUntrackedFilesConfig>,\n\t) -> Result<Status> {\n\t\tOk(Status {\n\t\t\titems: sync::status::get_status(\n\t\t\t\trepo,\n\t\t\t\tstatus_type,\n\t\t\t\tconfig,\n\t\t\t)?,\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/blame.rs",
    "content": "//! Sync git API for fetching a file blame\n\nuse super::{utils, CommitId, RepoPath};\nuse crate::{\n\terror::{Error, Result},\n\tsync::{get_commits_info, repository::repo},\n};\nuse git2::BlameOptions;\nuse scopetime::scope_time;\nuse std::collections::{HashMap, HashSet};\nuse std::io::{BufRead, BufReader};\nuse std::path::Path;\n\n/// A `BlameHunk` contains all the information that will be shown to the user.\n#[derive(Clone, Hash, Debug, PartialEq, Eq)]\npub struct BlameHunk {\n\t///\n\tpub commit_id: CommitId,\n\t///\n\tpub author: String,\n\t///\n\tpub time: i64,\n\t/// `git2::BlameHunk::final_start_line` returns 1-based indices, but\n\t/// `start_line` is 0-based because the `Vec` storing the lines starts at\n\t/// index 0.\n\tpub start_line: usize,\n\t///\n\tpub end_line: usize,\n}\n\n/// A `BlameFile` represents a collection of lines. This is targeted at how the\n/// data will be used by the UI.\n#[derive(Clone, Debug)]\npub struct FileBlame {\n\t///\n\tpub commit_id: CommitId,\n\t///\n\tpub path: String,\n\t///\n\tpub lines: Vec<(Option<BlameHunk>, String)>,\n}\n\n/// fixup `\\` windows path separators to git compatible `/`\nfn fixup_windows_path(path: &str) -> String {\n\t#[cfg(windows)]\n\t{\n\t\tpath.replace('\\\\', \"/\")\n\t}\n\n\t#[cfg(not(windows))]\n\t{\n\t\tpath.to_string()\n\t}\n}\n\n///\npub fn blame_file(\n\trepo_path: &RepoPath,\n\tfile_path: &str,\n\tcommit_id: Option<CommitId>,\n) -> Result<FileBlame> {\n\tscope_time!(\"blame_file\");\n\n\tlet repo = repo(repo_path)?;\n\n\tlet commit_id = if let Some(commit_id) = commit_id {\n\t\tcommit_id\n\t} else {\n\t\tutils::get_head_repo(&repo)?\n\t};\n\n\tlet spec =\n\t\tformat!(\"{}:{}\", commit_id, fixup_windows_path(file_path));\n\n\tlet object = repo.revparse_single(&spec)?;\n\tlet blob = repo.find_blob(object.id())?;\n\n\tif blob.is_binary() {\n\t\treturn Err(Error::NoBlameOnBinaryFile);\n\t}\n\n\tlet mut opts = BlameOptions::new();\n\topts.newest_commit(commit_id.into());\n\n\tlet blame =\n\t\trepo.blame_file(Path::new(file_path), Some(&mut opts))?;\n\n\tlet reader = BufReader::new(blob.content());\n\n\tlet unique_commit_ids: HashSet<_> = blame\n\t\t.iter()\n\t\t.map(|hunk| CommitId::new(hunk.final_commit_id()))\n\t\t.collect();\n\tlet mut commit_ids = Vec::with_capacity(unique_commit_ids.len());\n\tcommit_ids.extend(unique_commit_ids);\n\n\tlet commit_infos = get_commits_info(repo_path, &commit_ids, 0)?;\n\tlet unique_commit_infos: HashMap<_, _> = commit_infos\n\t\t.iter()\n\t\t.map(|commit_info| (commit_info.id, commit_info))\n\t\t.collect();\n\n\tlet lines: Vec<(Option<BlameHunk>, String)> = reader\n\t\t.lines()\n\t\t.enumerate()\n\t\t.map(|(i, line)| {\n\t\t\t// Line indices in a `FileBlame` are 1-based.\n\t\t\tlet corresponding_hunk = blame.get_line(i + 1);\n\n\t\t\tif let Some(hunk) = corresponding_hunk {\n\t\t\t\tlet commit_id = CommitId::new(hunk.final_commit_id());\n\t\t\t\t// Line indices in a `BlameHunk` are 1-based.\n\t\t\t\tlet start_line =\n\t\t\t\t\thunk.final_start_line().saturating_sub(1);\n\t\t\t\tlet end_line =\n\t\t\t\t\tstart_line.saturating_add(hunk.lines_in_hunk());\n\n\t\t\t\tif let Some(commit_info) =\n\t\t\t\t\tunique_commit_infos.get(&commit_id)\n\t\t\t\t{\n\t\t\t\t\tlet hunk = BlameHunk {\n\t\t\t\t\t\tcommit_id,\n\t\t\t\t\t\tauthor: commit_info.author.clone(),\n\t\t\t\t\t\ttime: commit_info.time,\n\t\t\t\t\t\tstart_line,\n\t\t\t\t\t\tend_line,\n\t\t\t\t\t};\n\n\t\t\t\t\treturn (\n\t\t\t\t\t\tSome(hunk),\n\t\t\t\t\t\tline.unwrap_or_else(|_| String::new()),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t(None, line.unwrap_or_else(|_| String::new()))\n\t\t})\n\t\t.collect();\n\n\tlet file_blame = FileBlame {\n\t\tcommit_id,\n\t\tpath: file_path.into(),\n\t\tlines,\n\t};\n\n\tOk(file_blame)\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::{\n\t\terror::Result,\n\t\tsync::{commit, stage_add_file, tests::repo_init_empty},\n\t};\n\tuse std::{\n\t\tfs::{File, OpenOptions},\n\t\tio::Write,\n\t\tpath::Path,\n\t};\n\n\t#[test]\n\tfn test_blame() -> Result<()> {\n\t\tlet file_path = Path::new(\"foo\");\n\t\tlet (_td, repo) = repo_init_empty()?;\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tassert!(blame_file(repo_path, \"foo\", None).is_err());\n\n\t\tFile::create(root.join(file_path))?.write_all(b\"line 1\\n\")?;\n\n\t\tstage_add_file(repo_path, file_path)?;\n\t\tcommit(repo_path, \"first commit\")?;\n\n\t\tlet blame = blame_file(repo_path, \"foo\", None)?;\n\n\t\tassert!(matches!(\n\t\t\tblame.lines.as_slice(),\n\t\t\t[(\n\t\t\t\tSome(BlameHunk {\n\t\t\t\t\tauthor,\n\t\t\t\t\tstart_line: 0,\n\t\t\t\t\tend_line: 1,\n\t\t\t\t\t..\n\t\t\t\t}),\n\t\t\t\tline\n\t\t\t)] if author == \"name\" && line == \"line 1\"\n\t\t));\n\n\t\tlet mut file = OpenOptions::new()\n\t\t\t.append(true)\n\t\t\t.open(root.join(file_path))?;\n\n\t\tfile.write(b\"line 2\\n\")?;\n\n\t\tstage_add_file(repo_path, file_path)?;\n\t\tcommit(repo_path, \"second commit\")?;\n\n\t\tlet blame = blame_file(repo_path, \"foo\", None)?;\n\n\t\tassert!(matches!(\n\t\t\tblame.lines.as_slice(),\n\t\t\t[\n\t\t\t\t(\n\t\t\t\t\tSome(BlameHunk {\n\t\t\t\t\t\tstart_line: 0,\n\t\t\t\t\t\tend_line: 1,\n\t\t\t\t\t\t..\n\t\t\t\t\t}),\n\t\t\t\t\tfirst_line\n\t\t\t\t),\n\t\t\t\t(\n\t\t\t\t\tSome(BlameHunk {\n\t\t\t\t\t\tauthor,\n\t\t\t\t\t\tstart_line: 1,\n\t\t\t\t\t\tend_line: 2,\n\t\t\t\t\t\t..\n\t\t\t\t\t}),\n\t\t\t\t\tsecond_line\n\t\t\t\t)\n\t\t\t] if author == \"name\" && first_line == \"line 1\" && second_line == \"line 2\"\n\t\t));\n\n\t\tfile.write(b\"line 3\\n\")?;\n\n\t\tlet blame = blame_file(repo_path, \"foo\", None)?;\n\n\t\tassert_eq!(blame.lines.len(), 2);\n\n\t\tstage_add_file(repo_path, file_path)?;\n\t\tcommit(repo_path, \"third commit\")?;\n\n\t\tlet blame = blame_file(repo_path, \"foo\", None)?;\n\n\t\tassert_eq!(blame.lines.len(), 3);\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_blame_windows_path_dividers() {\n\t\tlet file_path = Path::new(\"bar\\\\foo\");\n\t\tlet (_td, repo) = repo_init_empty().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tstd::fs::create_dir(root.join(\"bar\")).unwrap();\n\n\t\tFile::create(root.join(file_path))\n\t\t\t.unwrap()\n\t\t\t.write_all(b\"line 1\\n\")\n\t\t\t.unwrap();\n\n\t\tstage_add_file(repo_path, file_path).unwrap();\n\t\tcommit(repo_path, \"first commit\").unwrap();\n\n\t\tassert!(blame_file(repo_path, \"bar\\\\foo\", None).is_ok());\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/branch/merge_commit.rs",
    "content": "//! merging from upstream\n\nuse super::BranchType;\nuse crate::{\n\terror::{Error, Result},\n\tsync::{merge_msg, repository::repo, CommitId, RepoPath},\n};\nuse git2::Commit;\nuse scopetime::scope_time;\n\n/// merge upstream using a merge commit if we did not create conflicts.\n/// if we did not create conflicts we create a merge commit and return the commit id.\n/// Otherwise we return `None`\npub fn merge_upstream_commit(\n\trepo_path: &RepoPath,\n\tbranch_name: &str,\n) -> Result<Option<CommitId>> {\n\tscope_time!(\"merge_upstream_commit\");\n\n\tlet repo = repo(repo_path)?;\n\n\tlet branch = repo.find_branch(branch_name, BranchType::Local)?;\n\tlet upstream = branch.upstream()?;\n\n\tlet upstream_commit = upstream.get().peel_to_commit()?;\n\n\tlet annotated_upstream = repo\n\t\t.reference_to_annotated_commit(&upstream.into_reference())?;\n\n\tlet (analysis, pref) =\n\t\trepo.merge_analysis(&[&annotated_upstream])?;\n\n\tif !analysis.is_normal() {\n\t\treturn Err(Error::Generic(\n\t\t\t\"normal merge not possible\".into(),\n\t\t));\n\t}\n\n\tif analysis.is_fast_forward() && pref.is_fastforward_only() {\n\t\treturn Err(Error::Generic(\n\t\t\t\"ff merge would be possible\".into(),\n\t\t));\n\t}\n\n\t//TODO: support merge on unborn?\n\tif analysis.is_unborn() {\n\t\treturn Err(Error::Generic(\"head is unborn\".into()));\n\t}\n\n\trepo.merge(&[&annotated_upstream], None, None)?;\n\n\tif !repo.index()?.has_conflicts() {\n\t\tlet msg = merge_msg(repo_path)?;\n\n\t\tlet commit_id =\n\t\t\tcommit_merge_with_head(&repo, &[upstream_commit], &msg)?;\n\n\t\treturn Ok(Some(commit_id));\n\t}\n\n\tOk(None)\n}\n\npub(crate) fn commit_merge_with_head(\n\trepo: &git2::Repository,\n\tcommits: &[Commit],\n\tmsg: &str,\n) -> Result<CommitId> {\n\tlet signature =\n\t\tcrate::sync::commit::signature_allow_undefined_name(repo)?;\n\tlet mut index = repo.index()?;\n\tlet tree_id = index.write_tree()?;\n\tlet tree = repo.find_tree(tree_id)?;\n\tlet head_commit = repo.find_commit(\n\t\tcrate::sync::utils::get_head_repo(repo)?.into(),\n\t)?;\n\n\tlet mut parents = vec![&head_commit];\n\tparents.extend(commits);\n\n\tlet commit_id = repo\n\t\t.commit(\n\t\t\tSome(\"HEAD\"),\n\t\t\t&signature,\n\t\t\t&signature,\n\t\t\tmsg,\n\t\t\t&tree,\n\t\t\tparents.as_slice(),\n\t\t)?\n\t\t.into();\n\trepo.cleanup_state()?;\n\tOk(commit_id)\n}\n\n#[cfg(test)]\nmod test {\n\tuse git2::Time;\n\n\tuse super::*;\n\tuse crate::sync::{\n\t\tbranch_compare_upstream,\n\t\tremotes::{fetch, push::push_branch},\n\t\ttests::{\n\t\t\tdebug_cmd_print, get_commit_ids, repo_clone,\n\t\t\trepo_init_bare, write_commit_file, write_commit_file_at,\n\t\t},\n\t\tRepoState,\n\t};\n\n\t#[test]\n\tfn test_merge_normal() {\n\t\tlet (r1_dir, _repo) = repo_init_bare().unwrap();\n\n\t\tlet (clone1_dir, clone1) =\n\t\t\trepo_clone(r1_dir.path().to_str().unwrap()).unwrap();\n\n\t\tlet (clone2_dir, clone2) =\n\t\t\trepo_clone(r1_dir.path().to_str().unwrap()).unwrap();\n\n\t\tlet clone2_dir = clone2_dir.path().to_str().unwrap();\n\n\t\t// clone1\n\n\t\tlet commit1 = write_commit_file_at(\n\t\t\t&clone1,\n\t\t\t\"test.txt\",\n\t\t\t\"test\",\n\t\t\t\"commit1\",\n\t\t\tTime::new(1, 0),\n\t\t);\n\n\t\tpush_branch(\n\t\t\t&clone1_dir.path().to_str().unwrap().into(),\n\t\t\t\"origin\",\n\t\t\t\"master\",\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\n\t\t// clone2\n\n\t\tlet commit2 = write_commit_file_at(\n\t\t\t&clone2,\n\t\t\t\"test2.txt\",\n\t\t\t\"test\",\n\t\t\t\"commit2\",\n\t\t\tTime::new(2, 0),\n\t\t);\n\n\t\t//push should fail since origin diverged\n\t\tassert!(push_branch(\n\t\t\t&clone2_dir.into(),\n\t\t\t\"origin\",\n\t\t\t\"master\",\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.is_err());\n\n\t\t//lets fetch from origin\n\t\tlet bytes =\n\t\t\tfetch(&clone2_dir.into(), \"master\", None, None).unwrap();\n\t\tassert!(bytes > 0);\n\n\t\t//we should be one commit behind\n\t\tassert_eq!(\n\t\t\tbranch_compare_upstream(&clone2_dir.into(), \"master\")\n\t\t\t\t.unwrap()\n\t\t\t\t.behind,\n\t\t\t1\n\t\t);\n\n\t\tlet merge_commit =\n\t\t\tmerge_upstream_commit(&clone2_dir.into(), \"master\")\n\t\t\t\t.unwrap()\n\t\t\t\t.unwrap();\n\n\t\tlet state =\n\t\t\tcrate::sync::repo_state(&clone2_dir.into()).unwrap();\n\t\tassert_eq!(state, RepoState::Clean);\n\n\t\tassert!(!clone2.head_detached().unwrap());\n\n\t\tlet commits = get_commit_ids(&clone2, 10);\n\t\tassert_eq!(commits.len(), 3);\n\t\tassert_eq!(commits[0], merge_commit);\n\t\tassert_eq!(commits[1], commit2);\n\t\tassert_eq!(commits[2], commit1);\n\n\t\t//verify commit msg\n\t\tlet details = crate::sync::get_commit_details(\n\t\t\t&clone2_dir.into(),\n\t\t\tmerge_commit,\n\t\t)\n\t\t.unwrap();\n\t\tassert_eq!(\n            details.message.unwrap().combine(),\n            String::from(\"Merge remote-tracking branch 'refs/remotes/origin/master'\")\n        );\n\t}\n\n\t#[test]\n\tfn test_merge_normal_non_ff() {\n\t\tlet (r1_dir, _repo) = repo_init_bare().unwrap();\n\n\t\tlet (clone1_dir, clone1) =\n\t\t\trepo_clone(r1_dir.path().to_str().unwrap()).unwrap();\n\n\t\tlet (clone2_dir, clone2) =\n\t\t\trepo_clone(r1_dir.path().to_str().unwrap()).unwrap();\n\n\t\t// clone1\n\n\t\twrite_commit_file(\n\t\t\t&clone1,\n\t\t\t\"test.bin\",\n\t\t\t\"test\\nfooo\",\n\t\t\t\"commit1\",\n\t\t);\n\n\t\tdebug_cmd_print(\n\t\t\t&clone2_dir.path().to_str().unwrap().into(),\n\t\t\t\"git status\",\n\t\t);\n\n\t\tpush_branch(\n\t\t\t&clone1_dir.path().to_str().unwrap().into(),\n\t\t\t\"origin\",\n\t\t\t\"master\",\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\n\t\t// clone2\n\n\t\twrite_commit_file(\n\t\t\t&clone2,\n\t\t\t\"test.bin\",\n\t\t\t\"foobar\\ntest\",\n\t\t\t\"commit2\",\n\t\t);\n\n\t\tlet bytes = fetch(\n\t\t\t&clone2_dir.path().to_str().unwrap().into(),\n\t\t\t\"master\",\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\t\tassert!(bytes > 0);\n\n\t\tlet res = merge_upstream_commit(\n\t\t\t&clone2_dir.path().to_str().unwrap().into(),\n\t\t\t\"master\",\n\t\t)\n\t\t.unwrap();\n\n\t\t//this should not have committed cause we left conflicts behind\n\t\tassert_eq!(res, None);\n\n\t\tlet state = crate::sync::repo_state(\n\t\t\t&clone2_dir.path().to_str().unwrap().into(),\n\t\t)\n\t\t.unwrap();\n\n\t\t//validate the repo is in a merge state now\n\t\tassert_eq!(state, RepoState::Merge);\n\n\t\t//check that we still only have the first commit\n\t\tlet commits = get_commit_ids(&clone1, 10);\n\t\tassert_eq!(commits.len(), 1);\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/branch/merge_ff.rs",
    "content": "//! merging from upstream\n\nuse super::BranchType;\nuse crate::{\n\terror::{Error, Result},\n\tsync::{repository::repo, RepoPath},\n};\nuse scopetime::scope_time;\n\n///\npub fn branch_merge_upstream_fastforward(\n\trepo_path: &RepoPath,\n\tbranch: &str,\n) -> Result<()> {\n\tscope_time!(\"branch_merge_upstream\");\n\n\tlet repo = repo(repo_path)?;\n\n\tlet branch = repo.find_branch(branch, BranchType::Local)?;\n\tlet upstream = branch.upstream()?;\n\n\tlet upstream_commit =\n\t\tupstream.into_reference().peel_to_commit()?;\n\n\tlet annotated =\n\t\trepo.find_annotated_commit(upstream_commit.id())?;\n\n\tlet (analysis, pref) = repo.merge_analysis(&[&annotated])?;\n\n\tif !analysis.is_fast_forward() {\n\t\treturn Err(Error::Generic(\n\t\t\t\"fast forward merge not possible\".into(),\n\t\t));\n\t}\n\n\tif pref.is_no_fast_forward() {\n\t\treturn Err(Error::Generic(\"fast forward not wanted\".into()));\n\t}\n\n\t//TODO: support merge on unborn\n\tif analysis.is_unborn() {\n\t\treturn Err(Error::Generic(\"head is unborn\".into()));\n\t}\n\n\trepo.checkout_tree(upstream_commit.as_object(), None)?;\n\n\trepo.head()?.set_target(annotated.id(), \"\")?;\n\n\tOk(())\n}\n\n#[cfg(test)]\nmod test {\n\tuse super::*;\n\tuse crate::sync::{\n\t\tremotes::{fetch, push::push_branch},\n\t\ttests::{\n\t\t\tdebug_cmd_print, get_commit_ids, repo_clone,\n\t\t\trepo_init_bare, write_commit_file,\n\t\t},\n\t};\n\n\t#[test]\n\tfn test_merge_fastforward() {\n\t\tlet (r1_dir, _repo) = repo_init_bare().unwrap();\n\n\t\tlet (clone1_dir, clone1) =\n\t\t\trepo_clone(r1_dir.path().to_str().unwrap()).unwrap();\n\n\t\tlet (clone2_dir, clone2) =\n\t\t\trepo_clone(r1_dir.path().to_str().unwrap()).unwrap();\n\n\t\t// clone1\n\n\t\tlet commit1 =\n\t\t\twrite_commit_file(&clone1, \"test.txt\", \"test\", \"commit1\");\n\n\t\tpush_branch(\n\t\t\t&clone1_dir.path().to_str().unwrap().into(),\n\t\t\t\"origin\",\n\t\t\t\"master\",\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\n\t\t// clone2\n\t\tdebug_cmd_print(\n\t\t\t&clone2_dir.path().to_str().unwrap().into(),\n\t\t\t\"git pull --ff\",\n\t\t);\n\n\t\tlet commit2 = write_commit_file(\n\t\t\t&clone2,\n\t\t\t\"test2.txt\",\n\t\t\t\"test\",\n\t\t\t\"commit2\",\n\t\t);\n\n\t\tpush_branch(\n\t\t\t&clone2_dir.path().to_str().unwrap().into(),\n\t\t\t\"origin\",\n\t\t\t\"master\",\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\n\t\t// clone1 again\n\n\t\tlet bytes = fetch(\n\t\t\t&clone1_dir.path().to_str().unwrap().into(),\n\t\t\t\"master\",\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\t\tassert!(bytes > 0);\n\n\t\tlet bytes = fetch(\n\t\t\t&clone1_dir.path().to_str().unwrap().into(),\n\t\t\t\"master\",\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\t\tassert_eq!(bytes, 0);\n\n\t\tbranch_merge_upstream_fastforward(\n\t\t\t&clone1_dir.path().to_str().unwrap().into(),\n\t\t\t\"master\",\n\t\t)\n\t\t.unwrap();\n\n\t\tlet commits = get_commit_ids(&clone1, 10);\n\t\tassert_eq!(commits.len(), 2);\n\t\tassert_eq!(commits[1], commit1);\n\t\tassert_eq!(commits[0], commit2);\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/branch/merge_rebase.rs",
    "content": "//! merging from upstream (rebase)\n\nuse crate::{\n\terror::{Error, Result},\n\tsync::{\n\t\trebase::conflict_free_rebase, repository::repo, CommitId,\n\t\tRepoPath,\n\t},\n};\nuse git2::BranchType;\nuse scopetime::scope_time;\n\n/// tries merging current branch with its upstream using rebase\npub fn merge_upstream_rebase(\n\trepo_path: &RepoPath,\n\tbranch_name: &str,\n) -> Result<CommitId> {\n\tscope_time!(\"merge_upstream_rebase\");\n\n\tlet repo = repo(repo_path)?;\n\tif super::get_branch_name_repo(&repo)? != branch_name {\n\t\treturn Err(Error::Generic(String::from(\n\t\t\t\"can only rebase in head branch\",\n\t\t)));\n\t}\n\n\tlet branch = repo.find_branch(branch_name, BranchType::Local)?;\n\tlet upstream = branch.upstream()?;\n\tlet upstream_commit = upstream.get().peel_to_commit()?;\n\tlet annotated_upstream =\n\t\trepo.find_annotated_commit(upstream_commit.id())?;\n\n\tconflict_free_rebase(&repo, &annotated_upstream)\n}\n\n#[cfg(test)]\nmod test {\n\tuse super::*;\n\tuse crate::sync::{\n\t\tbranch_compare_upstream, get_commits_info,\n\t\tremotes::{fetch, push::push_branch},\n\t\ttests::{\n\t\t\tdebug_cmd_print, get_commit_ids, repo_clone,\n\t\t\trepo_init_bare, write_commit_file, write_commit_file_at,\n\t\t},\n\t\tRepoState,\n\t};\n\tuse git2::{Repository, Time};\n\n\tfn get_commit_msgs(r: &Repository) -> Vec<String> {\n\t\tlet commits = get_commit_ids(r, 10);\n\t\tget_commits_info(\n\t\t\t&r.workdir().unwrap().to_str().unwrap().into(),\n\t\t\t&commits,\n\t\t\t10,\n\t\t)\n\t\t.unwrap()\n\t\t.into_iter()\n\t\t.map(|c| c.message)\n\t\t.collect()\n\t}\n\n\t#[test]\n\tfn test_merge_normal() {\n\t\tlet (r1_dir, _repo) = repo_init_bare().unwrap();\n\n\t\tlet (clone1_dir, clone1) =\n\t\t\trepo_clone(r1_dir.path().to_str().unwrap()).unwrap();\n\n\t\tlet clone1_dir = clone1_dir.path().to_str().unwrap();\n\n\t\t// clone1\n\n\t\tlet _commit1 = write_commit_file_at(\n\t\t\t&clone1,\n\t\t\t\"test.txt\",\n\t\t\t\"test\",\n\t\t\t\"commit1\",\n\t\t\tgit2::Time::new(0, 0),\n\t\t);\n\n\t\tassert!(!clone1.head_detached().unwrap());\n\n\t\tpush_branch(\n\t\t\t&clone1_dir.into(),\n\t\t\t\"origin\",\n\t\t\t\"master\",\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\n\t\tassert!(!clone1.head_detached().unwrap());\n\n\t\t// clone2\n\n\t\tlet (clone2_dir, clone2) =\n\t\t\trepo_clone(r1_dir.path().to_str().unwrap()).unwrap();\n\n\t\tlet clone2_dir = clone2_dir.path().to_str().unwrap();\n\n\t\tlet _commit2 = write_commit_file_at(\n\t\t\t&clone2,\n\t\t\t\"test2.txt\",\n\t\t\t\"test\",\n\t\t\t\"commit2\",\n\t\t\tgit2::Time::new(1, 0),\n\t\t);\n\n\t\tassert!(!clone2.head_detached().unwrap());\n\n\t\tpush_branch(\n\t\t\t&clone2_dir.into(),\n\t\t\t\"origin\",\n\t\t\t\"master\",\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\n\t\tassert!(!clone2.head_detached().unwrap());\n\n\t\t// clone1\n\n\t\tlet _commit3 = write_commit_file_at(\n\t\t\t&clone1,\n\t\t\t\"test3.txt\",\n\t\t\t\"test\",\n\t\t\t\"commit3\",\n\t\t\tgit2::Time::new(2, 0),\n\t\t);\n\n\t\tassert!(!clone1.head_detached().unwrap());\n\n\t\t//lets fetch from origin\n\t\tlet bytes =\n\t\t\tfetch(&clone1_dir.into(), \"master\", None, None).unwrap();\n\t\tassert!(bytes > 0);\n\n\t\t//we should be one commit behind\n\t\tassert_eq!(\n\t\t\tbranch_compare_upstream(&clone1_dir.into(), \"master\")\n\t\t\t\t.unwrap()\n\t\t\t\t.behind,\n\t\t\t1\n\t\t);\n\n\t\t// debug_cmd_print(clone1_dir, \"git status\");\n\n\t\tassert!(!clone1.head_detached().unwrap());\n\n\t\tmerge_upstream_rebase(&clone1_dir.into(), \"master\").unwrap();\n\n\t\tdebug_cmd_print(&clone1_dir.into(), \"git log\");\n\n\t\tlet state =\n\t\t\tcrate::sync::repo_state(&clone1_dir.into()).unwrap();\n\t\tassert_eq!(state, RepoState::Clean);\n\n\t\tlet commits = get_commit_msgs(&clone1);\n\t\tassert_eq!(\n\t\t\tcommits,\n\t\t\tvec![\n\t\t\t\tString::from(\"commit3\"),\n\t\t\t\tString::from(\"commit2\"),\n\t\t\t\tString::from(\"commit1\")\n\t\t\t]\n\t\t);\n\n\t\tassert!(!clone1.head_detached().unwrap());\n\t}\n\n\t#[test]\n\tfn test_merge_multiple() {\n\t\tlet (r1_dir, _repo) = repo_init_bare().unwrap();\n\n\t\tlet (clone1_dir, clone1) =\n\t\t\trepo_clone(r1_dir.path().to_str().unwrap()).unwrap();\n\n\t\tlet clone1_dir = clone1_dir.path().to_str().unwrap();\n\n\t\t// clone1\n\n\t\twrite_commit_file_at(\n\t\t\t&clone1,\n\t\t\t\"test.txt\",\n\t\t\t\"test\",\n\t\t\t\"commit1\",\n\t\t\tTime::new(0, 0),\n\t\t);\n\n\t\tpush_branch(\n\t\t\t&clone1_dir.into(),\n\t\t\t\"origin\",\n\t\t\t\"master\",\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\n\t\t// clone2\n\n\t\tlet (clone2_dir, clone2) =\n\t\t\trepo_clone(r1_dir.path().to_str().unwrap()).unwrap();\n\n\t\tlet clone2_dir = clone2_dir.path().to_str().unwrap();\n\n\t\twrite_commit_file_at(\n\t\t\t&clone2,\n\t\t\t\"test2.txt\",\n\t\t\t\"test\",\n\t\t\t\"commit2\",\n\t\t\tTime::new(1, 0),\n\t\t);\n\n\t\tpush_branch(\n\t\t\t&clone2_dir.into(),\n\t\t\t\"origin\",\n\t\t\t\"master\",\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\n\t\t// clone1\n\n\t\twrite_commit_file_at(\n\t\t\t&clone1,\n\t\t\t\"test3.txt\",\n\t\t\t\"test\",\n\t\t\t\"commit3\",\n\t\t\tTime::new(2, 0),\n\t\t);\n\t\twrite_commit_file_at(\n\t\t\t&clone1,\n\t\t\t\"test4.txt\",\n\t\t\t\"test\",\n\t\t\t\"commit4\",\n\t\t\tTime::new(3, 0),\n\t\t);\n\n\t\t//lets fetch from origin\n\n\t\tfetch(&clone1_dir.into(), \"master\", None, None).unwrap();\n\n\t\tmerge_upstream_rebase(&clone1_dir.into(), \"master\").unwrap();\n\n\t\tdebug_cmd_print(&clone1_dir.into(), \"git log\");\n\n\t\tlet state =\n\t\t\tcrate::sync::repo_state(&clone1_dir.into()).unwrap();\n\t\tassert_eq!(state, RepoState::Clean);\n\n\t\tlet commits = get_commit_msgs(&clone1);\n\t\tassert_eq!(\n\t\t\tcommits,\n\t\t\tvec![\n\t\t\t\tString::from(\"commit4\"),\n\t\t\t\tString::from(\"commit3\"),\n\t\t\t\tString::from(\"commit2\"),\n\t\t\t\tString::from(\"commit1\")\n\t\t\t]\n\t\t);\n\n\t\tassert!(!clone1.head_detached().unwrap());\n\t}\n\n\t#[test]\n\tfn test_merge_conflict() {\n\t\tlet (r1_dir, _repo) = repo_init_bare().unwrap();\n\n\t\tlet (clone1_dir, clone1) =\n\t\t\trepo_clone(r1_dir.path().to_str().unwrap()).unwrap();\n\n\t\tlet clone1_dir = clone1_dir.path().to_str().unwrap();\n\n\t\t// clone1\n\n\t\tlet _commit1 =\n\t\t\twrite_commit_file(&clone1, \"test.txt\", \"test\", \"commit1\");\n\n\t\tpush_branch(\n\t\t\t&clone1_dir.into(),\n\t\t\t\"origin\",\n\t\t\t\"master\",\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\n\t\t// clone2\n\n\t\tlet (clone2_dir, clone2) =\n\t\t\trepo_clone(r1_dir.path().to_str().unwrap()).unwrap();\n\n\t\tlet clone2_dir = clone2_dir.path().to_str().unwrap();\n\n\t\tlet _commit2 = write_commit_file(\n\t\t\t&clone2,\n\t\t\t\"test2.txt\",\n\t\t\t\"test\",\n\t\t\t\"commit2\",\n\t\t);\n\n\t\tpush_branch(\n\t\t\t&clone2_dir.into(),\n\t\t\t\"origin\",\n\t\t\t\"master\",\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\n\t\t// clone1\n\n\t\tlet _commit3 =\n\t\t\twrite_commit_file(&clone1, \"test2.txt\", \"foo\", \"commit3\");\n\n\t\tlet bytes =\n\t\t\tfetch(&clone1_dir.into(), \"master\", None, None).unwrap();\n\t\tassert!(bytes > 0);\n\n\t\tassert_eq!(\n\t\t\tbranch_compare_upstream(&clone1_dir.into(), \"master\")\n\t\t\t\t.unwrap()\n\t\t\t\t.behind,\n\t\t\t1\n\t\t);\n\n\t\tlet res = merge_upstream_rebase(&clone1_dir.into(), \"master\");\n\t\tassert!(res.is_err());\n\n\t\tlet state =\n\t\t\tcrate::sync::repo_state(&clone1_dir.into()).unwrap();\n\n\t\tassert_eq!(state, RepoState::Clean);\n\n\t\tlet commits = get_commit_msgs(&clone1);\n\t\tassert_eq!(\n\t\t\tcommits,\n\t\t\tvec![String::from(\"commit3\"), String::from(\"commit1\")]\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/branch/mod.rs",
    "content": "//! branch functions\n\npub mod merge_commit;\npub mod merge_ff;\npub mod merge_rebase;\npub mod rename;\n\nuse super::{utils::bytes2string, RepoPath};\nuse crate::{\n\terror::{Error, Result},\n\tsync::{\n\t\tremotes::get_default_remote_for_push_in_repo,\n\t\trepository::repo, utils::get_head_repo, CommitId,\n\t},\n};\nuse git2::{Branch, BranchType, Repository};\nuse scopetime::scope_time;\nuse std::collections::HashSet;\n\n/// returns the branch-name head is currently pointing to\n/// this might be expensive, see `cached::BranchName`\npub(crate) fn get_branch_name(\n\trepo_path: &RepoPath,\n) -> Result<String> {\n\tlet repo = repo(repo_path)?;\n\n\tget_branch_name_repo(&repo)\n}\n\n/// ditto\npub(crate) fn get_branch_name_repo(\n\trepo: &Repository,\n) -> Result<String> {\n\tscope_time!(\"get_branch_name_repo\");\n\n\tlet head_ref = repo.head().map_err(|e| {\n\t\tif e.code() == git2::ErrorCode::UnbornBranch {\n\t\t\tError::NoHead\n\t\t} else {\n\t\t\te.into()\n\t\t}\n\t})?;\n\n\tbytes2string(head_ref.shorthand_bytes())\n}\n\n///\n#[derive(Clone, Debug)]\npub struct LocalBranch {\n\t///\n\tpub is_head: bool,\n\t///\n\tpub has_upstream: bool,\n\t///\n\tpub upstream: Option<UpstreamBranch>,\n\t///\n\tpub remote: Option<String>,\n}\n\n///\n#[derive(Clone, Debug)]\npub struct UpstreamBranch {\n\t///\n\tpub reference: String,\n}\n\n///\n#[derive(Clone, Debug)]\npub struct RemoteBranch {\n\t///\n\tpub has_tracking: bool,\n}\n\n///\n#[derive(Clone, Debug)]\npub enum BranchDetails {\n\t///\n\tLocal(LocalBranch),\n\t///\n\tRemote(RemoteBranch),\n}\n\n///\n#[derive(Clone, Debug)]\npub struct BranchInfo {\n\t///\n\tpub name: String,\n\t///\n\tpub reference: String,\n\t///\n\tpub top_commit_message: String,\n\t///\n\tpub top_commit: CommitId,\n\t///\n\tpub details: BranchDetails,\n}\n\nimpl BranchInfo {\n\t/// returns details about local branch or None\n\tpub const fn local_details(&self) -> Option<&LocalBranch> {\n\t\tif let BranchDetails::Local(details) = &self.details {\n\t\t\treturn Some(details);\n\t\t}\n\n\t\tNone\n\t}\n\n\t/// returns whether branch is local\n\tpub const fn is_local(&self) -> bool {\n\t\tmatches!(self.details, BranchDetails::Local(_))\n\t}\n}\n\n///\npub fn validate_branch_name(name: &str) -> Result<bool> {\n\tscope_time!(\"validate_branch_name\");\n\n\tlet valid = Branch::name_is_valid(name)?;\n\n\tOk(valid)\n}\n\n/// returns a list of `BranchInfo` with a simple summary on each branch\n/// `local` filters for local branches otherwise remote branches will be returned\npub fn get_branches_info(\n\trepo_path: &RepoPath,\n\tlocal: bool,\n) -> Result<Vec<BranchInfo>> {\n\tscope_time!(\"get_branches_info\");\n\n\tlet repo = repo(repo_path)?;\n\n\tlet (filter, remotes_with_tracking) = if local {\n\t\t(BranchType::Local, HashSet::default())\n\t} else {\n\t\tlet remotes: HashSet<_> = repo\n\t\t\t.branches(Some(BranchType::Local))?\n\t\t\t.filter_map(|b| {\n\t\t\t\tlet branch = b.ok()?.0;\n\t\t\t\tlet upstream = branch.upstream();\n\t\t\t\tupstream\n\t\t\t\t\t.ok()?\n\t\t\t\t\t.name_bytes()\n\t\t\t\t\t.ok()\n\t\t\t\t\t.map(ToOwned::to_owned)\n\t\t\t})\n\t\t\t.collect();\n\t\t(BranchType::Remote, remotes)\n\t};\n\n\tlet mut branches_for_display: Vec<BranchInfo> = repo\n\t\t.branches(Some(filter))?\n\t\t.map(|b| {\n\t\t\tlet branch = b?.0;\n\t\t\tlet top_commit = branch.get().peel_to_commit()?;\n\t\t\tlet reference = bytes2string(branch.get().name_bytes())?;\n\t\t\tlet upstream = branch.upstream();\n\n\t\t\tlet remote = repo\n\t\t\t\t.branch_upstream_remote(&reference)\n\t\t\t\t.ok()\n\t\t\t\t.as_ref()\n\t\t\t\t.and_then(git2::Buf::as_str)\n\t\t\t\t.map(String::from);\n\n\t\t\tlet name_bytes = branch.name_bytes()?;\n\n\t\t\tlet upstream_branch =\n\t\t\t\tupstream.ok().and_then(|upstream| {\n\t\t\t\t\tbytes2string(upstream.get().name_bytes())\n\t\t\t\t\t\t.ok()\n\t\t\t\t\t\t.map(|reference| UpstreamBranch { reference })\n\t\t\t\t});\n\n\t\t\tlet details = if local {\n\t\t\t\tBranchDetails::Local(LocalBranch {\n\t\t\t\t\tis_head: branch.is_head(),\n\t\t\t\t\thas_upstream: upstream_branch.is_some(),\n\t\t\t\t\tupstream: upstream_branch,\n\t\t\t\t\tremote,\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\tBranchDetails::Remote(RemoteBranch {\n\t\t\t\t\thas_tracking: remotes_with_tracking\n\t\t\t\t\t\t.contains(name_bytes),\n\t\t\t\t})\n\t\t\t};\n\n\t\t\tOk(BranchInfo {\n\t\t\t\tname: bytes2string(name_bytes)?,\n\t\t\t\treference,\n\t\t\t\ttop_commit_message: bytes2string(\n\t\t\t\t\ttop_commit.summary_bytes().unwrap_or_default(),\n\t\t\t\t)?,\n\t\t\t\ttop_commit: top_commit.id().into(),\n\t\t\t\tdetails,\n\t\t\t})\n\t\t})\n\t\t.filter_map(Result::ok)\n\t\t.collect();\n\n\tbranches_for_display.sort_by(|a, b| a.name.cmp(&b.name));\n\n\tOk(branches_for_display)\n}\n\n///\n#[derive(Debug, Default)]\npub struct BranchCompare {\n\t///\n\tpub ahead: usize,\n\t///\n\tpub behind: usize,\n}\n\n///\npub(crate) fn branch_set_upstream_after_push(\n\trepo: &Repository,\n\tbranch_name: &str,\n) -> Result<()> {\n\tscope_time!(\"branch_set_upstream\");\n\n\tlet mut branch =\n\t\trepo.find_branch(branch_name, BranchType::Local)?;\n\n\tif branch.upstream().is_err() {\n\t\tlet remote = get_default_remote_for_push_in_repo(repo)?;\n\t\tlet upstream_name = format!(\"{remote}/{branch_name}\");\n\t\tbranch.set_upstream(Some(upstream_name.as_str()))?;\n\t}\n\n\tOk(())\n}\n\n/// returns remote of the upstream tracking branch for `branch`\npub fn get_branch_remote(\n\trepo_path: &RepoPath,\n\tbranch: &str,\n) -> Result<Option<String>> {\n\tlet repo = repo(repo_path)?;\n\tlet branch = repo.find_branch(branch, BranchType::Local)?;\n\tlet reference = bytes2string(branch.get().name_bytes())?;\n\tlet remote_name = repo.branch_upstream_remote(&reference).ok();\n\tif let Some(remote_name) = remote_name {\n\t\tOk(Some(bytes2string(remote_name.as_ref())?))\n\t} else {\n\t\tOk(None)\n\t}\n}\n\n/// Retrieve the upstream merge of a local `branch`,\n/// configured in \"branch.*.merge\"\n///\n/// For details check git2 `branch_upstream_merge`\npub fn get_branch_upstream_merge(\n\trepo_path: &RepoPath,\n\tbranch: &str,\n) -> Result<Option<String>> {\n\tlet repo = repo(repo_path)?;\n\tlet branch = repo.find_branch(branch, BranchType::Local)?;\n\tlet reference = bytes2string(branch.get().name_bytes())?;\n\tlet remote_name = repo.branch_upstream_merge(&reference).ok();\n\tif let Some(remote_name) = remote_name {\n\t\tOk(Some(bytes2string(remote_name.as_ref())?))\n\t} else {\n\t\tOk(None)\n\t}\n}\n\n/// returns whether the pull merge strategy is set to rebase\npub fn config_is_pull_rebase(repo_path: &RepoPath) -> Result<bool> {\n\tlet repo = repo(repo_path)?;\n\tlet config = repo.config()?;\n\n\tif let Ok(rebase) = config.get_entry(\"pull.rebase\") {\n\t\tlet value =\n\t\t\trebase.value().map(String::from).unwrap_or_default();\n\t\treturn Ok(value == \"true\");\n\t}\n\n\tOk(false)\n}\n\n///\npub fn branch_compare_upstream(\n\trepo_path: &RepoPath,\n\tbranch: &str,\n) -> Result<BranchCompare> {\n\tscope_time!(\"branch_compare_upstream\");\n\n\tlet repo = repo(repo_path)?;\n\n\tlet branch = repo.find_branch(branch, BranchType::Local)?;\n\n\tlet upstream = branch.upstream()?;\n\n\tlet branch_commit =\n\t\tbranch.into_reference().peel_to_commit()?.id();\n\n\tlet upstream_commit =\n\t\tupstream.into_reference().peel_to_commit()?.id();\n\n\tlet (ahead, behind) =\n\t\trepo.graph_ahead_behind(branch_commit, upstream_commit)?;\n\n\tOk(BranchCompare { ahead, behind })\n}\n\n/// Switch branch to given `branch_name`.\n///\n/// Method will fail if there are conflicting changes between current and target branch. However,\n/// if files are not conflicting, they will remain in tree (e.g. tracked new file is not\n/// conflicting and therefore is kept in tree even after checkout).\npub fn checkout_branch(\n\trepo_path: &RepoPath,\n\tbranch_name: &str,\n) -> Result<()> {\n\tscope_time!(\"checkout_branch\");\n\n\tlet repo = repo(repo_path)?;\n\n\tlet branch = repo.find_branch(branch_name, BranchType::Local)?;\n\n\tlet branch_ref = branch.into_reference();\n\n\tlet target_treeish = branch_ref.peel_to_tree()?;\n\tlet target_treeish_object = target_treeish.as_object();\n\n\t// modify state to match branch's state\n\trepo.checkout_tree(\n\t\ttarget_treeish_object,\n\t\tSome(&mut git2::build::CheckoutBuilder::new()),\n\t)?;\n\n\tlet branch_ref = branch_ref.name().ok_or_else(|| {\n\t\tError::Generic(String::from(\"branch ref not found\"))\n\t});\n\n\t// modify HEAD to point to given branch\n\trepo.set_head(branch_ref?)?;\n\n\tOk(())\n}\n\n/// Detach HEAD to point to a commit then checkout HEAD, does not work if there are uncommitted changes\npub fn checkout_commit(\n\trepo_path: &RepoPath,\n\tcommit_hash: CommitId,\n) -> Result<()> {\n\tscope_time!(\"checkout_commit\");\n\n\tlet repo = repo(repo_path)?;\n\tlet cur_ref = repo.head()?;\n\tlet statuses = repo.statuses(Some(\n\t\tgit2::StatusOptions::new().include_ignored(false),\n\t))?;\n\n\tif statuses.is_empty() {\n\t\trepo.set_head_detached(commit_hash.into())?;\n\n\t\tif let Err(e) = repo.checkout_head(Some(\n\t\t\tgit2::build::CheckoutBuilder::new().force(),\n\t\t)) {\n\t\t\trepo.set_head(\n\t\t\t\tbytes2string(cur_ref.name_bytes())?.as_str(),\n\t\t\t)?;\n\t\t\treturn Err(Error::Git(e));\n\t\t}\n\t\tOk(())\n\t} else {\n\t\tErr(Error::UncommittedChanges)\n\t}\n}\n\n///\npub fn checkout_remote_branch(\n\trepo_path: &RepoPath,\n\tbranch: &BranchInfo,\n) -> Result<()> {\n\tscope_time!(\"checkout_remote_branch\");\n\n\tlet repo = repo(repo_path)?;\n\tlet cur_ref = repo.head()?;\n\n\tif !repo\n\t\t.statuses(Some(\n\t\t\tgit2::StatusOptions::new().include_ignored(false),\n\t\t))?\n\t\t.is_empty()\n\t{\n\t\treturn Err(Error::UncommittedChanges);\n\t}\n\n\tlet name = branch.name.find('/').map_or_else(\n\t\t|| branch.name.clone(),\n\t\t|pos| branch.name[pos..].to_string(),\n\t);\n\n\tlet commit = repo.find_commit(branch.top_commit.into())?;\n\tlet mut new_branch = repo.branch(&name, &commit, false)?;\n\tnew_branch.set_upstream(Some(&branch.name))?;\n\n\trepo.set_head(\n\t\tbytes2string(new_branch.into_reference().name_bytes())?\n\t\t\t.as_str(),\n\t)?;\n\n\tif let Err(e) = repo.checkout_head(Some(\n\t\tgit2::build::CheckoutBuilder::new().force(),\n\t)) {\n\t\t// This is safe because cur_ref was just found\n\t\trepo.set_head(bytes2string(cur_ref.name_bytes())?.as_str())?;\n\t\treturn Err(Error::Git(e));\n\t}\n\tOk(())\n}\n\n/// The user must not be on the branch for the branch to be deleted\npub fn delete_branch(\n\trepo_path: &RepoPath,\n\tbranch_ref: &str,\n) -> Result<()> {\n\tscope_time!(\"delete_branch\");\n\n\tlet repo = repo(repo_path)?;\n\tlet branch_as_ref = repo.find_reference(branch_ref)?;\n\tlet mut branch = git2::Branch::wrap(branch_as_ref);\n\tif branch.is_head() {\n\t\treturn Err(Error::Generic(\"You cannot be on the branch you want to delete, switch branch, then delete this branch\".to_string()));\n\t}\n\tbranch.delete()?;\n\tOk(())\n}\n\n/// creates a new branch pointing to current HEAD commit and updating HEAD to new branch\npub fn create_branch(\n\trepo_path: &RepoPath,\n\tname: &str,\n) -> Result<String> {\n\tscope_time!(\"create_branch\");\n\n\tlet repo = repo(repo_path)?;\n\n\tlet head_id = get_head_repo(&repo)?;\n\tlet head_commit = repo.find_commit(head_id.into())?;\n\n\tlet branch = repo.branch(name, &head_commit, false)?;\n\tlet branch_ref = branch.into_reference();\n\tlet branch_ref_name = bytes2string(branch_ref.name_bytes())?;\n\trepo.set_head(branch_ref_name.as_str())?;\n\n\tOk(branch_ref_name)\n}\n\n#[cfg(test)]\nmod tests_branch_name {\n\tuse super::*;\n\tuse crate::sync::tests::{repo_init, repo_init_empty};\n\n\t#[test]\n\tfn test_smoke() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tassert_eq!(\n\t\t\tget_branch_name(repo_path).unwrap().as_str(),\n\t\t\t\"master\"\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_empty_repo() {\n\t\tlet (_td, repo) = repo_init_empty().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tassert!(matches!(\n\t\t\tget_branch_name(repo_path),\n\t\t\tErr(Error::NoHead)\n\t\t));\n\t}\n}\n\n#[cfg(test)]\nmod tests_create_branch {\n\tuse super::*;\n\tuse crate::sync::tests::repo_init;\n\n\t#[test]\n\tfn test_smoke() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tcreate_branch(repo_path, \"branch1\").unwrap();\n\n\t\tassert_eq!(\n\t\t\tget_branch_name(repo_path).unwrap().as_str(),\n\t\t\t\"branch1\"\n\t\t);\n\t}\n}\n\n#[cfg(test)]\nmod tests_branch_compare {\n\tuse super::*;\n\tuse crate::sync::tests::repo_init;\n\n\t#[test]\n\tfn test_smoke() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tcreate_branch(repo_path, \"test\").unwrap();\n\n\t\tlet res = branch_compare_upstream(repo_path, \"test\");\n\n\t\tassert!(res.is_err());\n\t}\n}\n\n#[cfg(test)]\nmod tests_branches {\n\tuse super::*;\n\tuse crate::sync::{\n\t\tremotes::{get_remotes, push::push_branch},\n\t\trename_branch,\n\t\ttests::{\n\t\t\tdebug_cmd_print, repo_clone, repo_init, repo_init_bare,\n\t\t\twrite_commit_file,\n\t\t},\n\t};\n\n\t#[test]\n\tfn test_smoke() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tassert_eq!(\n\t\t\tget_branches_info(repo_path, true)\n\t\t\t\t.unwrap()\n\t\t\t\t.iter()\n\t\t\t\t.map(|b| b.name.clone())\n\t\t\t\t.collect::<Vec<_>>(),\n\t\t\tvec![\"master\"]\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_multiple() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tcreate_branch(repo_path, \"test\").unwrap();\n\n\t\tassert_eq!(\n\t\t\tget_branches_info(repo_path, true)\n\t\t\t\t.unwrap()\n\t\t\t\t.iter()\n\t\t\t\t.map(|b| b.name.clone())\n\t\t\t\t.collect::<Vec<_>>(),\n\t\t\tvec![\"master\", \"test\"]\n\t\t);\n\t}\n\n\tfn clone_branch_commit_push(target: &str, branch_name: &str) {\n\t\tlet (dir, repo) = repo_clone(target).unwrap();\n\t\tlet dir = dir.path().to_str().unwrap();\n\n\t\twrite_commit_file(&repo, \"f1.txt\", \"foo\", \"c1\");\n\t\trename_branch(&dir.into(), \"refs/heads/master\", branch_name)\n\t\t\t.unwrap();\n\t\tpush_branch(\n\t\t\t&dir.into(),\n\t\t\t\"origin\",\n\t\t\tbranch_name,\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\t}\n\n\t#[test]\n\tfn test_remotes_of_branches() {\n\t\tlet (r1_path, _remote1) = repo_init_bare().unwrap();\n\t\tlet (r2_path, _remote2) = repo_init_bare().unwrap();\n\t\tlet (_r, repo) = repo_init().unwrap();\n\n\t\tlet r1_path = r1_path.path().to_str().unwrap();\n\t\tlet r2_path = r2_path.path().to_str().unwrap();\n\n\t\t//Note: create those test branches in our remotes\n\t\tclone_branch_commit_push(r1_path, \"r1branch\");\n\t\tclone_branch_commit_push(r2_path, \"r2branch\");\n\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\t//add the remotes\n\t\trepo.remote(\"r1\", r1_path).unwrap();\n\t\trepo.remote(\"r2\", r2_path).unwrap();\n\n\t\t//verify we got the remotes\n\t\tlet remotes = get_remotes(repo_path).unwrap();\n\t\tassert_eq!(\n\t\t\tremotes,\n\t\t\tvec![String::from(\"r1\"), String::from(\"r2\")]\n\t\t);\n\n\t\t//verify we got only master right now\n\t\tlet branches = get_branches_info(repo_path, true).unwrap();\n\t\tassert_eq!(branches.len(), 1);\n\t\tassert_eq!(branches[0].name, String::from(\"master\"));\n\n\t\t//pull stuff from the two remotes\n\t\tdebug_cmd_print(repo_path, \"git pull r1\");\n\t\tdebug_cmd_print(repo_path, \"git pull r2\");\n\n\t\t//create local tracking branches\n\t\tdebug_cmd_print(\n\t\t\trepo_path,\n\t\t\t\"git checkout --track r1/r1branch\",\n\t\t);\n\t\tdebug_cmd_print(\n\t\t\trepo_path,\n\t\t\t\"git checkout --track r2/r2branch\",\n\t\t);\n\n\t\tlet branches = get_branches_info(repo_path, true).unwrap();\n\t\tassert_eq!(branches.len(), 3);\n\t\tassert_eq!(\n\t\t\tbranches[1]\n\t\t\t\t.local_details()\n\t\t\t\t.unwrap()\n\t\t\t\t.remote\n\t\t\t\t.as_ref()\n\t\t\t\t.unwrap(),\n\t\t\t\"r1\"\n\t\t);\n\t\tassert_eq!(\n\t\t\tbranches[2]\n\t\t\t\t.local_details()\n\t\t\t\t.unwrap()\n\t\t\t\t.remote\n\t\t\t\t.as_ref()\n\t\t\t\t.unwrap(),\n\t\t\t\"r2\"\n\t\t);\n\n\t\tassert_eq!(\n\t\t\tget_branch_remote(repo_path, \"r1branch\")\n\t\t\t\t.unwrap()\n\t\t\t\t.unwrap(),\n\t\t\tString::from(\"r1\")\n\t\t);\n\n\t\tassert_eq!(\n\t\t\tget_branch_remote(repo_path, \"r2branch\")\n\t\t\t\t.unwrap()\n\t\t\t\t.unwrap(),\n\t\t\tString::from(\"r2\")\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_branch_remote_no_upstream() {\n\t\tlet (_r, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tassert_eq!(\n\t\t\tget_branch_remote(repo_path, \"master\").unwrap(),\n\t\t\tNone\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_branch_remote_no_branch() {\n\t\tlet (_r, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tassert!(get_branch_remote(repo_path, \"foo\").is_err());\n\t}\n\n\t#[test]\n\tfn test_branch_no_upstream_merge_config() {\n\t\tlet (_r, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tlet upstream_merge_res =\n\t\t\tget_branch_upstream_merge(repo_path, \"master\");\n\t\tassert!(\n\t\t\tupstream_merge_res.is_ok_and(|v| v.as_ref().is_none())\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_branch_with_upstream_merge_config() {\n\t\tlet (_r, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tlet branch_name = \"master\";\n\t\tlet upstream_merge = \"refs/heads/master\";\n\n\t\tlet mut config = repo.config().unwrap();\n\t\tconfig\n\t\t\t.set_str(\n\t\t\t\t&format!(\"branch.{branch_name}.merge\"),\n\t\t\t\tupstream_merge,\n\t\t\t)\n\t\t\t.expect(\"fail set branch merge config\");\n\n\t\tlet upstream_merge_res =\n\t\t\tget_branch_upstream_merge(repo_path, branch_name);\n\t\tassert!(upstream_merge_res\n\t\t\t.as_ref()\n\t\t\t.is_ok_and(|v| v.as_ref().is_some()));\n\t\tassert_eq!(\n\t\t\t&upstream_merge_res.unwrap().unwrap(),\n\t\t\tupstream_merge\n\t\t);\n\t}\n}\n\n#[cfg(test)]\nmod tests_checkout {\n\tuse super::*;\n\tuse crate::sync::{stage_add_file, tests::repo_init};\n\tuse std::{fs::File, path::Path};\n\n\t#[test]\n\tfn test_smoke() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tassert!(checkout_branch(repo_path, \"master\").is_ok());\n\t\tassert!(checkout_branch(repo_path, \"foobar\").is_err());\n\t}\n\n\t#[test]\n\tfn test_multiple() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tcreate_branch(repo_path, \"test\").unwrap();\n\n\t\tassert!(checkout_branch(repo_path, \"test\").is_ok());\n\t\tassert!(checkout_branch(repo_path, \"master\").is_ok());\n\t\tassert!(checkout_branch(repo_path, \"test\").is_ok());\n\t}\n\n\t#[test]\n\tfn test_branch_with_slash_in_name() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tcreate_branch(repo_path, \"foo/bar\").unwrap();\n\t\tcheckout_branch(repo_path, \"foo/bar\").unwrap();\n\t}\n\n\t#[test]\n\tfn test_staged_new_file() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tcreate_branch(repo_path, \"test\").unwrap();\n\n\t\tlet filename = \"file.txt\";\n\t\tlet file = root.join(filename);\n\t\tFile::create(&file).unwrap();\n\n\t\tstage_add_file(repo_path, Path::new(filename)).unwrap();\n\n\t\tassert!(checkout_branch(repo_path, \"test\").is_ok());\n\t}\n}\n\n#[cfg(test)]\nmod tests_checkout_commit {\n\tuse super::*;\n\tuse crate::sync::tests::{repo_init, write_commit_file};\n\tuse crate::sync::RepoPath;\n\n\t#[test]\n\tfn test_smoke() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tlet commit =\n\t\t\twrite_commit_file(&repo, \"test_1.txt\", \"test\", \"commit1\");\n\t\twrite_commit_file(&repo, \"test_2.txt\", \"test\", \"commit2\");\n\n\t\tcheckout_commit(repo_path, commit).unwrap();\n\n\t\tassert!(repo.head_detached().unwrap());\n\t\tassert_eq!(\n\t\t\trepo.head().unwrap().target().unwrap(),\n\t\t\tcommit.get_oid()\n\t\t);\n\t}\n}\n\n#[cfg(test)]\nmod test_delete_branch {\n\tuse super::*;\n\tuse crate::sync::tests::repo_init;\n\n\t#[test]\n\tfn test_delete_branch() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tcreate_branch(repo_path, \"branch1\").unwrap();\n\t\tcreate_branch(repo_path, \"branch2\").unwrap();\n\n\t\tcheckout_branch(repo_path, \"branch1\").unwrap();\n\n\t\tassert_eq!(\n\t\t\trepo.branches(None)\n\t\t\t\t.unwrap()\n\t\t\t\t.nth(1)\n\t\t\t\t.unwrap()\n\t\t\t\t.unwrap()\n\t\t\t\t.0\n\t\t\t\t.name()\n\t\t\t\t.unwrap()\n\t\t\t\t.unwrap(),\n\t\t\t\"branch2\"\n\t\t);\n\n\t\tdelete_branch(repo_path, \"refs/heads/branch2\").unwrap();\n\n\t\tassert_eq!(\n\t\t\trepo.branches(None)\n\t\t\t\t.unwrap()\n\t\t\t\t.nth(1)\n\t\t\t\t.unwrap()\n\t\t\t\t.unwrap()\n\t\t\t\t.0\n\t\t\t\t.name()\n\t\t\t\t.unwrap()\n\t\t\t\t.unwrap(),\n\t\t\t\"master\"\n\t\t);\n\t}\n}\n\n#[cfg(test)]\nmod test_remote_branches {\n\tuse super::*;\n\tuse crate::sync::remotes::push::push_branch;\n\tuse crate::sync::tests::{\n\t\trepo_clone, repo_init_bare, write_commit_file,\n\t};\n\n\timpl BranchInfo {\n\t\t/// returns details about remote branch or None\n\t\tconst fn remote_details(&self) -> Option<&RemoteBranch> {\n\t\t\tif let BranchDetails::Remote(details) = &self.details {\n\t\t\t\tSome(details)\n\t\t\t} else {\n\t\t\t\tNone\n\t\t\t}\n\t\t}\n\t}\n\n\t#[test]\n\tfn test_remote_branches() {\n\t\tlet (r1_dir, _repo) = repo_init_bare().unwrap();\n\n\t\tlet (clone1_dir, clone1) =\n\t\t\trepo_clone(r1_dir.path().to_str().unwrap()).unwrap();\n\n\t\tlet clone1_dir = clone1_dir.path().to_str().unwrap();\n\n\t\t// clone1\n\n\t\twrite_commit_file(&clone1, \"test.txt\", \"test\", \"commit1\");\n\n\t\tpush_branch(\n\t\t\t&clone1_dir.into(),\n\t\t\t\"origin\",\n\t\t\t\"master\",\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\n\t\tcreate_branch(&clone1_dir.into(), \"foo\").unwrap();\n\n\t\twrite_commit_file(&clone1, \"test.txt\", \"test2\", \"commit2\");\n\n\t\tpush_branch(\n\t\t\t&clone1_dir.into(),\n\t\t\t\"origin\",\n\t\t\t\"foo\",\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\n\t\t// clone2\n\n\t\tlet (clone2_dir, _clone2) =\n\t\t\trepo_clone(r1_dir.path().to_str().unwrap()).unwrap();\n\n\t\tlet clone2_dir = clone2_dir.path().to_str().unwrap();\n\n\t\tlet local_branches =\n\t\t\tget_branches_info(&clone2_dir.into(), true).unwrap();\n\n\t\tassert_eq!(local_branches.len(), 1);\n\n\t\tlet branches =\n\t\t\tget_branches_info(&clone2_dir.into(), false).unwrap();\n\t\tassert_eq!(dbg!(&branches).len(), 3);\n\t\tassert_eq!(&branches[0].name, \"origin/HEAD\");\n\t\tassert_eq!(&branches[1].name, \"origin/foo\");\n\t\tassert_eq!(&branches[2].name, \"origin/master\");\n\t}\n\n\t#[test]\n\tfn test_checkout_remote_branch() {\n\t\tlet (r1_dir, _repo) = repo_init_bare().unwrap();\n\n\t\tlet (clone1_dir, clone1) =\n\t\t\trepo_clone(r1_dir.path().to_str().unwrap()).unwrap();\n\t\tlet clone1_dir = clone1_dir.path().to_str().unwrap();\n\n\t\t// clone1\n\n\t\twrite_commit_file(&clone1, \"test.txt\", \"test\", \"commit1\");\n\t\tpush_branch(\n\t\t\t&clone1_dir.into(),\n\t\t\t\"origin\",\n\t\t\t\"master\",\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\t\tcreate_branch(&clone1_dir.into(), \"foo\").unwrap();\n\t\twrite_commit_file(&clone1, \"test.txt\", \"test2\", \"commit2\");\n\t\tpush_branch(\n\t\t\t&clone1_dir.into(),\n\t\t\t\"origin\",\n\t\t\t\"foo\",\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\n\t\t// clone2\n\n\t\tlet (clone2_dir, _clone2) =\n\t\t\trepo_clone(r1_dir.path().to_str().unwrap()).unwrap();\n\n\t\tlet clone2_dir = clone2_dir.path().to_str().unwrap();\n\n\t\tlet local_branches =\n\t\t\tget_branches_info(&clone2_dir.into(), true).unwrap();\n\n\t\tassert_eq!(local_branches.len(), 1);\n\n\t\tlet branches =\n\t\t\tget_branches_info(&clone2_dir.into(), false).unwrap();\n\n\t\t// checkout origin/foo\n\t\tcheckout_remote_branch(&clone2_dir.into(), &branches[1])\n\t\t\t.unwrap();\n\n\t\tassert_eq!(\n\t\t\tget_branches_info(&clone2_dir.into(), true)\n\t\t\t\t.unwrap()\n\t\t\t\t.len(),\n\t\t\t2\n\t\t);\n\n\t\tassert_eq!(\n\t\t\t&get_branch_name(&clone2_dir.into()).unwrap(),\n\t\t\t\"foo\"\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_checkout_remote_branch_hierarchical() {\n\t\tlet (r1_dir, _repo) = repo_init_bare().unwrap();\n\n\t\tlet (clone1_dir, clone1) =\n\t\t\trepo_clone(r1_dir.path().to_str().unwrap()).unwrap();\n\t\tlet clone1_dir = clone1_dir.path().to_str().unwrap();\n\n\t\t// clone1\n\n\t\tlet branch_name = \"bar/foo\";\n\n\t\twrite_commit_file(&clone1, \"test.txt\", \"test\", \"commit1\");\n\t\tpush_branch(\n\t\t\t&clone1_dir.into(),\n\t\t\t\"origin\",\n\t\t\t\"master\",\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\t\tcreate_branch(&clone1_dir.into(), branch_name).unwrap();\n\t\twrite_commit_file(&clone1, \"test.txt\", \"test2\", \"commit2\");\n\t\tpush_branch(\n\t\t\t&clone1_dir.into(),\n\t\t\t\"origin\",\n\t\t\tbranch_name,\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\n\t\t// clone2\n\n\t\tlet (clone2_dir, _clone2) =\n\t\t\trepo_clone(r1_dir.path().to_str().unwrap()).unwrap();\n\t\tlet clone2_dir = clone2_dir.path().to_str().unwrap();\n\n\t\tlet branches =\n\t\t\tget_branches_info(&clone2_dir.into(), false).unwrap();\n\n\t\tcheckout_remote_branch(&clone2_dir.into(), &branches[1])\n\t\t\t.unwrap();\n\n\t\tassert_eq!(\n\t\t\t&get_branch_name(&clone2_dir.into()).unwrap(),\n\t\t\tbranch_name\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_has_tracking() {\n\t\tlet (r1_dir, _repo) = repo_init_bare().unwrap();\n\n\t\tlet (clone1_dir, clone1) =\n\t\t\trepo_clone(r1_dir.path().to_str().unwrap()).unwrap();\n\t\tlet clone1_dir = clone1_dir.path().to_str().unwrap();\n\n\t\t// clone1\n\n\t\twrite_commit_file(&clone1, \"test.txt\", \"test\", \"commit1\");\n\t\tpush_branch(\n\t\t\t&clone1_dir.into(),\n\t\t\t\"origin\",\n\t\t\t\"master\",\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\t\tcreate_branch(&clone1_dir.into(), \"foo\").unwrap();\n\t\twrite_commit_file(&clone1, \"test.txt\", \"test2\", \"commit2\");\n\t\tpush_branch(\n\t\t\t&clone1_dir.into(),\n\t\t\t\"origin\",\n\t\t\t\"foo\",\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\n\t\tlet branches_1 =\n\t\t\tget_branches_info(&clone1_dir.into(), false).unwrap();\n\n\t\tassert!(branches_1[0].remote_details().unwrap().has_tracking);\n\t\tassert!(branches_1[1].remote_details().unwrap().has_tracking);\n\n\t\t// clone2\n\n\t\tlet (clone2_dir, _clone2) =\n\t\t\trepo_clone(r1_dir.path().to_str().unwrap()).unwrap();\n\n\t\tlet clone2_dir = clone2_dir.path().to_str().unwrap();\n\n\t\tlet branches_2 =\n\t\t\tget_branches_info(&clone2_dir.into(), false).unwrap();\n\n\t\tassert!(\n\t\t\t!branches_2[0].remote_details().unwrap().has_tracking\n\t\t);\n\t\tassert!(\n\t\t\t!branches_2[1].remote_details().unwrap().has_tracking\n\t\t);\n\t\tassert!(branches_2[2].remote_details().unwrap().has_tracking);\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/branch/rename.rs",
    "content": "//! renaming of branches\n\nuse crate::{\n\terror::Result,\n\tsync::{repository::repo, RepoPath},\n};\nuse scopetime::scope_time;\n\n/// Rename the branch reference\npub fn rename_branch(\n\trepo_path: &RepoPath,\n\tbranch_ref: &str,\n\tnew_name: &str,\n) -> Result<()> {\n\tscope_time!(\"rename_branch\");\n\n\tlet repo = repo(repo_path)?;\n\tlet branch_as_ref = repo.find_reference(branch_ref)?;\n\tlet mut branch = git2::Branch::wrap(branch_as_ref);\n\tbranch.rename(new_name, true)?;\n\n\tOk(())\n}\n\n#[cfg(test)]\nmod test {\n\tuse super::super::{checkout_branch, create_branch, RepoPath};\n\tuse super::rename_branch;\n\tuse crate::sync::tests::repo_init;\n\n\t#[test]\n\tfn test_rename_branch() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tcreate_branch(repo_path, \"branch1\").unwrap();\n\n\t\tcheckout_branch(repo_path, \"branch1\").unwrap();\n\n\t\tassert_eq!(\n\t\t\trepo.branches(None)\n\t\t\t\t.unwrap()\n\t\t\t\t.next()\n\t\t\t\t.unwrap()\n\t\t\t\t.unwrap()\n\t\t\t\t.0\n\t\t\t\t.name()\n\t\t\t\t.unwrap()\n\t\t\t\t.unwrap(),\n\t\t\t\"branch1\"\n\t\t);\n\n\t\trename_branch(repo_path, \"refs/heads/branch1\", \"AnotherName\")\n\t\t\t.unwrap();\n\n\t\tassert_eq!(\n\t\t\trepo.branches(None)\n\t\t\t\t.unwrap()\n\t\t\t\t.next()\n\t\t\t\t.unwrap()\n\t\t\t\t.unwrap()\n\t\t\t\t.0\n\t\t\t\t.name()\n\t\t\t\t.unwrap()\n\t\t\t\t.unwrap(),\n\t\t\t\"AnotherName\"\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/commit.rs",
    "content": "//! Git Api for Commits\nuse super::{CommitId, RepoPath};\nuse crate::sync::sign::{SignBuilder, SignError};\nuse crate::{\n\terror::{Error, Result},\n\tsync::{repository::repo, utils::get_head_repo},\n};\nuse git2::{\n\tmessage_prettify, ErrorCode, ObjectType, Repository, Signature,\n};\nuse scopetime::scope_time;\n\n///\npub fn amend(\n\trepo_path: &RepoPath,\n\tid: CommitId,\n\tmsg: &str,\n) -> Result<CommitId> {\n\tscope_time!(\"amend\");\n\n\tlet repo = repo(repo_path)?;\n\tlet config = repo.config()?;\n\n\tlet commit = repo.find_commit(id.into())?;\n\n\tlet mut index = repo.index()?;\n\tlet tree_id = index.write_tree()?;\n\tlet tree = repo.find_tree(tree_id)?;\n\n\tif config.get_bool(\"commit.gpgsign\").unwrap_or(false) {\n\t\t// HACK: we undo the last commit and create a new one\n\t\tuse crate::sync::utils::undo_last_commit;\n\n\t\tlet head = get_head_repo(&repo)?;\n\t\tif head == commit.id().into() {\n\t\t\tundo_last_commit(repo_path)?;\n\t\t\treturn self::commit(repo_path, msg);\n\t\t}\n\n\t\treturn Err(Error::SignAmendNonLastCommit);\n\t}\n\n\tlet committer = signature_allow_undefined_name(&repo)?;\n\n\tlet new_id = commit.amend(\n\t\tSome(\"HEAD\"),\n\t\tNone,\n\t\tSome(&committer), // Passing a value will overwrite the committer.\n\t\tNone,\n\t\tSome(msg),\n\t\tSome(&tree),\n\t)?;\n\n\tOk(CommitId::new(new_id))\n}\n\n/// Wrap `Repository::signature` to allow unknown user.name.\n///\n/// See <https://github.com/gitui-org/gitui/issues/79>.\npub(crate) fn signature_allow_undefined_name(\n\trepo: &Repository,\n) -> std::result::Result<Signature<'_>, git2::Error> {\n\tlet signature = repo.signature();\n\n\tif let Err(ref e) = signature {\n\t\tif e.code() == ErrorCode::NotFound {\n\t\t\tlet config = repo.config()?;\n\n\t\t\tif let (Err(_), Ok(email_entry)) = (\n\t\t\t\tconfig.get_entry(\"user.name\"),\n\t\t\t\tconfig.get_entry(\"user.email\"),\n\t\t\t) {\n\t\t\t\tif let Some(email) = email_entry.value() {\n\t\t\t\t\treturn Signature::now(\"unknown\", email);\n\t\t\t\t}\n\t\t\t};\n\t\t}\n\t}\n\n\tsignature\n}\n\n/// this does not run any git hooks, git-hooks have to be executed manually, checkout `hooks_commit_msg` for example\npub fn commit(repo_path: &RepoPath, msg: &str) -> Result<CommitId> {\n\tscope_time!(\"commit\");\n\n\tlet repo = repo(repo_path)?;\n\tlet config = repo.config()?;\n\tlet signature = signature_allow_undefined_name(&repo)?;\n\tlet mut index = repo.index()?;\n\tlet tree_id = index.write_tree()?;\n\tlet tree = repo.find_tree(tree_id)?;\n\n\tlet parents = if let Ok(id) = get_head_repo(&repo) {\n\t\tvec![repo.find_commit(id.into())?]\n\t} else {\n\t\tVec::new()\n\t};\n\n\tlet parents = parents.iter().collect::<Vec<_>>();\n\n\tlet commit_id = if config\n\t\t.get_bool(\"commit.gpgsign\")\n\t\t.unwrap_or(false)\n\t{\n\t\tlet buffer = repo.commit_create_buffer(\n\t\t\t&signature,\n\t\t\t&signature,\n\t\t\tmsg,\n\t\t\t&tree,\n\t\t\tparents.as_slice(),\n\t\t)?;\n\n\t\tlet commit = std::str::from_utf8(&buffer).map_err(|_e| {\n\t\t\tSignError::Shellout(\"utf8 conversion error\".to_string())\n\t\t})?;\n\n\t\tlet signer = SignBuilder::from_gitconfig(&repo, &config)?;\n\t\tlet (signature, signature_field) = signer.sign(&buffer)?;\n\t\tlet commit_id = repo.commit_signed(\n\t\t\tcommit,\n\t\t\t&signature,\n\t\t\tsignature_field.as_deref(),\n\t\t)?;\n\n\t\t// manually advance to the new commit ID\n\t\t// repo.commit does that on its own, repo.commit_signed does not\n\t\t// if there is no head, read default branch or default to \"master\"\n\t\tif let Ok(mut head) = repo.head() {\n\t\t\thead.set_target(commit_id, msg)?;\n\t\t} else {\n\t\t\tlet default_branch_name = config\n\t\t\t\t.get_str(\"init.defaultBranch\")\n\t\t\t\t.unwrap_or(\"master\");\n\t\t\trepo.reference(\n\t\t\t\t&format!(\"refs/heads/{default_branch_name}\"),\n\t\t\t\tcommit_id,\n\t\t\t\ttrue,\n\t\t\t\tmsg,\n\t\t\t)?;\n\t\t}\n\n\t\tcommit_id\n\t} else {\n\t\trepo.commit(\n\t\t\tSome(\"HEAD\"),\n\t\t\t&signature,\n\t\t\t&signature,\n\t\t\tmsg,\n\t\t\t&tree,\n\t\t\tparents.as_slice(),\n\t\t)?\n\t};\n\n\tOk(commit_id.into())\n}\n\n/// Tag a commit.\n///\n/// This function will return an `Err(…)` variant if the tag’s name is refused\n/// by git or if the tag already exists.\npub fn tag_commit(\n\trepo_path: &RepoPath,\n\tcommit_id: &CommitId,\n\ttag: &str,\n\tmessage: Option<&str>,\n) -> Result<CommitId> {\n\tscope_time!(\"tag_commit\");\n\n\tlet repo = repo(repo_path)?;\n\n\tlet object_id = commit_id.get_oid();\n\tlet target =\n\t\trepo.find_object(object_id, Some(ObjectType::Commit))?;\n\n\tlet c = if let Some(message) = message {\n\t\tlet signature = signature_allow_undefined_name(&repo)?;\n\t\trepo.tag(tag, &target, &signature, message, false)?.into()\n\t} else {\n\t\trepo.tag_lightweight(tag, &target, false)?.into()\n\t};\n\n\tOk(c)\n}\n\n/// Loads the comment prefix from config & uses it to prettify commit messages\npub fn commit_message_prettify(\n\trepo_path: &RepoPath,\n\tmessage: String,\n) -> Result<String> {\n\tlet comment_char = repo(repo_path)?\n\t\t.config()?\n\t\t.get_string(\"core.commentChar\")\n\t\t.ok()\n\t\t.and_then(|char_string| char_string.chars().next())\n\t\t.unwrap_or('#') as u8;\n\n\tOk(message_prettify(message, Some(comment_char))?)\n}\n\n#[cfg(test)]\nmod tests {\n\tuse crate::error::Result;\n\tuse crate::sync::tags::Tag;\n\tuse crate::sync::RepoPath;\n\tuse crate::sync::{\n\t\tcommit, get_commit_details, get_commit_files, stage_add_file,\n\t\ttags::get_tags,\n\t\ttests::{get_statuses, repo_init, repo_init_empty},\n\t\tutils::get_head,\n\t\tLogWalker,\n\t};\n\tuse commit::{amend, commit_message_prettify, tag_commit};\n\tuse git2::Repository;\n\tuse std::{fs::File, io::Write, path::Path};\n\n\tfn count_commits(repo: &Repository, max: usize) -> usize {\n\t\tlet mut items = Vec::new();\n\t\tlet mut walk = LogWalker::new(repo, max).unwrap();\n\t\twalk.read(&mut items).unwrap();\n\t\titems.len()\n\t}\n\n\t#[test]\n\tfn test_commit() {\n\t\tlet file_path = Path::new(\"foo\");\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(file_path))\n\t\t\t.unwrap()\n\t\t\t.write_all(b\"test\\nfoo\")\n\t\t\t.unwrap();\n\n\t\tassert_eq!(get_statuses(repo_path), (1, 0));\n\n\t\tstage_add_file(repo_path, file_path).unwrap();\n\n\t\tassert_eq!(get_statuses(repo_path), (0, 1));\n\n\t\tcommit(repo_path, \"commit msg\").unwrap();\n\n\t\tassert_eq!(get_statuses(repo_path), (0, 0));\n\t}\n\n\t#[test]\n\tfn test_commit_in_empty_repo() {\n\t\tlet file_path = Path::new(\"foo\");\n\t\tlet (_td, repo) = repo_init_empty().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tassert_eq!(get_statuses(repo_path), (0, 0));\n\n\t\tFile::create(root.join(file_path))\n\t\t\t.unwrap()\n\t\t\t.write_all(b\"test\\nfoo\")\n\t\t\t.unwrap();\n\n\t\tassert_eq!(get_statuses(repo_path), (1, 0));\n\n\t\tstage_add_file(repo_path, file_path).unwrap();\n\n\t\tassert_eq!(get_statuses(repo_path), (0, 1));\n\n\t\tcommit(repo_path, \"commit msg\").unwrap();\n\n\t\tassert_eq!(get_statuses(repo_path), (0, 0));\n\t}\n\n\t#[test]\n\tfn test_amend() -> Result<()> {\n\t\tlet file_path1 = Path::new(\"foo\");\n\t\tlet file_path2 = Path::new(\"foo2\");\n\t\tlet (_td, repo) = repo_init_empty()?;\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(file_path1))?.write_all(b\"test1\")?;\n\n\t\tstage_add_file(repo_path, file_path1)?;\n\t\tlet id = commit(repo_path, \"commit msg\")?;\n\n\t\tassert_eq!(count_commits(&repo, 10), 1);\n\n\t\tFile::create(root.join(file_path2))?.write_all(b\"test2\")?;\n\n\t\tstage_add_file(repo_path, file_path2)?;\n\n\t\tlet new_id = amend(repo_path, id, \"amended\")?;\n\n\t\tassert_eq!(count_commits(&repo, 10), 1);\n\n\t\tlet details = get_commit_details(repo_path, new_id)?;\n\t\tassert_eq!(details.message.unwrap().subject, \"amended\");\n\n\t\tlet files = get_commit_files(repo_path, new_id, None)?;\n\n\t\tassert_eq!(files.len(), 2);\n\n\t\tlet head = get_head(repo_path)?;\n\n\t\tassert_eq!(head, new_id);\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_amend_with_different_user() {\n\t\tlet file_path1 = Path::new(\"foo\");\n\t\tlet file_path2 = Path::new(\"foo2\");\n\t\tlet (_td, repo) = repo_init_empty().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(file_path1))\n\t\t\t.unwrap()\n\t\t\t.write_all(b\"test1\")\n\t\t\t.unwrap();\n\n\t\tstage_add_file(repo_path, file_path1).unwrap();\n\t\tlet id = commit(repo_path, \"commit msg\").unwrap();\n\n\t\tlet amended_details =\n\t\t\tget_commit_details(repo_path, id).unwrap();\n\n\t\tassert_eq!(amended_details.committer, None);\n\n\t\tFile::create(root.join(file_path2))\n\t\t\t.unwrap()\n\t\t\t.write_all(b\"test2\")\n\t\t\t.unwrap();\n\n\t\tstage_add_file(repo_path, file_path2).unwrap();\n\n\t\trepo.config()\n\t\t\t.unwrap()\n\t\t\t.set_str(\"user.name\", \"changed name\")\n\t\t\t.unwrap();\n\t\trepo.config()\n\t\t\t.unwrap()\n\t\t\t.set_str(\"user.email\", \"changed@example.com\")\n\t\t\t.unwrap();\n\n\t\tlet new_id = amend(repo_path, id, \"amended\").unwrap();\n\n\t\tlet amended_details =\n\t\t\tget_commit_details(repo_path, new_id).unwrap();\n\t\tassert_eq!(amended_details.author.name, \"name\");\n\t\tassert_eq!(amended_details.author.email, \"email\");\n\t\tlet committer = amended_details.committer.unwrap();\n\t\tassert_eq!(committer.name, \"changed name\");\n\t\tassert_eq!(committer.email, \"changed@example.com\");\n\t}\n\n\t#[test]\n\tfn test_tag() -> Result<()> {\n\t\tlet file_path = Path::new(\"foo\");\n\t\tlet (_td, repo) = repo_init_empty().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(file_path))?\n\t\t\t.write_all(b\"test\\nfoo\")?;\n\n\t\tstage_add_file(repo_path, file_path)?;\n\n\t\tlet new_id = commit(repo_path, \"commit msg\")?;\n\n\t\ttag_commit(repo_path, &new_id, \"tag\", None)?;\n\n\t\tassert_eq!(\n\t\t\tget_tags(repo_path).unwrap()[&new_id],\n\t\t\tvec![Tag::new(\"tag\")]\n\t\t);\n\n\t\tassert!(tag_commit(repo_path, &new_id, \"tag\", None).is_err());\n\n\t\tassert_eq!(\n\t\t\tget_tags(repo_path).unwrap()[&new_id],\n\t\t\tvec![Tag::new(\"tag\")]\n\t\t);\n\n\t\ttag_commit(repo_path, &new_id, \"second-tag\", None)?;\n\n\t\tassert_eq!(\n\t\t\tget_tags(repo_path).unwrap()[&new_id],\n\t\t\tvec![Tag::new(\"second-tag\"), Tag::new(\"tag\")]\n\t\t);\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_tag_with_message() -> Result<()> {\n\t\tlet file_path = Path::new(\"foo\");\n\t\tlet (_td, repo) = repo_init_empty().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(file_path))?\n\t\t\t.write_all(b\"test\\nfoo\")?;\n\n\t\tstage_add_file(repo_path, file_path)?;\n\n\t\tlet new_id = commit(repo_path, \"commit msg\")?;\n\n\t\ttag_commit(repo_path, &new_id, \"tag\", Some(\"tag-message\"))?;\n\n\t\tassert_eq!(\n\t\t\tget_tags(repo_path).unwrap()[&new_id][0]\n\t\t\t\t.annotation\n\t\t\t\t.as_ref()\n\t\t\t\t.unwrap(),\n\t\t\t\"tag-message\"\n\t\t);\n\n\t\tOk(())\n\t}\n\n\t/// Beware: this test has to be run with a `$HOME/.gitconfig` that has\n\t/// `user.email` not set. Otherwise, git falls back to the value of\n\t/// `user.email` in `$HOME/.gitconfig` and this test fails.\n\t///\n\t/// As of February 2021, `repo_init_empty` sets all git config locations\n\t/// to an empty temporary directory, so this constraint is met.\n\t#[test]\n\tfn test_empty_email() -> Result<()> {\n\t\tlet file_path = Path::new(\"foo\");\n\t\tlet (_td, repo) = repo_init_empty().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(file_path))?\n\t\t\t.write_all(b\"test\\nfoo\")?;\n\n\t\tstage_add_file(repo_path, file_path)?;\n\n\t\trepo.config()?.remove(\"user.email\")?;\n\n\t\tlet error = commit(repo_path, \"commit msg\");\n\n\t\tassert!(error.is_err());\n\n\t\trepo.config()?.set_str(\"user.email\", \"email\")?;\n\n\t\tlet success = commit(repo_path, \"commit msg\");\n\n\t\tassert!(success.is_ok());\n\t\tassert_eq!(count_commits(&repo, 10), 1);\n\n\t\tlet details =\n\t\t\tget_commit_details(repo_path, success.unwrap()).unwrap();\n\n\t\tassert_eq!(details.author.name, \"name\");\n\t\tassert_eq!(details.author.email, \"email\");\n\n\t\tOk(())\n\t}\n\n\t/// See comment to `test_empty_email`.\n\t#[test]\n\tfn test_empty_name() -> Result<()> {\n\t\tlet file_path = Path::new(\"foo\");\n\t\tlet (_td, repo) = repo_init_empty().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(file_path))?\n\t\t\t.write_all(b\"test\\nfoo\")?;\n\n\t\tstage_add_file(repo_path, file_path)?;\n\n\t\trepo.config()?.remove(\"user.name\")?;\n\n\t\tlet mut success = commit(repo_path, \"commit msg\");\n\n\t\tassert!(success.is_ok());\n\t\tassert_eq!(count_commits(&repo, 10), 1);\n\n\t\tlet mut details =\n\t\t\tget_commit_details(repo_path, success.unwrap()).unwrap();\n\n\t\tassert_eq!(details.author.name, \"unknown\");\n\t\tassert_eq!(details.author.email, \"email\");\n\n\t\trepo.config()?.set_str(\"user.name\", \"name\")?;\n\n\t\tsuccess = commit(repo_path, \"commit msg\");\n\n\t\tassert!(success.is_ok());\n\t\tassert_eq!(count_commits(&repo, 10), 2);\n\n\t\tdetails =\n\t\t\tget_commit_details(repo_path, success.unwrap()).unwrap();\n\n\t\tassert_eq!(details.author.name, \"name\");\n\t\tassert_eq!(details.author.email, \"email\");\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_empty_comment_char() -> Result<()> {\n\t\tlet (_td, repo) = repo_init_empty().unwrap();\n\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tlet message = commit_message_prettify(\n\t\t\trepo_path,\n\t\t\t\"#This is a test message\\nTest\".to_owned(),\n\t\t)?;\n\n\t\tassert_eq!(message, \"Test\\n\");\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_with_comment_char() -> Result<()> {\n\t\tlet (_td, repo) = repo_init_empty().unwrap();\n\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\trepo.config()?.set_str(\"core.commentChar\", \";\")?;\n\n\t\tlet message = commit_message_prettify(\n\t\t\trepo_path,\n\t\t\t\";This is a test message\\nTest\".to_owned(),\n\t\t)?;\n\n\t\tassert_eq!(message, \"Test\\n\");\n\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/commit_details.rs",
    "content": "use super::{commits_info::get_message, CommitId, RepoPath};\nuse crate::{error::Result, sync::repository::repo};\nuse git2::Signature;\nuse scopetime::scope_time;\n\n///\n#[derive(Debug, PartialEq, Eq, Default, Clone)]\npub struct CommitSignature {\n\t///\n\tpub name: String,\n\t///\n\tpub email: String,\n\t/// time in secs since Unix epoch\n\tpub time: i64,\n}\n\nimpl CommitSignature {\n\t/// convert from git2-rs `Signature`\n\tpub fn from(s: &Signature<'_>) -> Self {\n\t\tSelf {\n\t\t\tname: s.name().unwrap_or(\"\").to_string(),\n\t\t\temail: s.email().unwrap_or(\"\").to_string(),\n\n\t\t\ttime: s.when().seconds(),\n\t\t}\n\t}\n}\n\n///\n#[derive(Default, Clone)]\npub struct CommitMessage {\n\t/// first line\n\tpub subject: String,\n\t/// remaining lines if more than one\n\tpub body: Option<String>,\n}\n\nimpl CommitMessage {\n\t///\n\tpub fn from(s: &str) -> Self {\n\t\tlet mut lines = s.lines();\n\t\tlet subject = lines.next().map_or_else(\n\t\t\tString::new,\n\t\t\tstd::string::ToString::to_string,\n\t\t);\n\n\t\tlet body: Vec<String> =\n\t\t\tlines.map(std::string::ToString::to_string).collect();\n\n\t\tSelf {\n\t\t\tsubject,\n\t\t\tbody: if body.is_empty() {\n\t\t\t\tNone\n\t\t\t} else {\n\t\t\t\tSome(body.join(\"\\n\"))\n\t\t\t},\n\t\t}\n\t}\n\n\t///\n\tpub fn combine(self) -> String {\n\t\tif let Some(body) = self.body {\n\t\t\tformat!(\"{}\\n{body}\", self.subject)\n\t\t} else {\n\t\t\tself.subject\n\t\t}\n\t}\n}\n\n///\n#[derive(Default, Clone)]\npub struct CommitDetails {\n\t///\n\tpub author: CommitSignature,\n\t/// committer when differs to `author` otherwise None\n\tpub committer: Option<CommitSignature>,\n\t///\n\tpub message: Option<CommitMessage>,\n\t///\n\tpub hash: String,\n}\n\nimpl CommitDetails {\n\t///\n\tpub fn short_hash(&self) -> &str {\n\t\t&self.hash[0..7]\n\t}\n}\n\n/// Get the author of a commit.\npub fn get_author_of_commit<'a>(\n\tcommit: &'a git2::Commit<'a>,\n\tmailmap: &git2::Mailmap,\n) -> git2::Signature<'a> {\n\tmatch commit.author_with_mailmap(mailmap) {\n\t\tOk(author) => author,\n\t\tErr(e) => {\n\t\t\tlog::error!(\n\t\t\t\t\"Couldn't get author with mailmap for {} (message: {:?}): {e}\",\n\t\t\t\tcommit.id(),\n\t\t\t\tcommit.message(),\n\t\t\t);\n\t\t\tcommit.author()\n\t\t}\n\t}\n}\n\n/// Get the committer of a commit.\npub fn get_committer_of_commit<'a>(\n\tcommit: &'a git2::Commit<'a>,\n\tmailmap: &git2::Mailmap,\n) -> git2::Signature<'a> {\n\tmatch commit.committer_with_mailmap(mailmap) {\n\t\tOk(committer) => committer,\n\t\tErr(e) => {\n\t\t\tlog::error!(\n\t\t\t\t\"Couldn't get committer with mailmap for {} (message: {:?}): {e}\",\n\t\t\t\tcommit.id(),\n\t\t\t\tcommit.message(),\n\t\t\t);\n\t\t\tcommit.committer()\n\t\t}\n\t}\n}\n\n///\npub fn get_commit_details(\n\trepo_path: &RepoPath,\n\tid: CommitId,\n) -> Result<CommitDetails> {\n\tscope_time!(\"get_commit_details\");\n\n\tlet repo = repo(repo_path)?;\n\tlet mailmap = repo.mailmap()?;\n\n\tlet commit = repo.find_commit(id.into())?;\n\n\tlet author = CommitSignature::from(&get_author_of_commit(\n\t\t&commit, &mailmap,\n\t));\n\tlet committer = CommitSignature::from(&get_committer_of_commit(\n\t\t&commit, &mailmap,\n\t));\n\n\tlet committer = if author == committer {\n\t\tNone\n\t} else {\n\t\tSome(committer)\n\t};\n\n\tlet msg =\n\t\tCommitMessage::from(get_message(&commit, None).as_str());\n\n\tlet details = CommitDetails {\n\t\tauthor,\n\t\tcommitter,\n\t\tmessage: Some(msg),\n\t\thash: id.to_string(),\n\t};\n\n\tOk(details)\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::{get_commit_details, CommitMessage};\n\tuse crate::{\n\t\terror::Result,\n\t\tsync::{\n\t\t\tcommit, stage_add_file, tests::repo_init_empty, RepoPath,\n\t\t},\n\t};\n\tuse std::{fs::File, io::Write, path::Path};\n\n\t#[test]\n\tfn test_msg_invalid_utf8() -> Result<()> {\n\t\tlet file_path = Path::new(\"foo\");\n\t\tlet (_td, repo) = repo_init_empty().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(file_path))?.write_all(b\"a\")?;\n\t\tstage_add_file(repo_path, file_path).unwrap();\n\n\t\tlet msg = invalidstring::invalid_utf8(\"test msg\");\n\t\tlet id = commit(repo_path, msg.as_str()).unwrap();\n\n\t\tlet res = get_commit_details(repo_path, id).unwrap();\n\n\t\tassert!(res\n\t\t\t.message\n\t\t\t.as_ref()\n\t\t\t.unwrap()\n\t\t\t.subject\n\t\t\t.starts_with(\"test msg\"));\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_msg_linefeeds() -> Result<()> {\n\t\tlet msg = CommitMessage::from(\"foo\\nbar\\r\\ntest\");\n\n\t\tassert_eq!(msg.subject, String::from(\"foo\"),);\n\t\tassert_eq!(msg.body, Some(String::from(\"bar\\ntest\")),);\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_commit_message_combine() -> Result<()> {\n\t\tlet msg = CommitMessage::from(\"foo\\nbar\\r\\ntest\");\n\n\t\tassert_eq!(msg.combine(), String::from(\"foo\\nbar\\ntest\"));\n\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/commit_files.rs",
    "content": "//! Functions for getting infos about files in commits\n\nuse super::{diff::DiffOptions, CommitId, RepoPath};\nuse crate::{\n\terror::Result,\n\tsync::{get_stashes, repository::repo},\n\tStatusItem, StatusItemType,\n};\nuse git2::{Diff, Repository};\nuse scopetime::scope_time;\nuse std::collections::HashSet;\n\n/// struct containing a new and an old version\n#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]\npub struct OldNew<T> {\n\t/// The old version\n\tpub old: T,\n\t/// The new version\n\tpub new: T,\n}\n\n/// Sort two commits.\npub fn sort_commits(\n\trepo: &Repository,\n\tcommits: (CommitId, CommitId),\n) -> Result<OldNew<CommitId>> {\n\tif repo.graph_descendant_of(\n\t\tcommits.0.get_oid(),\n\t\tcommits.1.get_oid(),\n\t)? {\n\t\tOk(OldNew {\n\t\t\told: commits.1,\n\t\t\tnew: commits.0,\n\t\t})\n\t} else {\n\t\tOk(OldNew {\n\t\t\told: commits.0,\n\t\t\tnew: commits.1,\n\t\t})\n\t}\n}\n\n/// get all files that are part of a commit\npub fn get_commit_files(\n\trepo_path: &RepoPath,\n\tid: CommitId,\n\tother: Option<CommitId>,\n) -> Result<Vec<StatusItem>> {\n\tscope_time!(\"get_commit_files\");\n\n\tlet repo = repo(repo_path)?;\n\n\tlet diff = if let Some(other) = other {\n\t\tget_compare_commits_diff(\n\t\t\t&repo,\n\t\t\tsort_commits(&repo, (id, other))?,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)?\n\t} else {\n\t\tget_commit_diff(\n\t\t\t&repo,\n\t\t\tid,\n\t\t\tNone,\n\t\t\tNone,\n\t\t\tSome(&get_stashes(repo_path)?.into_iter().collect()),\n\t\t)?\n\t};\n\n\tlet res = diff\n\t\t.deltas()\n\t\t.map(|delta| {\n\t\t\tlet status = StatusItemType::from(delta.status());\n\n\t\t\tStatusItem {\n\t\t\t\tpath: delta\n\t\t\t\t\t.new_file()\n\t\t\t\t\t.path()\n\t\t\t\t\t.map(|p| p.to_str().unwrap_or(\"\").to_string())\n\t\t\t\t\t.unwrap_or_default(),\n\t\t\t\tstatus,\n\t\t\t}\n\t\t})\n\t\t.collect::<Vec<_>>();\n\n\tOk(res)\n}\n\n/// get diff of two arbitrary commits\n#[allow(clippy::needless_pass_by_value)]\npub fn get_compare_commits_diff(\n\trepo: &Repository,\n\tids: OldNew<CommitId>,\n\tpathspec: Option<String>,\n\toptions: Option<DiffOptions>,\n) -> Result<Diff<'_>> {\n\t// scope_time!(\"get_compare_commits_diff\");\n\tlet commits = OldNew {\n\t\told: repo.find_commit(ids.old.into())?,\n\t\tnew: repo.find_commit(ids.new.into())?,\n\t};\n\n\tlet trees = OldNew {\n\t\told: commits.old.tree()?,\n\t\tnew: commits.new.tree()?,\n\t};\n\n\tlet mut opts = git2::DiffOptions::new();\n\tif let Some(options) = options {\n\t\topts.context_lines(options.context);\n\t\topts.ignore_whitespace(options.ignore_whitespace);\n\t\topts.interhunk_lines(options.interhunk_lines);\n\t}\n\tif let Some(p) = &pathspec {\n\t\topts.pathspec(p.clone());\n\t}\n\n\tlet diff: Diff<'_> = repo.diff_tree_to_tree(\n\t\tSome(&trees.old),\n\t\tSome(&trees.new),\n\t\tSome(&mut opts),\n\t)?;\n\n\tOk(diff)\n}\n\n/// get diff of a commit to its first parent\npub(crate) fn get_commit_diff<'a>(\n\trepo: &'a Repository,\n\tid: CommitId,\n\tpathspec: Option<String>,\n\toptions: Option<DiffOptions>,\n\tstashes: Option<&HashSet<CommitId>>,\n) -> Result<Diff<'a>> {\n\t// scope_time!(\"get_commit_diff\");\n\n\tlet commit = repo.find_commit(id.into())?;\n\tlet commit_tree = commit.tree()?;\n\n\tlet parent = if commit.parent_count() > 0 {\n\t\trepo.find_commit(commit.parent_id(0)?)\n\t\t\t.ok()\n\t\t\t.and_then(|c| c.tree().ok())\n\t} else {\n\t\tNone\n\t};\n\n\tlet mut opts = git2::DiffOptions::new();\n\tif let Some(options) = options {\n\t\topts.context_lines(options.context);\n\t\topts.ignore_whitespace(options.ignore_whitespace);\n\t\topts.interhunk_lines(options.interhunk_lines);\n\t}\n\tif let Some(p) = &pathspec {\n\t\topts.pathspec(p.clone());\n\t}\n\topts.show_binary(true);\n\n\tlet mut diff = repo.diff_tree_to_tree(\n\t\tparent.as_ref(),\n\t\tSome(&commit_tree),\n\t\tSome(&mut opts),\n\t)?;\n\n\tif stashes.is_some_and(|stashes| stashes.contains(&id)) {\n\t\tif let Ok(untracked_commit) = commit.parent_id(2) {\n\t\t\tlet untracked_diff = get_commit_diff(\n\t\t\t\trepo,\n\t\t\t\tCommitId::new(untracked_commit),\n\t\t\t\tpathspec,\n\t\t\t\toptions,\n\t\t\t\tstashes,\n\t\t\t)?;\n\n\t\t\tdiff.merge(&untracked_diff)?;\n\t\t}\n\t}\n\n\tOk(diff)\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::get_commit_files;\n\tuse crate::{\n\t\terror::Result,\n\t\tsync::{\n\t\t\tcommit, stage_add_file, stash_save,\n\t\t\ttests::{get_statuses, repo_init},\n\t\t\tRepoPath,\n\t\t},\n\t\tStatusItemType,\n\t};\n\tuse std::{fs::File, io::Write, path::Path};\n\n\t#[test]\n\tfn test_smoke() -> Result<()> {\n\t\tlet file_path = Path::new(\"file1.txt\");\n\t\tlet (_td, repo) = repo_init()?;\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(file_path))?\n\t\t\t.write_all(b\"test file1 content\")?;\n\n\t\tstage_add_file(repo_path, file_path)?;\n\n\t\tlet id = commit(repo_path, \"commit msg\")?;\n\n\t\tlet diff = get_commit_files(repo_path, id, None)?;\n\n\t\tassert_eq!(diff.len(), 1);\n\t\tassert_eq!(diff[0].status, StatusItemType::New);\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_stashed_untracked() -> Result<()> {\n\t\tlet file_path = Path::new(\"file1.txt\");\n\t\tlet (_td, repo) = repo_init()?;\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(file_path))?\n\t\t\t.write_all(b\"test file1 content\")?;\n\n\t\tlet id = stash_save(repo_path, None, true, false)?;\n\n\t\tlet diff = get_commit_files(repo_path, id, None)?;\n\n\t\tassert_eq!(diff.len(), 1);\n\t\tassert_eq!(diff[0].status, StatusItemType::New);\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_stashed_untracked_and_modified() -> Result<()> {\n\t\tlet file_path1 = Path::new(\"file1.txt\");\n\t\tlet file_path2 = Path::new(\"file2.txt\");\n\t\tlet (_td, repo) = repo_init()?;\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(file_path1))?.write_all(b\"test\")?;\n\t\tstage_add_file(repo_path, file_path1)?;\n\t\tcommit(repo_path, \"c1\")?;\n\n\t\tFile::create(root.join(file_path1))?\n\t\t\t.write_all(b\"modified\")?;\n\t\tFile::create(root.join(file_path2))?.write_all(b\"new\")?;\n\n\t\tassert_eq!(get_statuses(repo_path), (2, 0));\n\n\t\tlet id = stash_save(repo_path, None, true, false)?;\n\n\t\tlet diff = get_commit_files(repo_path, id, None)?;\n\n\t\tassert_eq!(diff.len(), 2);\n\t\tassert_eq!(diff[0].status, StatusItemType::Modified);\n\t\tassert_eq!(diff[1].status, StatusItemType::New);\n\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/commit_filter.rs",
    "content": "use super::{\n\tcommit_details::get_author_of_commit,\n\tcommit_files::get_commit_diff, CommitId,\n};\nuse crate::error::Result;\nuse bitflags::bitflags;\nuse fuzzy_matcher::FuzzyMatcher;\nuse git2::{Diff, Repository};\nuse std::sync::Arc;\n\n///\npub type SharedCommitFilterFn = Arc<\n\tBox<dyn Fn(&Repository, &CommitId) -> Result<bool> + Send + Sync>,\n>;\n\n///\npub fn diff_contains_file(file_path: String) -> SharedCommitFilterFn {\n\tArc::new(Box::new(\n\t\tmove |repo: &Repository,\n\t\t      commit_id: &CommitId|\n\t\t      -> Result<bool> {\n\t\t\tlet diff = get_commit_diff(\n\t\t\t\trepo,\n\t\t\t\t*commit_id,\n\t\t\t\tSome(file_path.clone()),\n\t\t\t\tNone,\n\t\t\t\tNone,\n\t\t\t)?;\n\n\t\t\tlet contains_file = diff.deltas().len() > 0;\n\n\t\t\tOk(contains_file)\n\t\t},\n\t))\n}\n\nbitflags! {\n\t///\n\t#[derive(Debug, Clone, Copy)]\n\tpub struct SearchFields: u32 {\n\t\t///\n\t\tconst MESSAGE_SUMMARY = 1 << 0;\n\t\t///\n\t\tconst MESSAGE_BODY = 1 << 1;\n\t\t///\n\t\tconst FILENAMES = 1 << 2;\n\t\t///\n\t\tconst AUTHORS = 1 << 3;\n\t\t//TODO:\n\t\t// const COMMIT_HASHES = 1 << 3;\n\t\t// ///\n\t\t// const DATES = 1 << 4;\n\t\t// ///\n\t\t// const DIFFS = 1 << 5;\n\t}\n}\n\nimpl Default for SearchFields {\n\tfn default() -> Self {\n\t\tSelf::MESSAGE_SUMMARY\n\t}\n}\n\nbitflags! {\n\t///\n\t#[derive(Debug, Clone, Copy)]\n\tpub struct SearchOptions: u32 {\n\t\t///\n\t\tconst CASE_SENSITIVE = 1 << 0;\n\t\t///\n\t\tconst FUZZY_SEARCH = 1 << 1;\n\t}\n}\n\nimpl Default for SearchOptions {\n\tfn default() -> Self {\n\t\tSelf::empty()\n\t}\n}\n\n///\n#[derive(Default, Debug, Clone)]\npub struct LogFilterSearchOptions {\n\t///\n\tpub search_pattern: String,\n\t///\n\tpub fields: SearchFields,\n\t///\n\tpub options: SearchOptions,\n}\n\n///\n#[derive(Default)]\npub struct LogFilterSearch {\n\t///\n\tpub matcher: fuzzy_matcher::skim::SkimMatcherV2,\n\t///\n\tpub options: LogFilterSearchOptions,\n}\n\nimpl LogFilterSearch {\n\t///\n\tpub fn new(options: LogFilterSearchOptions) -> Self {\n\t\tlet mut options = options;\n\t\tif !options.options.contains(SearchOptions::CASE_SENSITIVE) {\n\t\t\toptions.search_pattern =\n\t\t\t\toptions.search_pattern.to_lowercase();\n\t\t}\n\t\tSelf {\n\t\t\tmatcher: fuzzy_matcher::skim::SkimMatcherV2::default(),\n\t\t\toptions,\n\t\t}\n\t}\n\n\tfn match_diff(&self, diff: &Diff<'_>) -> bool {\n\t\tdiff.deltas().any(|delta| {\n\t\t\tif delta\n\t\t\t\t.new_file()\n\t\t\t\t.path()\n\t\t\t\t.and_then(|file| file.as_os_str().to_str())\n\t\t\t\t.is_some_and(|file| self.match_text(file))\n\t\t\t{\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tdelta\n\t\t\t\t.old_file()\n\t\t\t\t.path()\n\t\t\t\t.and_then(|file| file.as_os_str().to_str())\n\t\t\t\t.is_some_and(|file| self.match_text(file))\n\t\t})\n\t}\n\n\t///\n\tpub fn match_text(&self, text: &str) -> bool {\n\t\tif self.options.options.contains(SearchOptions::FUZZY_SEARCH)\n\t\t{\n\t\t\tself.matcher\n\t\t\t\t.fuzzy_match(\n\t\t\t\t\ttext,\n\t\t\t\t\tself.options.search_pattern.as_str(),\n\t\t\t\t)\n\t\t\t\t.is_some()\n\t\t} else if self\n\t\t\t.options\n\t\t\t.options\n\t\t\t.contains(SearchOptions::CASE_SENSITIVE)\n\t\t{\n\t\t\ttext.contains(self.options.search_pattern.as_str())\n\t\t} else {\n\t\t\ttext.to_lowercase()\n\t\t\t\t.contains(self.options.search_pattern.as_str())\n\t\t}\n\t}\n}\n\n///\npub fn filter_commit_by_search(\n\tfilter: LogFilterSearch,\n) -> SharedCommitFilterFn {\n\tArc::new(Box::new(\n\t\tmove |repo: &Repository,\n\t\t      commit_id: &CommitId|\n\t\t      -> Result<bool> {\n\t\t\tlet mailmap = repo.mailmap()?;\n\t\t\tlet commit = repo.find_commit((*commit_id).into())?;\n\n\t\t\tlet msg_summary_match = filter\n\t\t\t\t.options\n\t\t\t\t.fields\n\t\t\t\t.contains(SearchFields::MESSAGE_SUMMARY)\n\t\t\t\t.then(|| {\n\t\t\t\t\tcommit.summary().map(|msg| filter.match_text(msg))\n\t\t\t\t})\n\t\t\t\t.flatten()\n\t\t\t\t.unwrap_or_default();\n\n\t\t\tlet msg_body_match = filter\n\t\t\t\t.options\n\t\t\t\t.fields\n\t\t\t\t.contains(SearchFields::MESSAGE_BODY)\n\t\t\t\t.then(|| {\n\t\t\t\t\tcommit.body().map(|msg| filter.match_text(msg))\n\t\t\t\t})\n\t\t\t\t.flatten()\n\t\t\t\t.unwrap_or_default();\n\n\t\t\tlet file_match = filter\n\t\t\t\t.options\n\t\t\t\t.fields\n\t\t\t\t.contains(SearchFields::FILENAMES)\n\t\t\t\t.then(|| {\n\t\t\t\t\tget_commit_diff(\n\t\t\t\t\t\trepo, *commit_id, None, None, None,\n\t\t\t\t\t)\n\t\t\t\t\t.ok()\n\t\t\t\t})\n\t\t\t\t.flatten()\n\t\t\t\t.is_some_and(|diff| filter.match_diff(&diff));\n\n\t\t\tlet authors_match = if filter\n\t\t\t\t.options\n\t\t\t\t.fields\n\t\t\t\t.contains(SearchFields::AUTHORS)\n\t\t\t{\n\t\t\t\tlet author = get_author_of_commit(&commit, &mailmap);\n\t\t\t\t[author.email(), author.name()].iter().any(\n\t\t\t\t\t|opt_haystack| {\n\t\t\t\t\t\topt_haystack.is_some_and(|haystack| {\n\t\t\t\t\t\t\tfilter.match_text(haystack)\n\t\t\t\t\t\t})\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t} else {\n\t\t\t\tfalse\n\t\t\t};\n\n\t\t\tOk(msg_summary_match\n\t\t\t\t|| msg_body_match\n\t\t\t\t|| file_match\n\t\t\t\t|| authors_match)\n\t\t},\n\t))\n}\n"
  },
  {
    "path": "asyncgit/src/sync/commit_revert.rs",
    "content": "use super::{CommitId, RepoPath};\nuse crate::{\n\terror::Result,\n\tsync::{repository::repo, utils::read_file},\n};\nuse scopetime::scope_time;\n\nconst GIT_REVERT_HEAD_FILE: &str = \"REVERT_HEAD\";\n\n///\npub fn revert_commit(\n\trepo_path: &RepoPath,\n\tcommit: CommitId,\n) -> Result<()> {\n\tscope_time!(\"revert\");\n\n\tlet repo = repo(repo_path)?;\n\n\tlet commit = repo.find_commit(commit.into())?;\n\n\trepo.revert(&commit, None)?;\n\n\tOk(())\n}\n\n///\npub fn revert_head(repo_path: &RepoPath) -> Result<CommitId> {\n\tscope_time!(\"revert_head\");\n\n\tlet path = repo(repo_path)?.path().join(GIT_REVERT_HEAD_FILE);\n\n\tlet file_content = read_file(&path)?;\n\n\tlet id = git2::Oid::from_str(file_content.trim())?;\n\n\tOk(id.into())\n}\n\n///\npub fn commit_revert(\n\trepo_path: &RepoPath,\n\tmsg: &str,\n) -> Result<CommitId> {\n\tscope_time!(\"commit_revert\");\n\n\tlet id = crate::sync::commit(repo_path, msg)?;\n\n\trepo(repo_path)?.cleanup_state()?;\n\n\tOk(id)\n}\n"
  },
  {
    "path": "asyncgit/src/sync/commits_info.rs",
    "content": "use std::fmt::Display;\n\nuse super::RepoPath;\nuse crate::{\n\terror::Result,\n\tsync::{\n\t\tcommit_details::get_author_of_commit,\n\t\trepository::{gix_repo, repo},\n\t},\n};\nuse git2::{Commit, Error, Oid};\nuse scopetime::scope_time;\nuse unicode_truncate::UnicodeTruncateStr;\n\n/// identifies a single commit\n#[derive(\n\tDebug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd,\n)]\npub struct CommitId(Oid);\n\nimpl Default for CommitId {\n\tfn default() -> Self {\n\t\tSelf(Oid::zero())\n\t}\n}\n\nimpl CommitId {\n\t/// create new `CommitId`\n\tpub const fn new(id: Oid) -> Self {\n\t\tSelf(id)\n\t}\n\n\t///\n\tpub(crate) const fn get_oid(self) -> Oid {\n\t\tself.0\n\t}\n\n\t/// 7 chars short hash\n\tpub fn get_short_string(&self) -> String {\n\t\tself.to_string().chars().take(7).collect()\n\t}\n\n\t/// Tries to retrieve the `CommitId` form the revision if exists in the given repository\n\tpub fn from_revision(\n\t\trepo_path: &RepoPath,\n\t\trevision: &str,\n\t) -> Result<Self> {\n\t\tscope_time!(\"CommitId::from_revision\");\n\n\t\tlet repo = repo(repo_path)?;\n\n\t\tlet commit_obj = repo.revparse_single(revision)?;\n\t\tOk(commit_obj.id().into())\n\t}\n\n\t/// Tries to convert a &str representation of a commit id into\n\t/// a `CommitId`\n\tpub fn from_str_unchecked(commit_id_str: &str) -> Result<Self> {\n\t\tmatch Oid::from_str(commit_id_str) {\n\t\t\tErr(e) => Err(crate::Error::Generic(format!(\n\t\t\t\t\"Could not convert {}\",\n\t\t\t\te.message()\n\t\t\t))),\n\t\t\tOk(v) => Ok(Self::new(v)),\n\t\t}\n\t}\n}\n\nimpl Display for CommitId {\n\tfn fmt(\n\t\t&self,\n\t\tf: &mut std::fmt::Formatter<'_>,\n\t) -> std::fmt::Result {\n\t\twrite!(f, \"{}\", self.0)\n\t}\n}\n\nimpl From<CommitId> for Oid {\n\tfn from(id: CommitId) -> Self {\n\t\tid.0\n\t}\n}\n\nimpl From<Oid> for CommitId {\n\tfn from(id: Oid) -> Self {\n\t\tSelf::new(id)\n\t}\n}\n\nimpl From<gix::ObjectId> for CommitId {\n\tfn from(object_id: gix::ObjectId) -> Self {\n\t\t#[allow(clippy::expect_used)]\n\t\tlet oid = Oid::from_bytes(object_id.as_bytes()).expect(\"`Oid::from_bytes(object_id.as_bytes())` is expected to never fail\");\n\n\t\tSelf::new(oid)\n\t}\n}\n\nimpl From<gix::Commit<'_>> for CommitId {\n\tfn from(commit: gix::Commit<'_>) -> Self {\n\t\t#[allow(clippy::expect_used)]\n\t\tlet oid = Oid::from_bytes(commit.id().as_bytes()).expect(\"`Oid::from_bytes(commit.id().as_bytes())` is expected to never fail\");\n\n\t\tSelf::new(oid)\n\t}\n}\n\nimpl From<CommitId> for gix::ObjectId {\n\tfn from(id: CommitId) -> Self {\n\t\tSelf::from_bytes_or_panic(id.0.as_bytes())\n\t}\n}\n\n///\n#[derive(Debug, Clone)]\npub struct CommitInfo {\n\t///\n\tpub message: String,\n\t///\n\tpub time: i64,\n\t///\n\tpub author: String,\n\t///\n\tpub id: CommitId,\n}\n\n///\npub fn get_commits_info(\n\trepo_path: &RepoPath,\n\tids: &[CommitId],\n\tmessage_length_limit: usize,\n) -> Result<Vec<CommitInfo>> {\n\tscope_time!(\"get_commits_info\");\n\n\tlet repo = repo(repo_path)?;\n\tlet mailmap = repo.mailmap()?;\n\n\tlet commits = ids\n\t\t.iter()\n\t\t.map(|id| repo.find_commit((*id).into()))\n\t\t.collect::<std::result::Result<Vec<Commit>, Error>>()?\n\t\t.into_iter();\n\n\tlet res = commits\n\t\t.map(|c: Commit| {\n\t\t\tlet message = get_message(&c, Some(message_length_limit));\n\t\t\tlet author = get_author_of_commit(&c, &mailmap)\n\t\t\t\t.name()\n\t\t\t\t.map_or_else(\n\t\t\t\t\t|| String::from(\"<unknown>\"),\n\t\t\t\t\tString::from,\n\t\t\t\t);\n\t\t\tCommitInfo {\n\t\t\t\tmessage,\n\t\t\t\tauthor,\n\t\t\t\ttime: c.time().seconds(),\n\t\t\t\tid: CommitId(c.id()),\n\t\t\t}\n\t\t})\n\t\t.collect::<Vec<_>>();\n\n\tOk(res)\n}\n\n///\npub fn get_commit_info(\n\trepo_path: &RepoPath,\n\tcommit_id: &CommitId,\n) -> Result<CommitInfo> {\n\tscope_time!(\"get_commit_info\");\n\n\tlet repo: gix::Repository = gix_repo(repo_path)?;\n\tlet mailmap = repo.open_mailmap();\n\n\tlet commit = repo.find_commit(*commit_id)?;\n\tlet commit_ref = commit.decode()?;\n\n\tlet message = gix_get_message(&commit_ref, None);\n\n\tlet author = commit_ref.author()?;\n\n\tlet author = mailmap.try_resolve(author).map_or_else(\n\t\t|| author.name.into(),\n\t\t|signature| signature.name,\n\t);\n\n\tOk(CommitInfo {\n\t\tmessage,\n\t\tauthor: author.to_string(),\n\t\ttime: commit_ref.time()?.seconds,\n\t\tid: commit.id().detach().into(),\n\t})\n}\n\n/// if `message_limit` is set the message will be\n/// limited to the first line and truncated to fit\npub fn get_message(\n\tc: &git2::Commit,\n\tmessage_limit: Option<usize>,\n) -> String {\n\tlet msg = String::from_utf8_lossy(c.message_bytes());\n\tlet msg = msg.trim();\n\n\tmessage_limit.map_or_else(\n\t\t|| msg.to_string(),\n\t\t|limit| {\n\t\t\tlet msg = msg.lines().next().unwrap_or_default();\n\t\t\tmsg.unicode_truncate(limit).0.to_string()\n\t\t},\n\t)\n}\n\n/// if `message_limit` is set the message will be\n/// limited to the first line and truncated to fit\npub fn gix_get_message(\n\tcommit_ref: &gix::objs::CommitRef,\n\tmessage_limit: Option<usize>,\n) -> String {\n\tlet message = commit_ref.message.to_string();\n\tlet message = message.trim();\n\n\tmessage_limit.map_or_else(\n\t\t|| message.to_string(),\n\t\t|limit| {\n\t\t\tlet message = message.lines().next().unwrap_or_default();\n\t\t\tmessage.unicode_truncate(limit).0.to_string()\n\t\t},\n\t)\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::get_commits_info;\n\tuse crate::{\n\t\terror::Result,\n\t\tsync::{\n\t\t\tcommit, stage_add_file, tests::repo_init_empty,\n\t\t\tutils::get_head_repo, CommitId, RepoPath,\n\t\t},\n\t};\n\tuse std::{fs::File, io::Write, path::Path};\n\n\t#[test]\n\tfn test_log() -> Result<()> {\n\t\tlet file_path = Path::new(\"foo\");\n\t\tlet (_td, repo) = repo_init_empty().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(file_path))?.write_all(b\"a\")?;\n\t\tstage_add_file(repo_path, file_path).unwrap();\n\t\tlet c1 = commit(repo_path, \"commit1\").unwrap();\n\t\tFile::create(root.join(file_path))?.write_all(b\"a\")?;\n\t\tstage_add_file(repo_path, file_path).unwrap();\n\t\tlet c2 = commit(repo_path, \"commit2\").unwrap();\n\n\t\tlet res = get_commits_info(repo_path, &[c2, c1], 50).unwrap();\n\n\t\tassert_eq!(res.len(), 2);\n\t\tassert_eq!(res[0].message.as_str(), \"commit2\");\n\t\tassert_eq!(res[0].author.as_str(), \"name\");\n\t\tassert_eq!(res[1].message.as_str(), \"commit1\");\n\n\t\tFile::create(root.join(\".mailmap\"))?\n\t\t\t.write_all(b\"new name <newemail> <email>\")?;\n\t\tlet res = get_commits_info(repo_path, &[c2], 50).unwrap();\n\n\t\tassert_eq!(res[0].author.as_str(), \"new name\");\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_log_first_msg_line() -> Result<()> {\n\t\tlet file_path = Path::new(\"foo\");\n\t\tlet (_td, repo) = repo_init_empty().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(file_path))?.write_all(b\"a\")?;\n\t\tstage_add_file(repo_path, file_path).unwrap();\n\t\tlet c1 = commit(repo_path, \"subject\\nbody\").unwrap();\n\n\t\tlet res = get_commits_info(repo_path, &[c1], 50).unwrap();\n\n\t\tassert_eq!(res.len(), 1);\n\t\tassert_eq!(res[0].message.as_str(), \"subject\");\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_invalid_utf8() -> Result<()> {\n\t\tlet file_path = Path::new(\"foo\");\n\t\tlet (_td, repo) = repo_init_empty().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(file_path))?.write_all(b\"a\")?;\n\t\tstage_add_file(repo_path, file_path).unwrap();\n\n\t\tlet msg = invalidstring::invalid_utf8(\"test msg\");\n\t\tcommit(repo_path, msg.as_str()).unwrap();\n\n\t\tlet res = get_commits_info(\n\t\t\trepo_path,\n\t\t\t&[get_head_repo(&repo).unwrap()],\n\t\t\t50,\n\t\t)\n\t\t.unwrap();\n\n\t\tassert_eq!(res.len(), 1);\n\t\tdbg!(&res[0].message);\n\t\tassert!(res[0].message.starts_with(\"test msg\"));\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_get_commit_from_revision() -> Result<()> {\n\t\tlet (_td, repo) = repo_init_empty().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tlet foo_file = Path::new(\"foo\");\n\t\tFile::create(root.join(foo_file))?.write_all(b\"a\")?;\n\t\tstage_add_file(repo_path, foo_file).unwrap();\n\t\tlet c1 = commit(repo_path, \"subject: foo\\nbody\").unwrap();\n\t\tlet c1_rev = c1.get_short_string();\n\n\t\tassert_eq!(\n\t\t\tCommitId::from_revision(repo_path, c1_rev.as_str())\n\t\t\t\t.unwrap(),\n\t\t\tc1\n\t\t);\n\n\t\tconst FOREIGN_HASH: &str =\n\t\t\t\"d6d7d55cb6e4ba7301d6a11a657aab4211e5777e\";\n\t\tassert!(\n\t\t\tCommitId::from_revision(repo_path, FOREIGN_HASH).is_err()\n\t\t);\n\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/config.rs",
    "content": "use crate::error::Result;\nuse git2::Repository;\nuse scopetime::scope_time;\nuse serde::{Deserialize, Serialize};\n\nuse super::{repository::repo, RepoPath};\n\n// see https://git-scm.com/docs/git-config#Documentation/git-config.txt-statusshowUntrackedFiles\n/// represents the `status.showUntrackedFiles` git config state\n#[derive(\n\tHash, Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize,\n)]\npub enum ShowUntrackedFilesConfig {\n\t///\n\t#[default]\n\tNo,\n\t///\n\tNormal,\n\t///\n\tAll,\n}\n\nimpl ShowUntrackedFilesConfig {\n\t///\n\tpub const fn include_none(self) -> bool {\n\t\tmatches!(self, Self::No)\n\t}\n\n\t///\n\tpub const fn include_untracked(self) -> bool {\n\t\tmatches!(self, Self::Normal | Self::All)\n\t}\n\n\t///\n\tpub const fn recurse_untracked_dirs(self) -> bool {\n\t\tmatches!(self, Self::All)\n\t}\n}\n\npub fn untracked_files_config_repo(\n\trepo: &Repository,\n) -> Result<ShowUntrackedFilesConfig> {\n\tlet show_untracked_files =\n\t\tget_config_string_repo(repo, \"status.showUntrackedFiles\")?;\n\n\tif let Some(show_untracked_files) = show_untracked_files {\n\t\tif &show_untracked_files == \"no\" {\n\t\t\treturn Ok(ShowUntrackedFilesConfig::No);\n\t\t} else if &show_untracked_files == \"normal\" {\n\t\t\treturn Ok(ShowUntrackedFilesConfig::Normal);\n\t\t}\n\t}\n\n\t// This does not reflect how git works according to its docs that say: \"If this variable is not\n\t// specified, it defaults to `normal`.\"\n\t//\n\t// https://git-scm.com/docs/git-config#Documentation/git-config.txt-statusshowUntrackedFiles\n\t//\n\t// Note that this might become less relevant over time as more code gets migrated to `gitoxide`\n\t// because `gitoxide` respects `status.showUntrackedFiles` by default.\n\tOk(ShowUntrackedFilesConfig::All)\n}\n\n// see https://git-scm.com/docs/git-config#Documentation/git-config.txt-pushdefault\n/// represents `push.default` git config\n#[derive(PartialEq, Default, Eq)]\npub enum PushDefaultStrategyConfig {\n\tNothing,\n\tCurrent,\n\tUpstream,\n\t#[default]\n\tSimple,\n\tMatching,\n}\n\nimpl<'a> TryFrom<&'a str> for PushDefaultStrategyConfig {\n\ttype Error = crate::Error;\n\tfn try_from(\n\t\tvalue: &'a str,\n\t) -> std::result::Result<Self, Self::Error> {\n\t\tmatch value {\n\t\t\t\"nothing\" => Ok(Self::Nothing),\n\t\t\t\"current\" => Ok(Self::Current),\n\t\t\t\"upstream\" | \"tracking\" => Ok(Self::Upstream),\n\t\t\t\"simple\" => Ok(Self::Simple),\n\t\t\t\"matching\" => Ok(Self::Matching),\n\t\t\t_ => Err(crate::Error::GitConfig(format!(\n\t\t\t\t\"malformed value for push.default: {value}, must be one of nothing, matching, simple, upstream or current\"\n\t\t\t))),\n\t\t}\n\t}\n}\n\npub fn push_default_strategy_config_repo(\n\trepo: &Repository,\n) -> Result<PushDefaultStrategyConfig> {\n\t(get_config_string_repo(repo, \"push.default\")?).map_or_else(\n\t\t|| Ok(PushDefaultStrategyConfig::default()),\n\t\t|entry_str| {\n\t\t\tPushDefaultStrategyConfig::try_from(entry_str.as_str())\n\t\t},\n\t)\n}\n\n///\npub fn untracked_files_config(\n\trepo_path: &RepoPath,\n) -> Result<ShowUntrackedFilesConfig> {\n\tlet repo = repo(repo_path)?;\n\tuntracked_files_config_repo(&repo)\n}\n\n/// get string from config\npub fn get_config_string(\n\trepo_path: &RepoPath,\n\tkey: &str,\n) -> Result<Option<String>> {\n\tlet repo = repo(repo_path)?;\n\tget_config_string_repo(&repo, key)\n}\n\npub fn get_config_string_repo(\n\trepo: &Repository,\n\tkey: &str,\n) -> Result<Option<String>> {\n\tscope_time!(\"get_config_string_repo\");\n\n\tlet cfg = repo.config()?;\n\n\t// this code doesn't match what the doc says regarding what\n\t// gets returned when but it actually works\n\tlet entry_res = cfg.get_entry(key);\n\n\tlet Ok(entry) = entry_res else {\n\t\treturn Ok(None);\n\t};\n\n\tif entry.has_value() {\n\t\tOk(entry.value().map(std::string::ToString::to_string))\n\t} else {\n\t\tOk(None)\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::sync::tests::repo_init;\n\n\t#[test]\n\tfn test_get_config() {\n\t\tlet bad_dir_cfg = get_config_string(\n\t\t\t&\"oodly_noodly\".into(),\n\t\t\t\"this.doesnt.exist\",\n\t\t);\n\t\tassert!(bad_dir_cfg.is_err());\n\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet path = repo.path();\n\t\tlet rpath = path.as_os_str().to_str().unwrap();\n\t\tlet bad_cfg =\n\t\t\tget_config_string(&rpath.into(), \"this.doesnt.exist\");\n\t\tassert!(bad_cfg.is_ok());\n\t\tassert!(bad_cfg.unwrap().is_none());\n\t\t// repo init sets user.name\n\t\tlet good_cfg = get_config_string(&rpath.into(), \"user.name\");\n\t\tassert!(good_cfg.is_ok());\n\t\tassert!(good_cfg.unwrap().is_some());\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/cred.rs",
    "content": "//! credentials git helper\n\nuse super::{\n\tremotes::{\n\t\tget_default_remote_for_fetch_in_repo,\n\t\tget_default_remote_for_push_in_repo,\n\t\tget_default_remote_in_repo,\n\t},\n\trepository::repo,\n\tRepoPath,\n};\nuse crate::error::{Error, Result};\nuse git2::CredentialHelper;\n\n/// basic Authentication Credentials\n#[derive(Debug, Clone, Default, PartialEq, Eq)]\npub struct BasicAuthCredential {\n\t///\n\tpub username: Option<String>,\n\t///\n\tpub password: Option<String>,\n}\n\nimpl BasicAuthCredential {\n\t///\n\tpub const fn is_complete(&self) -> bool {\n\t\tself.username.is_some() && self.password.is_some()\n\t}\n\t///\n\tpub const fn new(\n\t\tusername: Option<String>,\n\t\tpassword: Option<String>,\n\t) -> Self {\n\t\tSelf { username, password }\n\t}\n}\n\n/// know if username and password are needed for this url\npub fn need_username_password(repo_path: &RepoPath) -> Result<bool> {\n\tlet repo = repo(repo_path)?;\n\tlet remote =\n\t\trepo.find_remote(&get_default_remote_in_repo(&repo)?)?;\n\tlet url = remote\n\t\t.pushurl()\n\t\t.or_else(|| remote.url())\n\t\t.ok_or(Error::UnknownRemote)?\n\t\t.to_owned();\n\tlet is_http = url.starts_with(\"http\");\n\tOk(is_http)\n}\n\n/// know if username and password are needed for this url\n/// TODO: Very similar to `need_username_password_for_fetch`. Can be refactored. See also\n/// `need_username_password`.\npub fn need_username_password_for_fetch(\n\trepo_path: &RepoPath,\n) -> Result<bool> {\n\tlet repo = repo(repo_path)?;\n\tlet remote = repo\n\t\t.find_remote(&get_default_remote_for_fetch_in_repo(&repo)?)?;\n\tlet url = remote\n\t\t.url()\n\t\t.or_else(|| remote.url())\n\t\t.ok_or(Error::UnknownRemote)?\n\t\t.to_owned();\n\tlet is_http = url.starts_with(\"http\");\n\tOk(is_http)\n}\n\n/// know if username and password are needed for this url\n/// TODO: Very similar to `need_username_password_for_fetch`. Can be refactored. See also\n/// `need_username_password`.\npub fn need_username_password_for_push(\n\trepo_path: &RepoPath,\n) -> Result<bool> {\n\tlet repo = repo(repo_path)?;\n\tlet remote = repo\n\t\t.find_remote(&get_default_remote_for_push_in_repo(&repo)?)?;\n\tlet url = remote\n\t\t.pushurl()\n\t\t.or_else(|| remote.url())\n\t\t.ok_or(Error::UnknownRemote)?\n\t\t.to_owned();\n\tlet is_http = url.starts_with(\"http\");\n\tOk(is_http)\n}\n\n/// extract username and password\npub fn extract_username_password(\n\trepo_path: &RepoPath,\n) -> Result<BasicAuthCredential> {\n\tlet repo = repo(repo_path)?;\n\tlet url = repo\n\t\t.find_remote(&get_default_remote_in_repo(&repo)?)?\n\t\t.url()\n\t\t.ok_or(Error::UnknownRemote)?\n\t\t.to_owned();\n\tlet mut helper = CredentialHelper::new(&url);\n\n\t//TODO: look at Cred::credential_helper,\n\t//if the username is in the url we need to set it here,\n\t//I dont think `config` will pick it up\n\n\tif let Ok(config) = repo.config() {\n\t\thelper.config(&config);\n\t}\n\n\tOk(match helper.execute() {\n\t\tSome((username, password)) => {\n\t\t\tBasicAuthCredential::new(Some(username), Some(password))\n\t\t}\n\t\tNone => extract_cred_from_url(&url),\n\t})\n}\n\n/// extract username and password\n/// TODO: Very similar to `extract_username_password_for_fetch`. Can be refactored.\npub fn extract_username_password_for_fetch(\n\trepo_path: &RepoPath,\n) -> Result<BasicAuthCredential> {\n\tlet repo = repo(repo_path)?;\n\tlet url = repo\n\t\t.find_remote(&get_default_remote_for_fetch_in_repo(&repo)?)?\n\t\t.url()\n\t\t.ok_or(Error::UnknownRemote)?\n\t\t.to_owned();\n\tlet mut helper = CredentialHelper::new(&url);\n\n\t//TODO: look at Cred::credential_helper,\n\t//if the username is in the url we need to set it here,\n\t//I dont think `config` will pick it up\n\n\tif let Ok(config) = repo.config() {\n\t\thelper.config(&config);\n\t}\n\n\tOk(match helper.execute() {\n\t\tSome((username, password)) => {\n\t\t\tBasicAuthCredential::new(Some(username), Some(password))\n\t\t}\n\t\tNone => extract_cred_from_url(&url),\n\t})\n}\n\n/// extract username and password\n/// TODO: Very similar to `extract_username_password_for_fetch`. Can be refactored.\npub fn extract_username_password_for_push(\n\trepo_path: &RepoPath,\n) -> Result<BasicAuthCredential> {\n\tlet repo = repo(repo_path)?;\n\tlet url = repo\n\t\t.find_remote(&get_default_remote_for_push_in_repo(&repo)?)?\n\t\t.url()\n\t\t.ok_or(Error::UnknownRemote)?\n\t\t.to_owned();\n\tlet mut helper = CredentialHelper::new(&url);\n\n\t//TODO: look at Cred::credential_helper,\n\t//if the username is in the url we need to set it here,\n\t//I dont think `config` will pick it up\n\n\tif let Ok(config) = repo.config() {\n\t\thelper.config(&config);\n\t}\n\n\tOk(match helper.execute() {\n\t\tSome((username, password)) => {\n\t\t\tBasicAuthCredential::new(Some(username), Some(password))\n\t\t}\n\t\tNone => extract_cred_from_url(&url),\n\t})\n}\n\n/// extract credentials from url\npub fn extract_cred_from_url(url: &str) -> BasicAuthCredential {\n\turl::Url::parse(url).map_or_else(\n\t\t|_| BasicAuthCredential::new(None, None),\n\t\t|url| {\n\t\t\tBasicAuthCredential::new(\n\t\t\t\tif url.username() == \"\" {\n\t\t\t\t\tNone\n\t\t\t\t} else {\n\t\t\t\t\tSome(url.username().to_owned())\n\t\t\t\t},\n\t\t\t\turl.password().map(std::borrow::ToOwned::to_owned),\n\t\t\t)\n\t\t},\n\t)\n}\n\n#[cfg(test)]\nmod tests {\n\tuse crate::sync::{\n\t\tcred::{\n\t\t\textract_cred_from_url, extract_username_password,\n\t\t\tneed_username_password, BasicAuthCredential,\n\t\t},\n\t\tremotes::DEFAULT_REMOTE_NAME,\n\t\ttests::repo_init,\n\t\tRepoPath,\n\t};\n\tuse serial_test::serial;\n\n\t#[test]\n\tfn test_credential_complete() {\n\t\tassert!(BasicAuthCredential::new(\n\t\t\tSome(\"username\".to_owned()),\n\t\t\tSome(\"password\".to_owned())\n\t\t)\n\t\t.is_complete());\n\t}\n\n\t#[test]\n\tfn test_credential_not_complete() {\n\t\tassert!(!BasicAuthCredential::new(\n\t\t\tNone,\n\t\t\tSome(\"password\".to_owned())\n\t\t)\n\t\t.is_complete());\n\t\tassert!(!BasicAuthCredential::new(\n\t\t\tSome(\"username\".to_owned()),\n\t\t\tNone\n\t\t)\n\t\t.is_complete());\n\t\tassert!(!BasicAuthCredential::new(None, None).is_complete());\n\t}\n\n\t#[test]\n\tfn test_extract_username_from_url() {\n\t\tassert_eq!(\n\t\t\textract_cred_from_url(\"https://user@github.com\"),\n\t\t\tBasicAuthCredential::new(Some(\"user\".to_owned()), None)\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_extract_username_password_from_url() {\n\t\tassert_eq!(\n\t\t\textract_cred_from_url(\"https://user:pwd@github.com\"),\n\t\t\tBasicAuthCredential::new(\n\t\t\t\tSome(\"user\".to_owned()),\n\t\t\t\tSome(\"pwd\".to_owned())\n\t\t\t)\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_extract_nothing_from_url() {\n\t\tassert_eq!(\n\t\t\textract_cred_from_url(\"https://github.com\"),\n\t\t\tBasicAuthCredential::new(None, None)\n\t\t);\n\t}\n\n\t#[test]\n\t#[serial]\n\tfn test_need_username_password_if_https() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\trepo.remote(DEFAULT_REMOTE_NAME, \"http://user@github.com\")\n\t\t\t.unwrap();\n\n\t\tassert!(need_username_password(repo_path).unwrap());\n\t}\n\n\t#[test]\n\t#[serial]\n\tfn test_dont_need_username_password_if_ssh() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\trepo.remote(DEFAULT_REMOTE_NAME, \"git@github.com:user/repo\")\n\t\t\t.unwrap();\n\n\t\tassert!(!need_username_password(repo_path).unwrap());\n\t}\n\n\t#[test]\n\t#[serial]\n\tfn test_dont_need_username_password_if_pushurl_ssh() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\trepo.remote(DEFAULT_REMOTE_NAME, \"http://user@github.com\")\n\t\t\t.unwrap();\n\t\trepo.remote_set_pushurl(\n\t\t\tDEFAULT_REMOTE_NAME,\n\t\t\tSome(\"git@github.com:user/repo\"),\n\t\t)\n\t\t.unwrap();\n\n\t\tassert!(!need_username_password(repo_path).unwrap());\n\t}\n\n\t#[test]\n\t#[serial]\n\t#[should_panic]\n\tfn test_error_if_no_remote_when_trying_to_retrieve_if_need_username_password(\n\t) {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tneed_username_password(repo_path).unwrap();\n\t}\n\n\t#[test]\n\t#[serial]\n\tfn test_extract_username_password_from_repo() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\trepo.remote(\n\t\t\tDEFAULT_REMOTE_NAME,\n\t\t\t\"http://user:pass@github.com\",\n\t\t)\n\t\t.unwrap();\n\n\t\tassert_eq!(\n\t\t\textract_username_password(repo_path).unwrap(),\n\t\t\tBasicAuthCredential::new(\n\t\t\t\tSome(\"user\".to_owned()),\n\t\t\t\tSome(\"pass\".to_owned())\n\t\t\t)\n\t\t);\n\t}\n\n\t#[test]\n\t#[serial]\n\tfn test_extract_username_from_repo() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\trepo.remote(DEFAULT_REMOTE_NAME, \"http://user@github.com\")\n\t\t\t.unwrap();\n\n\t\tassert_eq!(\n\t\t\textract_username_password(repo_path).unwrap(),\n\t\t\tBasicAuthCredential::new(Some(\"user\".to_owned()), None)\n\t\t);\n\t}\n\n\t#[test]\n\t#[serial]\n\t#[should_panic]\n\tfn test_error_if_no_remote_when_trying_to_extract_username_password(\n\t) {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\textract_username_password(repo_path).unwrap();\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/diff.rs",
    "content": "//! sync git api for fetching a diff\n\nuse super::{\n\tcommit_files::{\n\t\tget_commit_diff, get_compare_commits_diff, OldNew,\n\t},\n\tutils::{get_head_repo, work_dir},\n\tCommitId, RepoPath,\n};\nuse crate::{\n\terror::Error,\n\terror::Result,\n\thash,\n\tsync::{get_stashes, repository::repo},\n};\nuse easy_cast::Conv;\nuse git2::{\n\tDelta, Diff, DiffDelta, DiffFormat, DiffHunk, Patch, Repository,\n};\nuse scopetime::scope_time;\nuse serde::{Deserialize, Serialize};\nuse std::{cell::RefCell, fs, path::Path, rc::Rc};\n\n/// type of diff of a single line\n#[derive(Copy, Clone, Default, PartialEq, Eq, Hash, Debug)]\npub enum DiffLineType {\n\t/// just surrounding line, no change\n\t#[default]\n\tNone,\n\t/// header of the hunk\n\tHeader,\n\t/// line added\n\tAdd,\n\t/// line deleted\n\tDelete,\n}\n\nimpl From<git2::DiffLineType> for DiffLineType {\n\tfn from(line_type: git2::DiffLineType) -> Self {\n\t\tmatch line_type {\n\t\t\tgit2::DiffLineType::HunkHeader => Self::Header,\n\t\t\tgit2::DiffLineType::DeleteEOFNL\n\t\t\t| git2::DiffLineType::Deletion => Self::Delete,\n\t\t\tgit2::DiffLineType::AddEOFNL\n\t\t\t| git2::DiffLineType::Addition => Self::Add,\n\t\t\t_ => Self::None,\n\t\t}\n\t}\n}\n\n///\n#[derive(Default, Clone, Hash, Debug)]\npub struct DiffLine {\n\t///\n\tpub content: Box<str>,\n\t///\n\tpub line_type: DiffLineType,\n\t///\n\tpub position: DiffLinePosition,\n}\n\n///\n#[derive(Clone, Copy, Default, Hash, Debug, PartialEq, Eq)]\npub struct DiffLinePosition {\n\t///\n\tpub old_lineno: Option<u32>,\n\t///\n\tpub new_lineno: Option<u32>,\n}\n\nimpl PartialEq<&git2::DiffLine<'_>> for DiffLinePosition {\n\tfn eq(&self, other: &&git2::DiffLine) -> bool {\n\t\tother.new_lineno() == self.new_lineno\n\t\t\t&& other.old_lineno() == self.old_lineno\n\t}\n}\n\nimpl From<&git2::DiffLine<'_>> for DiffLinePosition {\n\tfn from(line: &git2::DiffLine<'_>) -> Self {\n\t\tSelf {\n\t\t\told_lineno: line.old_lineno(),\n\t\t\tnew_lineno: line.new_lineno(),\n\t\t}\n\t}\n}\n\n#[derive(Debug, Default, Clone, Copy, PartialEq, Hash)]\npub(crate) struct HunkHeader {\n\tpub old_start: u32,\n\tpub old_lines: u32,\n\tpub new_start: u32,\n\tpub new_lines: u32,\n}\n\nimpl From<DiffHunk<'_>> for HunkHeader {\n\tfn from(h: DiffHunk) -> Self {\n\t\tSelf {\n\t\t\told_start: h.old_start(),\n\t\t\told_lines: h.old_lines(),\n\t\t\tnew_start: h.new_start(),\n\t\t\tnew_lines: h.new_lines(),\n\t\t}\n\t}\n}\n\n/// single diff hunk\n#[derive(Default, Clone, Hash, Debug)]\npub struct Hunk {\n\t/// hash of the hunk header\n\tpub header_hash: u64,\n\t/// list of `DiffLine`s\n\tpub lines: Vec<DiffLine>,\n}\n\n/// collection of hunks, sum of all diff lines\n#[derive(Default, Clone, Hash, Debug)]\npub struct FileDiff {\n\t/// list of hunks\n\tpub hunks: Vec<Hunk>,\n\t/// lines total summed up over hunks\n\tpub lines: usize,\n\t///\n\tpub untracked: bool,\n\t/// old and new file size in bytes\n\tpub sizes: (u64, u64),\n\t/// size delta in bytes\n\tpub size_delta: i64,\n}\n\n/// see <https://libgit2.org/libgit2/#HEAD/type/git_diff_options>\n#[derive(\n\tDebug, Hash, Clone, Copy, PartialEq, Eq, Serialize, Deserialize,\n)]\npub struct DiffOptions {\n\t/// see <https://libgit2.org/libgit2/#HEAD/type/git_diff_options>\n\tpub ignore_whitespace: bool,\n\t/// see <https://libgit2.org/libgit2/#HEAD/type/git_diff_options>\n\tpub context: u32,\n\t/// see <https://libgit2.org/libgit2/#HEAD/type/git_diff_options>\n\tpub interhunk_lines: u32,\n}\n\nimpl Default for DiffOptions {\n\tfn default() -> Self {\n\t\tSelf {\n\t\t\tignore_whitespace: false,\n\t\t\tcontext: 3,\n\t\t\tinterhunk_lines: 0,\n\t\t}\n\t}\n}\n\npub(crate) fn get_diff_raw<'a>(\n\trepo: &'a Repository,\n\tp: &str,\n\tstage: bool,\n\treverse: bool,\n\toptions: Option<DiffOptions>,\n) -> Result<Diff<'a>> {\n\t// scope_time!(\"get_diff_raw\");\n\n\tlet mut opt = git2::DiffOptions::new();\n\tif let Some(options) = options {\n\t\topt.context_lines(options.context);\n\t\topt.ignore_whitespace(options.ignore_whitespace);\n\t\topt.interhunk_lines(options.interhunk_lines);\n\t}\n\topt.pathspec(p);\n\topt.reverse(reverse);\n\n\tlet diff = if stage {\n\t\t// diff against head\n\t\tif let Ok(id) = get_head_repo(repo) {\n\t\t\tlet parent = repo.find_commit(id.into())?;\n\n\t\t\tlet tree = parent.tree()?;\n\t\t\trepo.diff_tree_to_index(\n\t\t\t\tSome(&tree),\n\t\t\t\tSome(&repo.index()?),\n\t\t\t\tSome(&mut opt),\n\t\t\t)?\n\t\t} else {\n\t\t\trepo.diff_tree_to_index(\n\t\t\t\tNone,\n\t\t\t\tSome(&repo.index()?),\n\t\t\t\tSome(&mut opt),\n\t\t\t)?\n\t\t}\n\t} else {\n\t\topt.include_untracked(true);\n\t\topt.recurse_untracked_dirs(true);\n\t\trepo.diff_index_to_workdir(None, Some(&mut opt))?\n\t};\n\n\tOk(diff)\n}\n\n/// returns diff of a specific file either in `stage` or workdir\npub fn get_diff(\n\trepo_path: &RepoPath,\n\tp: &str,\n\tstage: bool,\n\toptions: Option<DiffOptions>,\n) -> Result<FileDiff> {\n\tscope_time!(\"get_diff\");\n\n\tlet repo = repo(repo_path)?;\n\tlet work_dir = work_dir(&repo)?;\n\tlet diff = get_diff_raw(&repo, p, stage, false, options)?;\n\n\traw_diff_to_file_diff(&diff, work_dir)\n}\n\n/// returns diff of a specific file inside a commit\n/// see `get_commit_diff`\npub fn get_diff_commit(\n\trepo_path: &RepoPath,\n\tid: CommitId,\n\tp: String,\n\toptions: Option<DiffOptions>,\n) -> Result<FileDiff> {\n\tscope_time!(\"get_diff_commit\");\n\n\tlet repo = repo(repo_path)?;\n\tlet work_dir = work_dir(&repo)?;\n\tlet diff = get_commit_diff(\n\t\t&repo,\n\t\tid,\n\t\tSome(p),\n\t\toptions,\n\t\tSome(&get_stashes(repo_path)?.into_iter().collect()),\n\t)?;\n\n\traw_diff_to_file_diff(&diff, work_dir)\n}\n\n/// get file changes of a diff between two commits\npub fn get_diff_commits(\n\trepo_path: &RepoPath,\n\tids: OldNew<CommitId>,\n\tp: String,\n\toptions: Option<DiffOptions>,\n) -> Result<FileDiff> {\n\tscope_time!(\"get_diff_commits\");\n\n\tlet repo = repo(repo_path)?;\n\tlet work_dir = work_dir(&repo)?;\n\tlet diff =\n\t\tget_compare_commits_diff(&repo, ids, Some(p), options)?;\n\n\traw_diff_to_file_diff(&diff, work_dir)\n}\n\n///\n//TODO: refactor into helper type with the inline closures as dedicated functions\n#[allow(clippy::too_many_lines)]\nfn raw_diff_to_file_diff(\n\tdiff: &Diff,\n\twork_dir: &Path,\n) -> Result<FileDiff> {\n\tlet res = Rc::new(RefCell::new(FileDiff::default()));\n\t{\n\t\tlet mut current_lines = Vec::new();\n\t\tlet mut current_hunk: Option<HunkHeader> = None;\n\n\t\tlet res_cell = Rc::clone(&res);\n\t\tlet adder = move |header: &HunkHeader,\n\t\t                  lines: &Vec<DiffLine>| {\n\t\t\tlet mut res = res_cell.borrow_mut();\n\t\t\tres.hunks.push(Hunk {\n\t\t\t\theader_hash: hash(header),\n\t\t\t\tlines: lines.clone(),\n\t\t\t});\n\t\t\tres.lines += lines.len();\n\t\t};\n\n\t\tlet res_cell = Rc::clone(&res);\n\t\tlet mut put = |delta: DiffDelta,\n\t\t               hunk: Option<DiffHunk>,\n\t\t               line: git2::DiffLine| {\n\t\t\t{\n\t\t\t\tlet mut res = res_cell.borrow_mut();\n\t\t\t\tres.sizes = (\n\t\t\t\t\tdelta.old_file().size(),\n\t\t\t\t\tdelta.new_file().size(),\n\t\t\t\t);\n\t\t\t\t//TODO: use try_conv\n\t\t\t\tres.size_delta = (i64::conv(res.sizes.1))\n\t\t\t\t\t.saturating_sub(i64::conv(res.sizes.0));\n\t\t\t}\n\t\t\tif let Some(hunk) = hunk {\n\t\t\t\tlet hunk_header = HunkHeader::from(hunk);\n\n\t\t\t\tmatch current_hunk {\n\t\t\t\t\tNone => current_hunk = Some(hunk_header),\n\t\t\t\t\tSome(h) => {\n\t\t\t\t\t\tif h != hunk_header {\n\t\t\t\t\t\t\tadder(&h, &current_lines);\n\t\t\t\t\t\t\tcurrent_lines.clear();\n\t\t\t\t\t\t\tcurrent_hunk = Some(hunk_header);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tlet diff_line = DiffLine {\n\t\t\t\t\tposition: DiffLinePosition::from(&line),\n\t\t\t\t\tcontent: String::from_utf8_lossy(line.content())\n\t\t\t\t\t\t//Note: trim await trailing newline characters\n\t\t\t\t\t\t.trim_matches(is_newline)\n\t\t\t\t\t\t.into(),\n\t\t\t\t\tline_type: line.origin_value().into(),\n\t\t\t\t};\n\n\t\t\t\tcurrent_lines.push(diff_line);\n\t\t\t}\n\t\t};\n\n\t\tlet new_file_diff = if diff.deltas().len() == 1 {\n\t\t\tif let Some(delta) = diff.deltas().next() {\n\t\t\t\tif delta.status() == Delta::Untracked {\n\t\t\t\t\tlet relative_path =\n\t\t\t\t\t\tdelta.new_file().path().ok_or_else(|| {\n\t\t\t\t\t\t\tError::Generic(\n\t\t\t\t\t\t\t\t\"new file path is unspecified.\"\n\t\t\t\t\t\t\t\t\t.to_string(),\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t})?;\n\n\t\t\t\t\tlet newfile_path = work_dir.join(relative_path);\n\n\t\t\t\t\tif let Some(newfile_content) =\n\t\t\t\t\t\tnew_file_content(&newfile_path)\n\t\t\t\t\t{\n\t\t\t\t\t\tlet mut patch = Patch::from_buffers(\n\t\t\t\t\t\t\t&[],\n\t\t\t\t\t\t\tNone,\n\t\t\t\t\t\t\tnewfile_content.as_slice(),\n\t\t\t\t\t\t\tSome(&newfile_path),\n\t\t\t\t\t\t\tNone,\n\t\t\t\t\t\t)?;\n\n\t\t\t\t\t\tpatch.print(\n\t\t\t\t\t\t\t&mut |delta,\n\t\t\t\t\t\t\t      hunk: Option<DiffHunk>,\n\t\t\t\t\t\t\t      line: git2::DiffLine| {\n\t\t\t\t\t\t\t\tput(delta, hunk, line);\n\t\t\t\t\t\t\t\ttrue\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t)?;\n\n\t\t\t\t\t\ttrue\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfalse\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tfalse\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfalse\n\t\t\t}\n\t\t} else {\n\t\t\tfalse\n\t\t};\n\n\t\tif !new_file_diff {\n\t\t\tdiff.print(\n\t\t\t\tDiffFormat::Patch,\n\t\t\t\tmove |delta, hunk, line: git2::DiffLine| {\n\t\t\t\t\tput(delta, hunk, line);\n\t\t\t\t\ttrue\n\t\t\t\t},\n\t\t\t)?;\n\t\t}\n\n\t\tif !current_lines.is_empty() {\n\t\t\tadder(\n\t\t\t\t&current_hunk.map_or_else(\n\t\t\t\t\t|| Err(Error::Generic(\"invalid hunk\".to_owned())),\n\t\t\t\t\tOk,\n\t\t\t\t)?,\n\t\t\t\t&current_lines,\n\t\t\t);\n\t\t}\n\n\t\tif new_file_diff {\n\t\t\tres.borrow_mut().untracked = true;\n\t\t}\n\t}\n\tlet res = Rc::try_unwrap(res)\n\t\t.map_err(|_| Error::Generic(\"rc unwrap error\".to_owned()))?;\n\tOk(res.into_inner())\n}\n\nconst fn is_newline(c: char) -> bool {\n\tc == '\\n' || c == '\\r'\n}\n\nfn new_file_content(path: &Path) -> Option<Vec<u8>> {\n\tif let Ok(meta) = fs::symlink_metadata(path) {\n\t\tif meta.file_type().is_symlink() {\n\t\t\tif let Ok(path) = fs::read_link(path) {\n\t\t\t\treturn Some(\n\t\t\t\t\tpath.to_str()?.to_string().as_bytes().into(),\n\t\t\t\t);\n\t\t\t}\n\t\t} else if !meta.file_type().is_dir() {\n\t\t\tif let Ok(content) = fs::read(path) {\n\t\t\t\treturn Some(content);\n\t\t\t}\n\t\t}\n\t}\n\n\tNone\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::{get_diff, get_diff_commit};\n\tuse crate::{\n\t\terror::Result,\n\t\tsync::{\n\t\t\tcommit, stage_add_file,\n\t\t\tstatus::{get_status, StatusType},\n\t\t\ttests::{get_statuses, repo_init, repo_init_empty},\n\t\t\tRepoPath,\n\t\t},\n\t};\n\tuse std::{\n\t\tfs::{self, File},\n\t\tio::Write,\n\t\tpath::Path,\n\t};\n\n\t#[test]\n\tfn test_untracked_subfolder() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tassert_eq!(get_statuses(repo_path), (0, 0));\n\n\t\tfs::create_dir(root.join(\"foo\")).unwrap();\n\t\tFile::create(root.join(\"foo/bar.txt\"))\n\t\t\t.unwrap()\n\t\t\t.write_all(b\"test\\nfoo\")\n\t\t\t.unwrap();\n\n\t\tassert_eq!(get_statuses(repo_path), (1, 0));\n\n\t\tlet diff =\n\t\t\tget_diff(repo_path, \"foo/bar.txt\", false, None).unwrap();\n\n\t\tassert_eq!(diff.hunks.len(), 1);\n\t\tassert_eq!(&*diff.hunks[0].lines[1].content, \"test\");\n\t}\n\n\t#[test]\n\tfn test_empty_repo() {\n\t\tlet file_path = Path::new(\"foo.txt\");\n\t\tlet (_td, repo) = repo_init_empty().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tassert_eq!(get_statuses(repo_path), (0, 0));\n\n\t\tFile::create(root.join(file_path))\n\t\t\t.unwrap()\n\t\t\t.write_all(b\"test\\nfoo\")\n\t\t\t.unwrap();\n\n\t\tassert_eq!(get_statuses(repo_path), (1, 0));\n\n\t\tstage_add_file(repo_path, file_path).unwrap();\n\n\t\tassert_eq!(get_statuses(repo_path), (0, 1));\n\n\t\tlet diff = get_diff(\n\t\t\trepo_path,\n\t\t\tfile_path.to_str().unwrap(),\n\t\t\ttrue,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\n\t\tassert_eq!(diff.hunks.len(), 1);\n\t}\n\n\tstatic HUNK_A: &str = r\"\n1   start\n2\n3\n4\n5\n6   middle\n7\n8\n9\n0\n1   end\";\n\n\tstatic HUNK_B: &str = r\"\n1   start\n2   newa\n3\n4\n5\n6   middle\n7\n8\n9\n0   newb\n1   end\";\n\n\t#[test]\n\tfn test_hunks() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tassert_eq!(get_statuses(repo_path), (0, 0));\n\n\t\tlet file_path = root.join(\"bar.txt\");\n\n\t\t{\n\t\t\tFile::create(&file_path)\n\t\t\t\t.unwrap()\n\t\t\t\t.write_all(HUNK_A.as_bytes())\n\t\t\t\t.unwrap();\n\t\t}\n\n\t\tlet res = get_status(repo_path, StatusType::WorkingDir, None)\n\t\t\t.unwrap();\n\t\tassert_eq!(res.len(), 1);\n\t\tassert_eq!(res[0].path, \"bar.txt\");\n\n\t\tstage_add_file(repo_path, Path::new(\"bar.txt\")).unwrap();\n\t\tassert_eq!(get_statuses(repo_path), (0, 1));\n\n\t\t// overwrite with next content\n\t\t{\n\t\t\tFile::create(&file_path)\n\t\t\t\t.unwrap()\n\t\t\t\t.write_all(HUNK_B.as_bytes())\n\t\t\t\t.unwrap();\n\t\t}\n\n\t\tassert_eq!(get_statuses(repo_path), (1, 1));\n\n\t\tlet res =\n\t\t\tget_diff(repo_path, \"bar.txt\", false, None).unwrap();\n\n\t\tassert_eq!(res.hunks.len(), 2);\n\t}\n\n\t#[test]\n\tfn test_diff_newfile_in_sub_dir_current_dir() {\n\t\tlet file_path = Path::new(\"foo/foo.txt\");\n\t\tlet (_td, repo) = repo_init_empty().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\n\t\tlet sub_path = root.join(\"foo/\");\n\n\t\tfs::create_dir_all(&sub_path).unwrap();\n\t\tFile::create(root.join(file_path))\n\t\t\t.unwrap()\n\t\t\t.write_all(b\"test\")\n\t\t\t.unwrap();\n\n\t\tlet diff = get_diff(\n\t\t\t&sub_path.to_str().unwrap().into(),\n\t\t\tfile_path.to_str().unwrap(),\n\t\t\tfalse,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\n\t\tassert_eq!(&*diff.hunks[0].lines[1].content, \"test\");\n\t}\n\n\t#[test]\n\tfn test_diff_delta_size() -> Result<()> {\n\t\tlet file_path = Path::new(\"bar\");\n\t\tlet (_td, repo) = repo_init_empty().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(file_path))?.write_all(b\"\\x00\")?;\n\n\t\tstage_add_file(repo_path, file_path).unwrap();\n\n\t\tcommit(repo_path, \"commit\").unwrap();\n\n\t\tFile::create(root.join(file_path))?.write_all(b\"\\x00\\x02\")?;\n\n\t\tlet diff = get_diff(\n\t\t\trepo_path,\n\t\t\tfile_path.to_str().unwrap(),\n\t\t\tfalse,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\n\t\tdbg!(&diff);\n\t\tassert_eq!(diff.sizes, (1, 2));\n\t\tassert_eq!(diff.size_delta, 1);\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_binary_diff_delta_size_untracked() -> Result<()> {\n\t\tlet file_path = Path::new(\"bar\");\n\t\tlet (_td, repo) = repo_init_empty().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(file_path))?.write_all(b\"\\x00\\xc7\")?;\n\n\t\tlet diff = get_diff(\n\t\t\trepo_path,\n\t\t\tfile_path.to_str().unwrap(),\n\t\t\tfalse,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\n\t\tdbg!(&diff);\n\t\tassert_eq!(diff.sizes, (0, 2));\n\t\tassert_eq!(diff.size_delta, 2);\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_diff_delta_size_commit() -> Result<()> {\n\t\tlet file_path = Path::new(\"bar\");\n\t\tlet (_td, repo) = repo_init_empty().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(file_path))?.write_all(b\"\\x00\")?;\n\n\t\tstage_add_file(repo_path, file_path).unwrap();\n\n\t\tcommit(repo_path, \"\").unwrap();\n\n\t\tFile::create(root.join(file_path))?.write_all(b\"\\x00\\x02\")?;\n\n\t\tstage_add_file(repo_path, file_path).unwrap();\n\n\t\tlet id = commit(repo_path, \"\").unwrap();\n\n\t\tlet diff =\n\t\t\tget_diff_commit(repo_path, id, String::new(), None)\n\t\t\t\t.unwrap();\n\n\t\tdbg!(&diff);\n\t\tassert_eq!(diff.sizes, (1, 2));\n\t\tassert_eq!(diff.size_delta, 1);\n\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/hooks.rs",
    "content": "use super::{repository::repo, RepoPath};\nuse crate::{\n\terror::Result,\n\tsync::{\n\t\tbranch::get_branch_upstream_merge,\n\t\tconfig::{\n\t\t\tpush_default_strategy_config_repo,\n\t\t\tPushDefaultStrategyConfig,\n\t\t},\n\t\tremotes::{proxy_auto, tags::tags_missing_remote, Callbacks},\n\t},\n};\nuse git2::{BranchType, Direction, Oid};\npub use git2_hooks::{PrePushRef, PrepareCommitMsgSource};\nuse scopetime::scope_time;\nuse std::collections::HashMap;\n\n///\n#[derive(Debug, PartialEq, Eq)]\npub enum HookResult {\n\t/// Everything went fine\n\tOk,\n\t/// Hook returned error\n\tNotOk(String),\n}\n\nimpl From<git2_hooks::HookResult> for HookResult {\n\tfn from(v: git2_hooks::HookResult) -> Self {\n\t\tmatch v {\n\t\t\tgit2_hooks::HookResult::NoHookFound => Self::Ok,\n\t\t\tgit2_hooks::HookResult::Run(response) => {\n\t\t\t\tif response.is_successful() {\n\t\t\t\t\tSelf::Ok\n\t\t\t\t} else {\n\t\t\t\t\tSelf::NotOk(if response.stderr.is_empty() {\n\t\t\t\t\t\tresponse.stdout\n\t\t\t\t\t} else if response.stdout.is_empty() {\n\t\t\t\t\t\tresponse.stderr\n\t\t\t\t\t} else {\n\t\t\t\t\t\tformat!(\n\t\t\t\t\t\t\t\"{}\\n{}\",\n\t\t\t\t\t\t\tresponse.stdout, response.stderr\n\t\t\t\t\t\t)\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n/// Retrieve advertised refs from the remote for the upcoming push.\nfn advertised_remote_refs(\n\trepo_path: &RepoPath,\n\tremote: Option<&str>,\n\turl: &str,\n\tbasic_credential: Option<crate::sync::cred::BasicAuthCredential>,\n) -> Result<HashMap<String, Oid>> {\n\tlet repo = repo(repo_path)?;\n\tlet mut remote_handle = if let Some(name) = remote {\n\t\trepo.find_remote(name)?\n\t} else {\n\t\trepo.remote_anonymous(url)?\n\t};\n\n\tlet callbacks = Callbacks::new(None, basic_credential);\n\tlet conn = remote_handle.connect_auth(\n\t\tDirection::Push,\n\t\tSome(callbacks.callbacks()),\n\t\tSome(proxy_auto()),\n\t)?;\n\n\tlet mut map = HashMap::new();\n\tfor head in conn.list()? {\n\t\tmap.insert(head.name().to_string(), head.oid());\n\t}\n\n\tOk(map)\n}\n\n/// Determine the remote ref name for a branch push.\n///\n/// Respects `push.default=upstream` config when set and upstream is configured.\n/// Otherwise defaults to `refs/heads/{branch}`. Delete operations always use\n/// the simple ref name.\nfn get_remote_ref_for_push(\n\trepo_path: &RepoPath,\n\tbranch: &str,\n\tdelete: bool,\n) -> Result<String> {\n\t// For delete operations, always use the simple ref name\n\t// regardless of push.default configuration\n\tif delete {\n\t\treturn Ok(format!(\"refs/heads/{branch}\"));\n\t}\n\n\tlet repo = repo(repo_path)?;\n\tlet push_default_strategy =\n\t\tpush_default_strategy_config_repo(&repo)?;\n\n\t// When push.default=upstream, use the configured upstream ref if available\n\tif push_default_strategy == PushDefaultStrategyConfig::Upstream {\n\t\tif let Ok(Some(upstream_ref)) =\n\t\t\tget_branch_upstream_merge(repo_path, branch)\n\t\t{\n\t\t\treturn Ok(upstream_ref);\n\t\t}\n\t\t// If upstream strategy is set but no upstream is configured,\n\t\t// fall through to default behavior\n\t}\n\n\t// Default: push to remote branch with same name as local\n\tOk(format!(\"refs/heads/{branch}\"))\n}\n\n/// see `git2_hooks::hooks_commit_msg`\npub fn hooks_commit_msg(\n\trepo_path: &RepoPath,\n\tmsg: &mut String,\n) -> Result<HookResult> {\n\tscope_time!(\"hooks_commit_msg\");\n\n\tlet repo = repo(repo_path)?;\n\n\tOk(git2_hooks::hooks_commit_msg(&repo, None, msg)?.into())\n}\n\n/// see `git2_hooks::hooks_pre_commit`\npub fn hooks_pre_commit(repo_path: &RepoPath) -> Result<HookResult> {\n\tscope_time!(\"hooks_pre_commit\");\n\n\tlet repo = repo(repo_path)?;\n\n\tOk(git2_hooks::hooks_pre_commit(&repo, None)?.into())\n}\n\n/// see `git2_hooks::hooks_post_commit`\npub fn hooks_post_commit(repo_path: &RepoPath) -> Result<HookResult> {\n\tscope_time!(\"hooks_post_commit\");\n\n\tlet repo = repo(repo_path)?;\n\n\tOk(git2_hooks::hooks_post_commit(&repo, None)?.into())\n}\n\n/// see `git2_hooks::hooks_prepare_commit_msg`\npub fn hooks_prepare_commit_msg(\n\trepo_path: &RepoPath,\n\tsource: PrepareCommitMsgSource,\n\tmsg: &mut String,\n) -> Result<HookResult> {\n\tscope_time!(\"hooks_prepare_commit_msg\");\n\n\tlet repo = repo(repo_path)?;\n\n\tOk(git2_hooks::hooks_prepare_commit_msg(\n\t\t&repo, None, source, msg,\n\t)?\n\t.into())\n}\n\n/// see `git2_hooks::hooks_pre_push`\npub fn hooks_pre_push(\n\trepo_path: &RepoPath,\n\tremote: &str,\n\tpush: &PrePushTarget<'_>,\n\tbasic_credential: Option<crate::sync::cred::BasicAuthCredential>,\n) -> Result<HookResult> {\n\tscope_time!(\"hooks_pre_push\");\n\n\tlet repo = repo(repo_path)?;\n\tif !git2_hooks::hook_available(\n\t\t&repo,\n\t\tNone,\n\t\tgit2_hooks::HOOK_PRE_PUSH,\n\t)? {\n\t\treturn Ok(HookResult::Ok);\n\t}\n\n\tlet git_remote = repo.find_remote(remote)?;\n\tlet url = git_remote\n\t\t.pushurl()\n\t\t.or_else(|| git_remote.url())\n\t\t.ok_or_else(|| {\n\t\t\tcrate::error::Error::Generic(format!(\n\t\t\t\t\"remote '{remote}' has no URL configured\"\n\t\t\t))\n\t\t})?\n\t\t.to_string();\n\n\tlet advertised = advertised_remote_refs(\n\t\trepo_path,\n\t\tSome(remote),\n\t\t&url,\n\t\tbasic_credential,\n\t)?;\n\tlet updates = match push {\n\t\tPrePushTarget::Branch { branch, delete } => {\n\t\t\tlet remote_ref =\n\t\t\t\tget_remote_ref_for_push(repo_path, branch, *delete)?;\n\t\t\tvec![pre_push_branch_update(\n\t\t\t\trepo_path,\n\t\t\t\tbranch,\n\t\t\t\t&remote_ref,\n\t\t\t\t*delete,\n\t\t\t\t&advertised,\n\t\t\t)?]\n\t\t}\n\t\tPrePushTarget::Tags => {\n\t\t\tpre_push_tag_updates(repo_path, remote, &advertised)?\n\t\t}\n\t};\n\n\tOk(git2_hooks::hooks_pre_push(\n\t\t&repo,\n\t\tNone,\n\t\tSome(remote),\n\t\t&url,\n\t\t&updates,\n\t)?\n\t.into())\n}\n\n/// Build a single pre-push update line for a branch.\nfn pre_push_branch_update(\n\trepo_path: &RepoPath,\n\tbranch_name: &str,\n\tremote_ref: &str,\n\tdelete: bool,\n\tadvertised: &HashMap<String, Oid>,\n) -> Result<PrePushRef> {\n\tlet repo = repo(repo_path)?;\n\tlet local_ref = format!(\"refs/heads/{branch_name}\");\n\tlet local_oid = (!delete)\n\t\t.then(|| {\n\t\t\trepo.find_branch(branch_name, BranchType::Local)\n\t\t\t\t.ok()\n\t\t\t\t.and_then(|branch| branch.get().peel_to_commit().ok())\n\t\t\t\t.map(|commit| commit.id())\n\t\t})\n\t\t.flatten();\n\n\tlet remote_oid = advertised.get(remote_ref).copied();\n\n\tOk(PrePushRef::new(\n\t\tlocal_ref, local_oid, remote_ref, remote_oid,\n\t))\n}\n\n/// Build pre-push updates for tags that are missing on the remote.\nfn pre_push_tag_updates(\n\trepo_path: &RepoPath,\n\tremote: &str,\n\tadvertised: &HashMap<String, Oid>,\n) -> Result<Vec<PrePushRef>> {\n\tlet repo = repo(repo_path)?;\n\tlet tags = tags_missing_remote(repo_path, remote, None)?;\n\tlet mut updates = Vec::with_capacity(tags.len());\n\n\tfor tag_ref in tags {\n\t\tif let Ok(reference) = repo.find_reference(&tag_ref) {\n\t\t\tlet tag_oid = reference.target().or_else(|| {\n\t\t\t\treference.peel_to_commit().ok().map(|c| c.id())\n\t\t\t});\n\t\t\tlet remote_ref = tag_ref.clone();\n\t\t\tlet advertised_oid = advertised.get(&remote_ref).copied();\n\t\t\tupdates.push(PrePushRef::new(\n\t\t\t\ttag_ref.clone(),\n\t\t\t\ttag_oid,\n\t\t\t\tremote_ref,\n\t\t\t\tadvertised_oid,\n\t\t\t));\n\t\t}\n\t}\n\n\tOk(updates)\n}\n\n/// What is being pushed.\npub enum PrePushTarget<'a> {\n\t/// Push a single branch.\n\tBranch {\n\t\t/// Local branch name being pushed.\n\t\tbranch: &'a str,\n\t\t/// Whether this is a delete push.\n\t\tdelete: bool,\n\t},\n\t/// Push tags.\n\tTags,\n}\n\n#[cfg(test)]\nmod tests {\n\tuse std::{ffi::OsString, io::Write as _, path::Path};\n\n\tuse git2::Repository;\n\tuse tempfile::TempDir;\n\n\tuse super::*;\n\tuse crate::sync::tests::repo_init_with_prefix;\n\n\tfn repo_init() -> Result<(TempDir, Repository)> {\n\t\tlet mut os_string: OsString = OsString::new();\n\n\t\tos_string.push(\"gitui $# ' \");\n\n\t\t#[cfg(target_os = \"linux\")]\n\t\t{\n\t\t\tuse std::os::unix::ffi::OsStrExt;\n\n\t\t\tconst INVALID_UTF8: &[u8] = b\"\\xED\\xA0\\x80\";\n\n\t\t\tos_string.push(std::ffi::OsStr::from_bytes(INVALID_UTF8));\n\n\t\t\tassert!(os_string.to_str().is_none());\n\t\t}\n\n\t\tos_string.push(\" \");\n\n\t\trepo_init_with_prefix(os_string)\n\t}\n\n\tfn create_hook_in_path(path: &Path, hook_script: &[u8]) {\n\t\tstd::fs::File::create(path)\n\t\t\t.unwrap()\n\t\t\t.write_all(hook_script)\n\t\t\t.unwrap();\n\n\t\t#[cfg(unix)]\n\t\t{\n\t\t\tstd::process::Command::new(\"chmod\")\n\t\t\t\t.arg(\"+x\")\n\t\t\t\t.arg(path)\n\t\t\t\t// .current_dir(path)\n\t\t\t\t.output()\n\t\t\t\t.unwrap();\n\t\t}\n\t}\n\n\t#[test]\n\tfn test_post_commit_hook_reject_in_subfolder() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.workdir().unwrap();\n\n\t\tlet hook = b\"#!/bin/sh\n\techo 'rejected'\n\texit 1\n\t\t\t\";\n\n\t\tgit2_hooks::create_hook(\n\t\t\t&repo,\n\t\t\tgit2_hooks::HOOK_POST_COMMIT,\n\t\t\thook,\n\t\t);\n\n\t\tlet subfolder = root.join(\"foo/\");\n\t\tstd::fs::create_dir_all(&subfolder).unwrap();\n\n\t\tlet res = hooks_post_commit(&subfolder.into()).unwrap();\n\n\t\tassert_eq!(\n\t\t\tres,\n\t\t\tHookResult::NotOk(String::from(\"rejected\\n\"))\n\t\t);\n\t}\n\n\t// make sure we run the hooks with the correct pwd.\n\t// for non-bare repos this is the dir of the worktree\n\t// unfortunately does not work on windows\n\t#[test]\n\t#[cfg(unix)]\n\tfn test_pre_commit_workdir() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.workdir().unwrap();\n\t\tlet repo_path: &RepoPath = &root.to_path_buf().into();\n\n\t\tlet hook = b\"#!/bin/sh\n\techo \\\"$(pwd)\\\"\n\texit 1\n\t\t\";\n\t\tgit2_hooks::create_hook(\n\t\t\t&repo,\n\t\t\tgit2_hooks::HOOK_PRE_COMMIT,\n\t\t\thook,\n\t\t);\n\t\tlet res = hooks_pre_commit(repo_path).unwrap();\n\t\tif let HookResult::NotOk(res) = res {\n\t\t\tassert_eq!(\n\t\t\t\tres.trim_end().trim_end_matches('/'),\n\t\t\t\t// TODO: fix if output isn't utf8.\n\t\t\t\troot.to_string_lossy().trim_end_matches('/'),\n\t\t\t);\n\t\t} else {\n\t\t\tassert!(false);\n\t\t}\n\t}\n\n\t#[test]\n\tfn test_hooks_commit_msg_reject_in_subfolder() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.workdir().unwrap();\n\n\t\tlet hook = b\"#!/bin/sh\n\techo 'msg' > \\\"$1\\\"\n\techo 'rejected'\n\texit 1\n\t\t\";\n\n\t\tgit2_hooks::create_hook(\n\t\t\t&repo,\n\t\t\tgit2_hooks::HOOK_COMMIT_MSG,\n\t\t\thook,\n\t\t);\n\n\t\tlet subfolder = root.join(\"foo/\");\n\t\tstd::fs::create_dir_all(&subfolder).unwrap();\n\n\t\tlet mut msg = String::from(\"test\");\n\t\tlet res =\n\t\t\thooks_commit_msg(&subfolder.into(), &mut msg).unwrap();\n\n\t\tassert_eq!(\n\t\t\tres,\n\t\t\tHookResult::NotOk(String::from(\"rejected\\n\"))\n\t\t);\n\n\t\tassert_eq!(msg, String::from(\"msg\\n\"));\n\t}\n\n\t#[test]\n\tfn test_hooks_commit_msg_reject_in_hooks_folder_githooks_moved_absolute(\n\t) {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.workdir().unwrap();\n\t\tlet mut config = repo.config().unwrap();\n\n\t\tconst HOOKS_DIR: &str = \"my_hooks\";\n\t\tconfig.set_str(\"core.hooksPath\", HOOKS_DIR).unwrap();\n\n\t\tlet hook = b\"#!/bin/sh\n\techo 'msg' > \\\"$1\\\"\n\techo 'rejected'\n\texit 1\n\t        \";\n\t\tlet hooks_folder = root.join(HOOKS_DIR);\n\t\tstd::fs::create_dir_all(&hooks_folder).unwrap();\n\t\tcreate_hook_in_path(&hooks_folder.join(\"commit-msg\"), hook);\n\n\t\tlet mut msg = String::from(\"test\");\n\t\tlet res =\n\t\t\thooks_commit_msg(&hooks_folder.into(), &mut msg).unwrap();\n\t\tassert_eq!(\n\t\t\tres,\n\t\t\tHookResult::NotOk(String::from(\"rejected\\n\"))\n\t\t);\n\n\t\tassert_eq!(msg, String::from(\"msg\\n\"));\n\t}\n\n\t#[test]\n\tfn test_pre_push_hook_rejects_based_on_stdin() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\n\t\tlet hook = b\"#!/bin/sh\ncat\nexit 1\n        \";\n\n\t\tgit2_hooks::create_hook(\n\t\t\t&repo,\n\t\t\tgit2_hooks::HOOK_PRE_PUSH,\n\t\t\thook,\n\t\t);\n\n\t\tlet commit_id = repo.head().unwrap().target().unwrap();\n\t\tlet update = git2_hooks::PrePushRef::new(\n\t\t\t\"refs/heads/master\",\n\t\t\tSome(commit_id),\n\t\t\t\"refs/heads/master\",\n\t\t\tNone,\n\t\t);\n\n\t\tlet expected_stdin =\n\t\t\tgit2_hooks::PrePushRef::to_stdin(&[update.clone()]);\n\n\t\tlet res = git2_hooks::hooks_pre_push(\n\t\t\t&repo,\n\t\t\tNone,\n\t\t\tSome(\"origin\"),\n\t\t\t\"https://github.com/test/repo.git\",\n\t\t\t&[update],\n\t\t)\n\t\t.unwrap();\n\n\t\tlet git2_hooks::HookResult::Run(response) = res else {\n\t\t\tpanic!(\"Expected Run result\");\n\t\t};\n\t\tassert!(!response.is_successful());\n\t\tassert_eq!(response.stdout, expected_stdin);\n\t\tassert!(expected_stdin.contains(\"refs/heads/master\"));\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/hunks.rs",
    "content": "use super::{\n\tdiff::{get_diff_raw, DiffOptions, HunkHeader},\n\tRepoPath,\n};\nuse crate::{\n\terror::{Error, Result},\n\thash,\n\tsync::repository::repo,\n};\nuse git2::{ApplyLocation, ApplyOptions, Diff};\nuse scopetime::scope_time;\n\n///\npub fn stage_hunk(\n\trepo_path: &RepoPath,\n\tfile_path: &str,\n\thunk_hash: u64,\n\toptions: Option<DiffOptions>,\n) -> Result<()> {\n\tscope_time!(\"stage_hunk\");\n\n\tlet repo = repo(repo_path)?;\n\n\tlet diff = get_diff_raw(&repo, file_path, false, false, options)?;\n\n\tlet mut opt = ApplyOptions::new();\n\topt.hunk_callback(|hunk| {\n\t\thunk.is_some_and(|hunk| {\n\t\t\tlet header = HunkHeader::from(hunk);\n\t\t\thash(&header) == hunk_hash\n\t\t})\n\t});\n\n\trepo.apply(&diff, ApplyLocation::Index, Some(&mut opt))?;\n\n\tOk(())\n}\n\n/// this will fail for an all untracked file\npub fn reset_hunk(\n\trepo_path: &RepoPath,\n\tfile_path: &str,\n\thunk_hash: u64,\n\toptions: Option<DiffOptions>,\n) -> Result<()> {\n\tscope_time!(\"reset_hunk\");\n\n\tlet repo = repo(repo_path)?;\n\n\tlet diff = get_diff_raw(&repo, file_path, false, false, options)?;\n\n\tlet hunk_index = find_hunk_index(&diff, hunk_hash);\n\tif let Some(hunk_index) = hunk_index {\n\t\tlet mut hunk_idx = 0;\n\t\tlet mut opt = ApplyOptions::new();\n\t\topt.hunk_callback(|_hunk| {\n\t\t\tlet res = hunk_idx == hunk_index;\n\t\t\thunk_idx += 1;\n\t\t\tres\n\t\t});\n\n\t\tlet diff = get_diff_raw(&repo, file_path, false, true, None)?;\n\n\t\trepo.apply(&diff, ApplyLocation::WorkDir, Some(&mut opt))?;\n\n\t\tOk(())\n\t} else {\n\t\tErr(Error::Generic(\"hunk not found\".to_string()))\n\t}\n}\n\nfn find_hunk_index(diff: &Diff, hunk_hash: u64) -> Option<usize> {\n\tlet mut result = None;\n\n\tlet mut hunk_count = 0;\n\n\tlet foreach_result = diff.foreach(\n\t\t&mut |_, _| true,\n\t\tNone,\n\t\tSome(&mut |_, hunk| {\n\t\t\tlet header = HunkHeader::from(hunk);\n\t\t\tif hash(&header) == hunk_hash {\n\t\t\t\tresult = Some(hunk_count);\n\t\t\t}\n\t\t\thunk_count += 1;\n\t\t\ttrue\n\t\t}),\n\t\tNone,\n\t);\n\n\tif foreach_result.is_ok() {\n\t\tresult\n\t} else {\n\t\tNone\n\t}\n}\n\n///\npub fn unstage_hunk(\n\trepo_path: &RepoPath,\n\tfile_path: &str,\n\thunk_hash: u64,\n\toptions: Option<DiffOptions>,\n) -> Result<bool> {\n\tscope_time!(\"revert_hunk\");\n\n\tlet repo = repo(repo_path)?;\n\n\tlet diff = get_diff_raw(&repo, file_path, true, false, options)?;\n\tlet diff_count_positive = diff.deltas().len();\n\n\tlet hunk_index = find_hunk_index(&diff, hunk_hash);\n\tlet hunk_index = hunk_index.map_or_else(\n\t\t|| Err(Error::Generic(\"hunk not found\".to_string())),\n\t\tOk,\n\t)?;\n\n\tlet diff = get_diff_raw(&repo, file_path, true, true, options)?;\n\n\tif diff.deltas().len() != diff_count_positive {\n\t\treturn Err(Error::Generic(format!(\n\t\t\t\"hunk error: {}!={}\",\n\t\t\tdiff.deltas().len(),\n\t\t\tdiff_count_positive\n\t\t)));\n\t}\n\n\tlet mut count = 0;\n\t{\n\t\tlet mut hunk_idx = 0;\n\t\tlet mut opt = ApplyOptions::new();\n\t\topt.hunk_callback(|_hunk| {\n\t\t\tlet res = if hunk_idx == hunk_index {\n\t\t\t\tcount += 1;\n\t\t\t\ttrue\n\t\t\t} else {\n\t\t\t\tfalse\n\t\t\t};\n\n\t\t\thunk_idx += 1;\n\n\t\t\tres\n\t\t});\n\n\t\trepo.apply(&diff, ApplyLocation::Index, Some(&mut opt))?;\n\t}\n\n\tOk(count == 1)\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::{\n\t\terror::Result,\n\t\tsync::{diff::get_diff, tests::repo_init_empty},\n\t};\n\tuse std::{\n\t\tfs::{self, File},\n\t\tio::Write,\n\t\tpath::Path,\n\t};\n\n\t#[test]\n\tfn reset_untracked_file_which_will_not_find_hunk() -> Result<()> {\n\t\tlet file_path = Path::new(\"foo/foo.txt\");\n\t\tlet (_td, repo) = repo_init_empty()?;\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\t\tlet sub_path = root.join(\"foo/\");\n\n\t\tfs::create_dir_all(&sub_path)?;\n\t\tFile::create(root.join(file_path))?.write_all(b\"test\")?;\n\n\t\tlet sub_path: &RepoPath = &sub_path.to_str().unwrap().into();\n\t\tlet diff = get_diff(\n\t\t\tsub_path,\n\t\t\tfile_path.to_str().unwrap(),\n\t\t\tfalse,\n\t\t\tNone,\n\t\t)?;\n\n\t\tassert!(reset_hunk(\n\t\t\trepo_path,\n\t\t\tfile_path.to_str().unwrap(),\n\t\t\tdiff.hunks[0].header_hash,\n\t\t\tNone,\n\t\t)\n\t\t.is_err());\n\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/ignore.rs",
    "content": "use super::{utils::work_dir, RepoPath};\nuse crate::{\n\terror::{Error, Result},\n\tsync::repository::repo,\n};\nuse scopetime::scope_time;\nuse std::{\n\tfs::{File, OpenOptions},\n\tio::{Read, Seek, SeekFrom, Write},\n\tpath::Path,\n};\n\nstatic GITIGNORE: &str = \".gitignore\";\n\n/// add file or path to root ignore file\npub fn add_to_ignore(\n\trepo_path: &RepoPath,\n\tpath_to_ignore: &str,\n) -> Result<()> {\n\tscope_time!(\"add_to_ignore\");\n\n\tlet repo = repo(repo_path)?;\n\n\tif Path::new(path_to_ignore).file_name()\n\t\t== Path::new(GITIGNORE).file_name()\n\t{\n\t\treturn Err(Error::Generic(String::from(\n\t\t\t\"cannot ignore gitignore\",\n\t\t)));\n\t}\n\n\tlet ignore_file = work_dir(&repo)?.join(GITIGNORE);\n\n\tlet optional_newline = ignore_file.exists()\n\t\t&& !file_ends_with_newline(&ignore_file)?;\n\n\tlet mut file = OpenOptions::new()\n\t\t.append(true)\n\t\t.create(true)\n\t\t.open(ignore_file)?;\n\n\twriteln!(\n\t\tfile,\n\t\t\"{}{}\",\n\t\tif optional_newline { \"\\n\" } else { \"\" },\n\t\tpath_to_ignore\n\t)?;\n\n\tOk(())\n}\n\nfn file_ends_with_newline(file: &Path) -> Result<bool> {\n\tlet mut file = File::open(file)?;\n\tlet size = file.metadata()?.len();\n\n\tfile.seek(SeekFrom::Start(size.saturating_sub(1)))?;\n\tlet mut last_char = String::with_capacity(1);\n\tfile.read_to_string(&mut last_char)?;\n\n\tOk(last_char == \"\\n\")\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::sync::{tests::repo_init, utils::repo_write_file};\n\tuse io::BufRead;\n\tuse pretty_assertions::assert_eq;\n\tuse std::{fs::File, io, path::Path};\n\n\t#[test]\n\tfn test_empty() -> Result<()> {\n\t\tlet ignore_file_path = Path::new(\".gitignore\");\n\t\tlet file_path = Path::new(\"foo.txt\");\n\t\tlet (_td, repo) = repo_init()?;\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(file_path))?.write_all(b\"test\")?;\n\n\t\tassert_eq!(root.join(ignore_file_path).exists(), false);\n\t\tadd_to_ignore(repo_path, file_path.to_str().unwrap())?;\n\t\tassert_eq!(root.join(ignore_file_path).exists(), true);\n\n\t\tOk(())\n\t}\n\n\tfn read_lines<P>(\n\t\tfilename: P,\n\t) -> io::Result<io::Lines<io::BufReader<File>>>\n\twhere\n\t\tP: AsRef<Path>,\n\t{\n\t\tlet file = File::open(filename)?;\n\t\tOk(io::BufReader::new(file).lines())\n\t}\n\n\t#[test]\n\tfn test_append() -> Result<()> {\n\t\tlet ignore_file_path = Path::new(\".gitignore\");\n\t\tlet file_path = Path::new(\"foo.txt\");\n\t\tlet (_td, repo) = repo_init()?;\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(file_path))?.write_all(b\"test\")?;\n\t\tFile::create(root.join(ignore_file_path))?\n\t\t\t.write_all(b\"foo\\n\")?;\n\n\t\tadd_to_ignore(repo_path, file_path.to_str().unwrap())?;\n\n\t\tlet mut lines =\n\t\t\tread_lines(root.join(ignore_file_path)).unwrap();\n\t\tassert_eq!(&lines.nth(1).unwrap().unwrap(), \"foo.txt\");\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_append_no_newline_at_end() -> Result<()> {\n\t\tlet ignore_file_path = Path::new(\".gitignore\");\n\t\tlet file_path = Path::new(\"foo.txt\");\n\t\tlet (_td, repo) = repo_init()?;\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(file_path))?.write_all(b\"test\")?;\n\t\tFile::create(root.join(ignore_file_path))?\n\t\t\t.write_all(b\"foo\")?;\n\n\t\tadd_to_ignore(repo_path, file_path.to_str().unwrap())?;\n\n\t\tlet mut lines =\n\t\t\tread_lines(root.join(ignore_file_path)).unwrap();\n\t\tassert_eq!(&lines.nth(1).unwrap().unwrap(), \"foo.txt\");\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_ignore_ignore() {\n\t\tlet ignore_file_path = Path::new(\".gitignore\");\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\trepo_write_file(&repo, \".gitignore\", \"#foo\").unwrap();\n\n\t\tlet res = add_to_ignore(repo_path, \".gitignore\");\n\t\tassert!(res.is_err());\n\n\t\tlet lines = read_lines(root.join(ignore_file_path)).unwrap();\n\t\tassert_eq!(lines.count(), 1);\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/logwalker.rs",
    "content": "use super::{CommitId, SharedCommitFilterFn};\nuse crate::error::Result;\nuse git2::{Commit, Oid, Repository};\nuse gix::revision::Walk;\nuse std::{\n\tcmp::Ordering,\n\tcollections::{BinaryHeap, HashSet},\n};\n\nstruct TimeOrderedCommit<'a>(Commit<'a>);\n\nimpl Eq for TimeOrderedCommit<'_> {}\n\nimpl PartialEq for TimeOrderedCommit<'_> {\n\tfn eq(&self, other: &Self) -> bool {\n\t\tself.0.time().eq(&other.0.time())\n\t}\n}\n\nimpl PartialOrd for TimeOrderedCommit<'_> {\n\tfn partial_cmp(&self, other: &Self) -> Option<Ordering> {\n\t\tSome(self.cmp(other))\n\t}\n}\n\nimpl Ord for TimeOrderedCommit<'_> {\n\tfn cmp(&self, other: &Self) -> Ordering {\n\t\tself.0.time().cmp(&other.0.time())\n\t}\n}\n\n///\npub struct LogWalker<'a> {\n\tcommits: BinaryHeap<TimeOrderedCommit<'a>>,\n\tvisited: HashSet<Oid>,\n\tlimit: usize,\n\trepo: &'a Repository,\n\tfilter: Option<SharedCommitFilterFn>,\n}\n\nimpl<'a> LogWalker<'a> {\n\t///\n\tpub fn new(repo: &'a Repository, limit: usize) -> Result<Self> {\n\t\tlet c = repo.head()?.peel_to_commit()?;\n\n\t\tlet mut commits = BinaryHeap::with_capacity(10);\n\t\tcommits.push(TimeOrderedCommit(c));\n\n\t\tOk(Self {\n\t\t\tcommits,\n\t\t\tlimit,\n\t\t\tvisited: HashSet::with_capacity(1000),\n\t\t\trepo,\n\t\t\tfilter: None,\n\t\t})\n\t}\n\n\t///\n\tpub fn visited(&self) -> usize {\n\t\tself.visited.len()\n\t}\n\n\t///\n\t#[must_use]\n\tpub fn filter(\n\t\tself,\n\t\tfilter: Option<SharedCommitFilterFn>,\n\t) -> Self {\n\t\tSelf { filter, ..self }\n\t}\n\n\t///\n\tpub fn read(&mut self, out: &mut Vec<CommitId>) -> Result<usize> {\n\t\tlet mut count = 0_usize;\n\n\t\twhile let Some(c) = self.commits.pop() {\n\t\t\tfor p in c.0.parents() {\n\t\t\t\tself.visit(p);\n\t\t\t}\n\n\t\t\tlet id: CommitId = c.0.id().into();\n\t\t\tlet commit_should_be_included =\n\t\t\t\tif let Some(ref filter) = self.filter {\n\t\t\t\t\tfilter(self.repo, &id)?\n\t\t\t\t} else {\n\t\t\t\t\ttrue\n\t\t\t\t};\n\n\t\t\tif commit_should_be_included {\n\t\t\t\tout.push(id);\n\t\t\t}\n\n\t\t\tcount += 1;\n\t\t\tif count == self.limit {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tOk(count)\n\t}\n\n\t//\n\tfn visit(&mut self, c: Commit<'a>) {\n\t\tif self.visited.insert(c.id()) {\n\t\t\tself.commits.push(TimeOrderedCommit(c));\n\t\t}\n\t}\n}\n\n/// This is separate from `LogWalker` because filtering currently (June 2024) works through\n/// `SharedCommitFilterFn`.\n///\n/// `SharedCommitFilterFn` requires access to a `git2::repo::Repository` because, under the hood,\n/// it calls into functions that work with a `git2::repo::Repository`. It seems unwise to open a\n/// repo both through `gix::discover` and `Repository::open_ext` at the same time, so there is a\n/// separate struct that works with `gix::Repository` only.\n///\n/// A more long-term option is to refactor filtering to work with a `gix::Repository` and to remove\n/// `LogWalker` once this is done, but this is a larger effort.\npub struct LogWalkerWithoutFilter<'a> {\n\twalk: Walk<'a>,\n\tlimit: usize,\n\tvisited: usize,\n}\n\nimpl<'a> LogWalkerWithoutFilter<'a> {\n\t///\n\tpub fn new(\n\t\trepo: &'a mut gix::Repository,\n\t\tlimit: usize,\n\t) -> Result<Self> {\n\t\t// This seems to be an object cache size that yields optimal performance. There’s no specific\n\t\t// reason this is 2^14, so benchmarking might reveal that there’s better values.\n\t\trepo.object_cache_size_if_unset(2_usize.pow(14));\n\n\t\tlet commit = repo.head()?.peel_to_commit()?;\n\n\t\tlet tips = [commit.id];\n\n\t\tlet platform = repo\n\t\t\t.rev_walk(tips)\n\t\t\t.sorting(gix::revision::walk::Sorting::ByCommitTime(gix::traverse::commit::simple::CommitTimeOrder::NewestFirst))\n\t\t\t.use_commit_graph(false);\n\n\t\tlet walk = platform.all()?;\n\n\t\tOk(Self {\n\t\t\twalk,\n\t\t\tlimit,\n\t\t\tvisited: 0,\n\t\t})\n\t}\n\n\t///\n\tpub const fn visited(&self) -> usize {\n\t\tself.visited\n\t}\n\n\t///\n\tpub fn read(&mut self, out: &mut Vec<CommitId>) -> Result<usize> {\n\t\tlet mut count = 0_usize;\n\n\t\twhile let Some(Ok(info)) = self.walk.next() {\n\t\t\tout.push(info.id.into());\n\n\t\t\tcount += 1;\n\n\t\t\tif count == self.limit {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tself.visited += count;\n\n\t\tOk(count)\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::error::Result;\n\tuse crate::sync::commit_filter::{SearchFields, SearchOptions};\n\tuse crate::sync::repository::gix_repo;\n\tuse crate::sync::tests::write_commit_file;\n\tuse crate::sync::{\n\t\tcommit, get_commits_info, stage_add_file,\n\t\ttests::repo_init_empty,\n\t};\n\tuse crate::sync::{\n\t\tdiff_contains_file, filter_commit_by_search, LogFilterSearch,\n\t\tLogFilterSearchOptions, RepoPath,\n\t};\n\tuse pretty_assertions::assert_eq;\n\tuse std::{fs::File, io::Write, path::Path};\n\n\t#[test]\n\tfn test_limit() -> Result<()> {\n\t\tlet file_path = Path::new(\"foo\");\n\t\tlet (_td, repo) = repo_init_empty().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(file_path))?.write_all(b\"a\")?;\n\t\tstage_add_file(repo_path, file_path).unwrap();\n\t\tcommit(repo_path, \"commit1\").unwrap();\n\t\tFile::create(root.join(file_path))?.write_all(b\"a\")?;\n\t\tstage_add_file(repo_path, file_path).unwrap();\n\t\tlet oid2 = commit(repo_path, \"commit2\").unwrap();\n\n\t\tlet mut items = Vec::new();\n\t\tlet mut walk = LogWalker::new(&repo, 1)?;\n\t\twalk.read(&mut items).unwrap();\n\n\t\tassert_eq!(items.len(), 1);\n\t\tassert_eq!(items[0], oid2);\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_logwalker() -> Result<()> {\n\t\tlet file_path = Path::new(\"foo\");\n\t\tlet (_td, repo) = repo_init_empty().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(file_path))?.write_all(b\"a\")?;\n\t\tstage_add_file(repo_path, file_path).unwrap();\n\t\tcommit(repo_path, \"commit1\").unwrap();\n\t\tFile::create(root.join(file_path))?.write_all(b\"a\")?;\n\t\tstage_add_file(repo_path, file_path).unwrap();\n\t\tlet oid2 = commit(repo_path, \"commit2\").unwrap();\n\n\t\tlet mut items = Vec::new();\n\t\tlet mut walk = LogWalker::new(&repo, 100)?;\n\t\twalk.read(&mut items).unwrap();\n\n\t\tlet info = get_commits_info(repo_path, &items, 50).unwrap();\n\t\tdbg!(&info);\n\n\t\tassert_eq!(items.len(), 2);\n\t\tassert_eq!(items[0], oid2);\n\n\t\tlet mut items = Vec::new();\n\t\twalk.read(&mut items).unwrap();\n\n\t\tassert_eq!(items.len(), 0);\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_logwalker_without_filter() -> Result<()> {\n\t\tlet file_path = Path::new(\"foo\");\n\t\tlet (_td, repo) = repo_init_empty().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(file_path))?.write_all(b\"a\")?;\n\t\tstage_add_file(repo_path, file_path).unwrap();\n\t\tcommit(repo_path, \"commit1\").unwrap();\n\t\tFile::create(root.join(file_path))?.write_all(b\"a\")?;\n\t\tstage_add_file(repo_path, file_path).unwrap();\n\t\tlet oid2 = commit(repo_path, \"commit2\").unwrap();\n\n\t\tlet mut repo: gix::Repository = gix_repo(repo_path)?;\n\t\tlet mut walk = LogWalkerWithoutFilter::new(&mut repo, 100)?;\n\t\tlet mut items = Vec::new();\n\t\tassert!(matches!(walk.read(&mut items), Ok(2)));\n\n\t\tlet info = get_commits_info(repo_path, &items, 50).unwrap();\n\t\tdbg!(&info);\n\n\t\tassert_eq!(items.len(), 2);\n\t\tassert_eq!(items[0], oid2);\n\n\t\tlet mut items = Vec::new();\n\t\tassert!(matches!(walk.read(&mut items), Ok(0)));\n\n\t\tassert_eq!(items.len(), 0);\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_logwalker_with_filter() -> Result<()> {\n\t\tlet file_path = Path::new(\"foo\");\n\t\tlet second_file_path = Path::new(\"baz\");\n\t\tlet (_td, repo) = repo_init_empty().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: RepoPath =\n\t\t\troot.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(file_path))?.write_all(b\"a\")?;\n\t\tstage_add_file(&repo_path, file_path).unwrap();\n\n\t\tlet _first_commit_id = commit(&repo_path, \"commit1\").unwrap();\n\n\t\tFile::create(root.join(second_file_path))?.write_all(b\"a\")?;\n\t\tstage_add_file(&repo_path, second_file_path).unwrap();\n\n\t\tlet second_commit_id = commit(&repo_path, \"commit2\").unwrap();\n\n\t\tFile::create(root.join(file_path))?.write_all(b\"b\")?;\n\t\tstage_add_file(&repo_path, file_path).unwrap();\n\n\t\tlet _third_commit_id = commit(&repo_path, \"commit3\").unwrap();\n\n\t\tlet diff_contains_baz = diff_contains_file(\"baz\".into());\n\n\t\tlet mut items = Vec::new();\n\t\tlet mut walker = LogWalker::new(&repo, 100)?\n\t\t\t.filter(Some(diff_contains_baz));\n\t\twalker.read(&mut items).unwrap();\n\n\t\tassert_eq!(items.len(), 1);\n\t\tassert_eq!(items[0], second_commit_id);\n\n\t\tlet mut items = Vec::new();\n\t\twalker.read(&mut items).unwrap();\n\n\t\tassert_eq!(items.len(), 0);\n\n\t\tlet diff_contains_bar = diff_contains_file(\"bar\".into());\n\n\t\tlet mut items = Vec::new();\n\t\tlet mut walker = LogWalker::new(&repo, 100)?\n\t\t\t.filter(Some(diff_contains_bar));\n\t\twalker.read(&mut items).unwrap();\n\n\t\tassert_eq!(items.len(), 0);\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_logwalker_with_filter_search() {\n\t\tlet (_td, repo) = repo_init_empty().unwrap();\n\n\t\twrite_commit_file(&repo, \"foo\", \"a\", \"commit1\");\n\t\tlet second_commit_id = write_commit_file(\n\t\t\t&repo,\n\t\t\t\"baz\",\n\t\t\t\"a\",\n\t\t\t\"my commit msg (#2)\",\n\t\t);\n\t\twrite_commit_file(&repo, \"foo\", \"b\", \"commit3\");\n\n\t\tlet log_filter = filter_commit_by_search(\n\t\t\tLogFilterSearch::new(LogFilterSearchOptions {\n\t\t\t\tfields: SearchFields::MESSAGE_SUMMARY,\n\t\t\t\toptions: SearchOptions::FUZZY_SEARCH,\n\t\t\t\tsearch_pattern: String::from(\"my msg\"),\n\t\t\t}),\n\t\t);\n\n\t\tlet mut items = Vec::new();\n\t\tlet mut walker = LogWalker::new(&repo, 100)\n\t\t\t.unwrap()\n\t\t\t.filter(Some(log_filter));\n\t\twalker.read(&mut items).unwrap();\n\n\t\tassert_eq!(items.len(), 1);\n\t\tassert_eq!(items[0], second_commit_id);\n\n\t\tlet log_filter = filter_commit_by_search(\n\t\t\tLogFilterSearch::new(LogFilterSearchOptions {\n\t\t\t\tfields: SearchFields::FILENAMES,\n\t\t\t\toptions: SearchOptions::FUZZY_SEARCH,\n\t\t\t\tsearch_pattern: String::from(\"fo\"),\n\t\t\t}),\n\t\t);\n\n\t\tlet mut items = Vec::new();\n\t\tlet mut walker = LogWalker::new(&repo, 100)\n\t\t\t.unwrap()\n\t\t\t.filter(Some(log_filter));\n\t\twalker.read(&mut items).unwrap();\n\n\t\tassert_eq!(items.len(), 2);\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/merge.rs",
    "content": "use crate::{\n\terror::{Error, Result},\n\tsync::{\n\t\tbranch::merge_commit::commit_merge_with_head,\n\t\trebase::{\n\t\t\tabort_rebase, continue_rebase, get_rebase_progress,\n\t\t},\n\t\trepository::repo,\n\t\treset_stage, reset_workdir, CommitId,\n\t},\n};\nuse git2::{BranchType, Commit, MergeOptions, Repository};\nuse scopetime::scope_time;\n\nuse super::{\n\trebase::{RebaseProgress, RebaseState},\n\tRepoPath,\n};\n\n///\npub fn mergehead_ids(repo_path: &RepoPath) -> Result<Vec<CommitId>> {\n\tscope_time!(\"mergehead_ids\");\n\n\tlet mut repo = repo(repo_path)?;\n\n\tlet mut ids: Vec<CommitId> = Vec::new();\n\trepo.mergehead_foreach(|id| {\n\t\tids.push(CommitId::from(*id));\n\t\ttrue\n\t})?;\n\n\tOk(ids)\n}\n\n/// does these steps:\n/// * reset all staged changes,\n/// * revert all changes in workdir\n/// * cleanup repo merge state\npub fn abort_pending_state(repo_path: &RepoPath) -> Result<()> {\n\tscope_time!(\"abort_pending_state\");\n\n\tlet repo = repo(repo_path)?;\n\n\treset_stage(repo_path, \"*\")?;\n\treset_workdir(repo_path, \"*\")?;\n\n\trepo.cleanup_state()?;\n\n\tOk(())\n}\n\n///\npub fn merge_branch(\n\trepo_path: &RepoPath,\n\tbranch: &str,\n\tbranch_type: BranchType,\n) -> Result<()> {\n\tscope_time!(\"merge_branch\");\n\n\tlet repo = repo(repo_path)?;\n\n\tmerge_branch_repo(&repo, branch, branch_type)?;\n\n\tOk(())\n}\n\n///\npub fn rebase_progress(\n\trepo_path: &RepoPath,\n) -> Result<RebaseProgress> {\n\tscope_time!(\"rebase_progress\");\n\n\tlet repo = repo(repo_path)?;\n\n\tget_rebase_progress(&repo)\n}\n\n///\npub fn continue_pending_rebase(\n\trepo_path: &RepoPath,\n) -> Result<RebaseState> {\n\tscope_time!(\"continue_pending_rebase\");\n\n\tlet repo = repo(repo_path)?;\n\n\tcontinue_rebase(&repo)\n}\n\n///\npub fn abort_pending_rebase(repo_path: &RepoPath) -> Result<()> {\n\tscope_time!(\"abort_pending_rebase\");\n\n\tlet repo = repo(repo_path)?;\n\n\tabort_rebase(&repo)\n}\n\n///\npub fn merge_branch_repo(\n\trepo: &Repository,\n\tbranch: &str,\n\tbranch_type: BranchType,\n) -> Result<()> {\n\tlet branch = repo.find_branch(branch, branch_type)?;\n\n\tlet annotated =\n\t\trepo.reference_to_annotated_commit(&branch.into_reference())?;\n\n\tlet (analysis, _) = repo.merge_analysis(&[&annotated])?;\n\n\t//TODO: support merge on unborn\n\tif analysis.is_unborn() {\n\t\treturn Err(Error::Generic(\"head is unborn\".into()));\n\t}\n\n\tlet mut opt = MergeOptions::default();\n\n\trepo.merge(&[&annotated], Some(&mut opt), None)?;\n\n\tOk(())\n}\n\n///\npub fn merge_msg(repo_path: &RepoPath) -> Result<String> {\n\tscope_time!(\"merge_msg\");\n\n\tlet repo = repo(repo_path)?;\n\tlet content = repo.message()?;\n\n\tOk(content)\n}\n\n///\npub fn merge_commit(\n\trepo_path: &RepoPath,\n\tmsg: &str,\n\tids: &[CommitId],\n) -> Result<CommitId> {\n\tscope_time!(\"merge_commit\");\n\n\tlet repo = repo(repo_path)?;\n\n\tlet mut commits: Vec<Commit> = Vec::new();\n\n\tfor id in ids {\n\t\tcommits.push(repo.find_commit((*id).into())?);\n\t}\n\n\tlet id = commit_merge_with_head(&repo, &commits, msg)?;\n\n\tOk(id)\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::sync::{\n\t\tcreate_branch,\n\t\ttests::{repo_init, write_commit_file},\n\t\tRepoPath,\n\t};\n\tuse pretty_assertions::assert_eq;\n\n\t#[test]\n\tfn test_smoke() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tlet c1 =\n\t\t\twrite_commit_file(&repo, \"test.txt\", \"test\", \"commit1\");\n\n\t\tcreate_branch(repo_path, \"foo\").unwrap();\n\n\t\twrite_commit_file(&repo, \"test.txt\", \"test2\", \"commit2\");\n\n\t\tmerge_branch(repo_path, \"master\", BranchType::Local).unwrap();\n\n\t\tlet msg = merge_msg(repo_path).unwrap();\n\n\t\tassert_eq!(&msg[0..12], \"Merge branch\");\n\n\t\tlet mergeheads = mergehead_ids(repo_path).unwrap();\n\n\t\tassert_eq!(mergeheads[0], c1);\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/mod.rs",
    "content": "//! sync git api\n\n//TODO: remove once we have this activated on the toplevel\n#![deny(clippy::expect_used)]\n\npub mod blame;\npub mod branch;\npub mod commit;\nmod commit_details;\npub mod commit_files;\nmod commit_filter;\nmod commit_revert;\nmod commits_info;\nmod config;\npub mod cred;\npub mod diff;\nmod hooks;\nmod hunks;\nmod ignore;\nmod logwalker;\nmod merge;\nmod patches;\nmod rebase;\npub mod remotes;\nmod repository;\nmod reset;\nmod reword;\npub mod sign;\nmod staging;\nmod stash;\nmod state;\npub mod status;\nmod submodules;\nmod tags;\nmod tree;\npub mod utils;\n\npub use blame::{blame_file, BlameHunk, FileBlame};\npub use branch::{\n\tbranch_compare_upstream, checkout_branch, checkout_commit,\n\tconfig_is_pull_rebase, create_branch, delete_branch,\n\tget_branch_remote, get_branch_upstream_merge, get_branches_info,\n\tmerge_commit::merge_upstream_commit,\n\tmerge_ff::branch_merge_upstream_fastforward,\n\tmerge_rebase::merge_upstream_rebase, rename::rename_branch,\n\tvalidate_branch_name, BranchCompare, BranchDetails, BranchInfo,\n};\npub use commit::{amend, commit, tag_commit};\npub use commit_details::{\n\tget_commit_details, CommitDetails, CommitMessage, CommitSignature,\n};\npub use commit_files::get_commit_files;\npub use commit_filter::{\n\tdiff_contains_file, filter_commit_by_search, LogFilterSearch,\n\tLogFilterSearchOptions, SearchFields, SearchOptions,\n\tSharedCommitFilterFn,\n};\npub use commit_revert::{commit_revert, revert_commit, revert_head};\npub use commits_info::{\n\tget_commit_info, get_commits_info, CommitId, CommitInfo,\n};\npub use config::{\n\tget_config_string, untracked_files_config,\n\tShowUntrackedFilesConfig,\n};\npub use diff::get_diff_commit;\npub use git2::BranchType;\npub use hooks::{\n\thooks_commit_msg, hooks_post_commit, hooks_pre_commit,\n\thooks_pre_push, hooks_prepare_commit_msg, HookResult,\n\tPrePushTarget, PrepareCommitMsgSource,\n};\npub use hunks::{reset_hunk, stage_hunk, unstage_hunk};\npub use ignore::add_to_ignore;\npub use logwalker::{LogWalker, LogWalkerWithoutFilter};\npub use merge::{\n\tabort_pending_rebase, abort_pending_state,\n\tcontinue_pending_rebase, merge_branch, merge_commit, merge_msg,\n\tmergehead_ids, rebase_progress,\n};\npub use rebase::rebase_branch;\npub use remotes::{\n\tadd_remote, delete_remote, get_default_remote,\n\tget_default_remote_for_fetch, get_default_remote_for_push,\n\tget_remote_url, get_remotes, push::AsyncProgress, rename_remote,\n\ttags::PushTagsProgress, update_remote_url, validate_remote_name,\n};\npub(crate) use repository::{gix_repo, repo};\npub use repository::{RepoPath, RepoPathRef};\npub use reset::{reset_repo, reset_stage, reset_workdir};\npub use reword::reword;\npub use staging::{discard_lines, stage_lines};\npub use stash::{\n\tget_stashes, stash_apply, stash_drop, stash_pop, stash_save,\n};\npub use state::{repo_state, RepoState};\npub use status::is_workdir_clean;\npub use submodules::{\n\tget_submodules, submodule_parent_info, update_submodule,\n\tSubmoduleInfo, SubmoduleParentInfo, SubmoduleStatus,\n};\npub use tags::{\n\tdelete_tag, get_tags, get_tags_with_metadata, CommitTags, Tag,\n\tTagWithMetadata, Tags,\n};\npub use tree::{tree_file_content, tree_files, TreeFile};\npub use utils::{\n\tget_head, get_head_tuple, repo_dir, repo_open_error,\n\tstage_add_all, stage_add_file, stage_addremoved, Head,\n};\n\npub use git2::ResetType;\n\n/// test utils\n#[cfg(test)]\npub mod tests {\n\tuse super::{\n\t\tcommit,\n\t\trepository::repo,\n\t\tstage_add_file,\n\t\tstatus::{get_status, StatusType},\n\t\tutils::{get_head_repo, repo_write_file},\n\t\tCommitId, LogWalker, RepoPath,\n\t};\n\tuse crate::error::Result;\n\tuse git2::Repository;\n\tuse std::{ffi::OsStr, path::Path, process::Command};\n\tuse tempfile::TempDir;\n\n\t///\n\tpub fn repo_init_empty() -> Result<(TempDir, Repository)> {\n\t\tinit_log();\n\n\t\tsandbox_config_files();\n\n\t\tlet td = TempDir::new()?;\n\t\tlet repo = Repository::init(td.path())?;\n\t\t{\n\t\t\tlet mut config = repo.config()?;\n\t\t\tconfig.set_str(\"user.name\", \"name\")?;\n\t\t\tconfig.set_str(\"user.email\", \"email\")?;\n\t\t}\n\t\tOk((td, repo))\n\t}\n\n\t///\n\tpub fn repo_init() -> Result<(TempDir, Repository)> {\n\t\trepo_init_with_prefix(\"gitui\")\n\t}\n\n\t///\n\t#[inline]\n\tpub fn repo_init_with_prefix(\n\t\tprefix: impl AsRef<OsStr>,\n\t) -> Result<(TempDir, Repository)> {\n\t\tinit_log();\n\n\t\tsandbox_config_files();\n\n\t\tlet td = TempDir::with_prefix(prefix)?;\n\t\tlet repo = Repository::init(td.path())?;\n\t\t{\n\t\t\tlet mut config = repo.config()?;\n\t\t\tconfig.set_str(\"user.name\", \"name\")?;\n\t\t\tconfig.set_str(\"user.email\", \"email\")?;\n\n\t\t\tlet mut index = repo.index()?;\n\t\t\tlet id = index.write_tree()?;\n\n\t\t\tlet tree = repo.find_tree(id)?;\n\t\t\tlet sig = repo.signature()?;\n\t\t\trepo.commit(\n\t\t\t\tSome(\"HEAD\"),\n\t\t\t\t&sig,\n\t\t\t\t&sig,\n\t\t\t\t\"initial\",\n\t\t\t\t&tree,\n\t\t\t\t&[],\n\t\t\t)?;\n\t\t}\n\t\tOk((td, repo))\n\t}\n\n\t///\n\tpub fn repo_clone(p: &str) -> Result<(TempDir, Repository)> {\n\t\tsandbox_config_files();\n\n\t\tlet td = TempDir::new()?;\n\n\t\tlet td_path = td.path().as_os_str().to_str().unwrap();\n\n\t\tlet repo = Repository::clone(p, td_path).unwrap();\n\n\t\tlet mut config = repo.config()?;\n\t\tconfig.set_str(\"user.name\", \"name\")?;\n\t\tconfig.set_str(\"user.email\", \"email\")?;\n\n\t\tOk((td, repo))\n\t}\n\n\t/// write, stage and commit a file\n\tpub fn write_commit_file(\n\t\trepo: &Repository,\n\t\tfile: &str,\n\t\tcontent: &str,\n\t\tcommit_name: &str,\n\t) -> CommitId {\n\t\trepo_write_file(repo, file, content).unwrap();\n\n\t\tstage_add_file(\n\t\t\t&repo.workdir().unwrap().to_str().unwrap().into(),\n\t\t\tPath::new(file),\n\t\t)\n\t\t.unwrap();\n\n\t\tcommit(\n\t\t\t&repo.workdir().unwrap().to_str().unwrap().into(),\n\t\t\tcommit_name,\n\t\t)\n\t\t.unwrap()\n\t}\n\n\t/// write, stage and commit a file giving the commit a specific timestamp\n\tpub fn write_commit_file_at(\n\t\trepo: &Repository,\n\t\tfile: &str,\n\t\tcontent: &str,\n\t\tcommit_name: &str,\n\t\ttime: git2::Time,\n\t) -> CommitId {\n\t\trepo_write_file(repo, file, content).unwrap();\n\n\t\tlet path: &RepoPath =\n\t\t\t&repo.workdir().unwrap().to_str().unwrap().into();\n\n\t\tstage_add_file(path, Path::new(file)).unwrap();\n\n\t\tcommit_at(path, commit_name, time)\n\t}\n\n\t/// helper returning amount of files with changes in the (wd,stage)\n\tpub fn get_statuses(repo_path: &RepoPath) -> (usize, usize) {\n\t\t(\n\t\t\tget_status(repo_path, StatusType::WorkingDir, None)\n\t\t\t\t.unwrap()\n\t\t\t\t.len(),\n\t\t\tget_status(repo_path, StatusType::Stage, None)\n\t\t\t\t.unwrap()\n\t\t\t\t.len(),\n\t\t)\n\t}\n\n\t///\n\tpub fn debug_cmd_print(path: &RepoPath, cmd: &str) {\n\t\tlet cmd = debug_cmd(path, cmd);\n\t\teprintln!(\"\\n----\\n{cmd}\");\n\t}\n\n\t/// helper to fetch commit details using log walker\n\tpub fn get_commit_ids(\n\t\tr: &Repository,\n\t\tmax_count: usize,\n\t) -> Vec<CommitId> {\n\t\tlet mut commit_ids = Vec::<CommitId>::new();\n\t\tLogWalker::new(r, max_count)\n\t\t\t.unwrap()\n\t\t\t.read(&mut commit_ids)\n\t\t\t.unwrap();\n\n\t\tcommit_ids\n\t}\n\n\t/// Same as `repo_init`, but the repo is a bare repo (--bare)\n\tpub fn repo_init_bare() -> Result<(TempDir, Repository)> {\n\t\tinit_log();\n\n\t\tlet tmp_repo_dir = TempDir::new()?;\n\t\tlet bare_repo = Repository::init_bare(tmp_repo_dir.path())?;\n\t\tOk((tmp_repo_dir, bare_repo))\n\t}\n\n\t/// Calling `set_search_path` with an empty directory makes sure that there\n\t/// is no git config interfering with our tests (for example user-local\n\t/// `.gitconfig`).\n\t#[allow(unsafe_code)]\n\tfn sandbox_config_files() {\n\t\tuse git2::{opts::set_search_path, ConfigLevel};\n\t\tuse std::sync::Once;\n\n\t\tstatic INIT: Once = Once::new();\n\n\t\t// Adapted from https://github.com/rust-lang/cargo/pull/9035\n\t\tINIT.call_once(|| unsafe {\n\t\t\tlet temp_dir = TempDir::new().unwrap();\n\t\t\tlet path = temp_dir.path();\n\n\t\t\tset_search_path(ConfigLevel::System, path).unwrap();\n\t\t\tset_search_path(ConfigLevel::Global, path).unwrap();\n\t\t\tset_search_path(ConfigLevel::XDG, path).unwrap();\n\t\t\tset_search_path(ConfigLevel::ProgramData, path).unwrap();\n\t\t});\n\t}\n\n\tfn commit_at(\n\t\trepo_path: &RepoPath,\n\t\tmsg: &str,\n\t\ttime: git2::Time,\n\t) -> CommitId {\n\t\tlet repo = repo(repo_path).unwrap();\n\n\t\tlet signature =\n\t\t\tgit2::Signature::new(\"name\", \"email\", &time).unwrap();\n\t\tlet mut index = repo.index().unwrap();\n\t\tlet tree_id = index.write_tree().unwrap();\n\t\tlet tree = repo.find_tree(tree_id).unwrap();\n\n\t\tlet parents = if let Ok(id) = get_head_repo(&repo) {\n\t\t\tvec![repo.find_commit(id.into()).unwrap()]\n\t\t} else {\n\t\t\tVec::new()\n\t\t};\n\n\t\tlet parents = parents.iter().collect::<Vec<_>>();\n\n\t\tlet commit = repo\n\t\t\t.commit(\n\t\t\t\tSome(\"HEAD\"),\n\t\t\t\t&signature,\n\t\t\t\t&signature,\n\t\t\t\tmsg,\n\t\t\t\t&tree,\n\t\t\t\tparents.as_slice(),\n\t\t\t)\n\t\t\t.unwrap()\n\t\t\t.into();\n\n\t\tcommit\n\t}\n\n\t// init log\n\tfn init_log() {\n\t\tlet _ = env_logger::builder()\n\t\t\t.is_test(true)\n\t\t\t.filter_level(log::LevelFilter::Trace)\n\t\t\t.try_init();\n\t}\n\n\tfn debug_cmd(path: &RepoPath, cmd: &str) -> String {\n\t\tlet output = if cfg!(target_os = \"windows\") {\n\t\t\tCommand::new(\"cmd\")\n\t\t\t\t.args([\"/C\", cmd])\n\t\t\t\t.current_dir(path.gitpath())\n\t\t\t\t.output()\n\t\t\t\t.unwrap()\n\t\t} else {\n\t\t\tCommand::new(\"sh\")\n\t\t\t\t.arg(\"-c\")\n\t\t\t\t.arg(cmd)\n\t\t\t\t.current_dir(path.gitpath())\n\t\t\t\t.output()\n\t\t\t\t.unwrap()\n\t\t};\n\n\t\tlet stdout = String::from_utf8_lossy(&output.stdout);\n\t\tlet stderr = String::from_utf8_lossy(&output.stderr);\n\t\tformat!(\n\t\t\t\"{}{}\",\n\t\t\tif stdout.is_empty() {\n\t\t\t\tString::new()\n\t\t\t} else {\n\t\t\t\tformat!(\"out:\\n{stdout}\")\n\t\t\t},\n\t\t\tif stderr.is_empty() {\n\t\t\t\tString::new()\n\t\t\t} else {\n\t\t\t\tformat!(\"err:\\n{stderr}\")\n\t\t\t}\n\t\t)\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/patches.rs",
    "content": "use super::diff::{get_diff_raw, DiffOptions, HunkHeader};\nuse crate::error::{Error, Result};\nuse git2::{Diff, DiffLine, Patch, Repository};\n\npub struct HunkLines<'a> {\n\tpub hunk: HunkHeader,\n\tpub lines: Vec<DiffLine<'a>>,\n}\n\npub fn get_file_diff_patch<'a>(\n\trepo: &'a Repository,\n\tfile: &str,\n\tis_staged: bool,\n\treverse: bool,\n) -> Result<Patch<'a>> {\n\tlet diff = get_diff_raw(\n\t\trepo,\n\t\tfile,\n\t\tis_staged,\n\t\treverse,\n\t\tSome(DiffOptions {\n\t\t\tcontext: 1,\n\t\t\t..DiffOptions::default()\n\t\t}),\n\t)?;\n\tlet patches = get_patches(&diff)?;\n\tif patches.len() > 1 {\n\t\treturn Err(Error::Generic(String::from(\"patch error\")));\n\t}\n\n\tlet patch = patches.into_iter().next().ok_or_else(|| {\n\t\tError::Generic(String::from(\"no patch found\"))\n\t})?;\n\n\tOk(patch)\n}\n\n//\npub fn patch_get_hunklines<'a>(\n\tpatch: &'a Patch<'a>,\n) -> Result<Vec<HunkLines<'a>>> {\n\tlet count_hunks = patch.num_hunks();\n\tlet mut res = Vec::with_capacity(count_hunks);\n\tfor hunk_idx in 0..count_hunks {\n\t\tlet (hunk, _) = patch.hunk(hunk_idx)?;\n\n\t\tlet count_lines = patch.num_lines_in_hunk(hunk_idx)?;\n\n\t\tlet mut hunk = HunkLines {\n\t\t\thunk: HunkHeader::from(hunk),\n\t\t\tlines: Vec::with_capacity(count_lines),\n\t\t};\n\n\t\tfor line_idx in 0..count_lines {\n\t\t\tlet line = patch.line_in_hunk(hunk_idx, line_idx)?;\n\t\t\thunk.lines.push(line);\n\t\t}\n\n\t\tres.push(hunk);\n\t}\n\n\tOk(res)\n}\n\n//\nfn get_patches<'a>(diff: &Diff<'a>) -> Result<Vec<Patch<'a>>> {\n\tlet count = diff.deltas().len();\n\n\tlet mut res = Vec::with_capacity(count);\n\tfor idx in 0..count {\n\t\tlet p = Patch::from_diff(diff, idx)?;\n\t\tif let Some(p) = p {\n\t\t\tres.push(p);\n\t\t}\n\t}\n\n\tOk(res)\n}\n"
  },
  {
    "path": "asyncgit/src/sync/rebase.rs",
    "content": "use git2::{BranchType, Repository};\nuse scopetime::scope_time;\n\nuse crate::{\n\terror::{Error, Result},\n\tsync::repository::repo,\n};\n\nuse super::{CommitId, RepoPath};\n\n/// rebase current HEAD on `branch`\npub fn rebase_branch(\n\trepo_path: &RepoPath,\n\tbranch: &str,\n\tbranch_type: BranchType,\n) -> Result<RebaseState> {\n\tscope_time!(\"rebase_branch\");\n\n\tlet repo = repo(repo_path)?;\n\n\trebase_branch_repo(&repo, branch, branch_type)\n}\n\nfn rebase_branch_repo(\n\trepo: &Repository,\n\tbranch_name: &str,\n\tbranch_type: BranchType,\n) -> Result<RebaseState> {\n\tlet branch = repo.find_branch(branch_name, branch_type)?;\n\n\tlet annotated =\n\t\trepo.reference_to_annotated_commit(&branch.into_reference())?;\n\n\trebase(repo, &annotated)\n}\n\n/// rebase attempt which aborts and undo's rebase if any conflict appears\npub fn conflict_free_rebase(\n\trepo: &git2::Repository,\n\tcommit: &git2::AnnotatedCommit,\n) -> Result<CommitId> {\n\tlet mut rebase = repo.rebase(None, Some(commit), None, None)?;\n\tlet signature =\n\t\tcrate::sync::commit::signature_allow_undefined_name(repo)?;\n\tlet mut last_commit = None;\n\twhile let Some(op) = rebase.next() {\n\t\tlet _op = op?;\n\n\t\tif repo.index()?.has_conflicts() {\n\t\t\trebase.abort()?;\n\t\t\treturn Err(Error::RebaseConflict);\n\t\t}\n\n\t\tlet c = rebase.commit(None, &signature, None)?;\n\n\t\tlast_commit = Some(CommitId::from(c));\n\t}\n\n\tif repo.index()?.has_conflicts() {\n\t\trebase.abort()?;\n\t\treturn Err(Error::RebaseConflict);\n\t}\n\n\trebase.finish(Some(&signature))?;\n\n\tlast_commit.ok_or_else(|| {\n\t\tError::Generic(String::from(\"no commit rebased\"))\n\t})\n}\n\n///\n#[derive(PartialEq, Eq, Debug)]\npub enum RebaseState {\n\t///\n\tFinished,\n\t///\n\tConflicted,\n}\n\n/// rebase\npub fn rebase(\n\trepo: &git2::Repository,\n\tcommit: &git2::AnnotatedCommit,\n) -> Result<RebaseState> {\n\tlet mut rebase = repo.rebase(None, Some(commit), None, None)?;\n\tlet signature =\n\t\tcrate::sync::commit::signature_allow_undefined_name(repo)?;\n\n\twhile let Some(op) = rebase.next() {\n\t\tlet _op = op?;\n\t\t// dbg!(op.id());\n\n\t\tif repo.index()?.has_conflicts() {\n\t\t\treturn Ok(RebaseState::Conflicted);\n\t\t}\n\n\t\trebase.commit(None, &signature, None)?;\n\t}\n\n\tif repo.index()?.has_conflicts() {\n\t\treturn Ok(RebaseState::Conflicted);\n\t}\n\n\trebase.finish(Some(&signature))?;\n\n\tOk(RebaseState::Finished)\n}\n\n/// continue pending rebase\npub fn continue_rebase(\n\trepo: &git2::Repository,\n) -> Result<RebaseState> {\n\tlet mut rebase = repo.open_rebase(None)?;\n\tlet signature =\n\t\tcrate::sync::commit::signature_allow_undefined_name(repo)?;\n\n\tif repo.index()?.has_conflicts() {\n\t\treturn Ok(RebaseState::Conflicted);\n\t}\n\n\t// try commit current rebase step\n\tif !repo.index()?.is_empty() {\n\t\trebase.commit(None, &signature, None)?;\n\t}\n\n\twhile let Some(op) = rebase.next() {\n\t\tlet _op = op?;\n\t\t// dbg!(op.id());\n\n\t\tif repo.index()?.has_conflicts() {\n\t\t\treturn Ok(RebaseState::Conflicted);\n\t\t}\n\n\t\trebase.commit(None, &signature, None)?;\n\t}\n\n\tif repo.index()?.has_conflicts() {\n\t\treturn Ok(RebaseState::Conflicted);\n\t}\n\n\trebase.finish(Some(&signature))?;\n\n\tOk(RebaseState::Finished)\n}\n\n///\n#[derive(PartialEq, Eq, Debug)]\npub struct RebaseProgress {\n\t///\n\tpub steps: usize,\n\t///\n\tpub current: usize,\n\t///\n\tpub current_commit: Option<CommitId>,\n}\n\n///\npub fn get_rebase_progress(\n\trepo: &git2::Repository,\n) -> Result<RebaseProgress> {\n\tlet mut rebase = repo.open_rebase(None)?;\n\n\tlet current_commit: Option<CommitId> = rebase\n\t\t.operation_current()\n\t\t.and_then(|idx| rebase.nth(idx))\n\t\t.map(|op| op.id().into());\n\n\tlet progress = RebaseProgress {\n\t\tsteps: rebase.len(),\n\t\tcurrent: rebase.operation_current().unwrap_or_default(),\n\t\tcurrent_commit,\n\t};\n\n\tOk(progress)\n}\n\n///\npub fn abort_rebase(repo: &git2::Repository) -> Result<()> {\n\tlet mut rebase = repo.open_rebase(None)?;\n\n\trebase.abort()?;\n\n\tOk(())\n}\n\n#[cfg(test)]\nmod test_conflict_free_rebase {\n\tuse crate::sync::{\n\t\tcheckout_branch, create_branch,\n\t\trebase::{rebase_branch, RebaseState},\n\t\trepo_state,\n\t\trepository::repo,\n\t\ttests::{repo_init, write_commit_file},\n\t\tCommitId, RepoPath, RepoState,\n\t};\n\tuse git2::{BranchType, Repository};\n\n\tuse super::conflict_free_rebase;\n\n\tfn parent_ids(repo: &Repository, c: CommitId) -> Vec<CommitId> {\n\t\tlet foo = repo\n\t\t\t.find_commit(c.into())\n\t\t\t.unwrap()\n\t\t\t.parent_ids()\n\t\t\t.map(CommitId::from)\n\t\t\t.collect();\n\n\t\tfoo\n\t}\n\n\t///\n\tfn test_rebase_branch_repo(\n\t\trepo_path: &RepoPath,\n\t\tbranch_name: &str,\n\t) -> CommitId {\n\t\tlet repo = repo(repo_path).unwrap();\n\n\t\tlet branch =\n\t\t\trepo.find_branch(branch_name, BranchType::Local).unwrap();\n\n\t\tlet annotated = repo\n\t\t\t.reference_to_annotated_commit(&branch.into_reference())\n\t\t\t.unwrap();\n\n\t\tconflict_free_rebase(&repo, &annotated).unwrap()\n\t}\n\n\t#[test]\n\tfn test_smoke() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tlet c1 =\n\t\t\twrite_commit_file(&repo, \"test1.txt\", \"test\", \"commit1\");\n\n\t\tcreate_branch(repo_path, \"foo\").unwrap();\n\n\t\tlet c2 =\n\t\t\twrite_commit_file(&repo, \"test2.txt\", \"test\", \"commit2\");\n\n\t\tassert_eq!(parent_ids(&repo, c2), vec![c1]);\n\n\t\tcheckout_branch(repo_path, \"master\").unwrap();\n\n\t\tlet c3 =\n\t\t\twrite_commit_file(&repo, \"test3.txt\", \"test\", \"commit3\");\n\n\t\tcheckout_branch(repo_path, \"foo\").unwrap();\n\n\t\tlet r = test_rebase_branch_repo(repo_path, \"master\");\n\n\t\tassert_eq!(parent_ids(&repo, r), vec![c3]);\n\t}\n\n\t#[test]\n\tfn test_conflict() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\twrite_commit_file(&repo, \"test.txt\", \"test1\", \"commit1\");\n\n\t\tcreate_branch(repo_path, \"foo\").unwrap();\n\n\t\twrite_commit_file(&repo, \"test.txt\", \"test2\", \"commit2\");\n\n\t\tcheckout_branch(repo_path, \"master\").unwrap();\n\n\t\twrite_commit_file(&repo, \"test.txt\", \"test3\", \"commit3\");\n\n\t\tcheckout_branch(repo_path, \"foo\").unwrap();\n\n\t\tlet res =\n\t\t\trebase_branch(repo_path, \"master\", BranchType::Local);\n\n\t\tassert!(matches!(res.unwrap(), RebaseState::Conflicted));\n\n\t\tassert_eq!(repo_state(repo_path).unwrap(), RepoState::Rebase);\n\t}\n}\n\n#[cfg(test)]\nmod test_rebase {\n\tuse crate::sync::{\n\t\tcheckout_branch, create_branch,\n\t\trebase::{\n\t\t\tabort_rebase, get_rebase_progress, RebaseProgress,\n\t\t\tRebaseState,\n\t\t},\n\t\trebase_branch, repo_state,\n\t\ttests::{repo_init, write_commit_file},\n\t\tRepoPath, RepoState,\n\t};\n\tuse git2::BranchType;\n\n\t#[test]\n\tfn test_conflicted_abort() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\twrite_commit_file(&repo, \"test.txt\", \"test1\", \"commit1\");\n\n\t\tcreate_branch(repo_path, \"foo\").unwrap();\n\n\t\tlet c =\n\t\t\twrite_commit_file(&repo, \"test.txt\", \"test2\", \"commit2\");\n\n\t\tcheckout_branch(repo_path, \"master\").unwrap();\n\n\t\twrite_commit_file(&repo, \"test.txt\", \"test3\", \"commit3\");\n\n\t\tcheckout_branch(repo_path, \"foo\").unwrap();\n\n\t\tassert!(get_rebase_progress(&repo).is_err());\n\n\t\t// rebase\n\n\t\tlet r = rebase_branch(repo_path, \"master\", BranchType::Local)\n\t\t\t.unwrap();\n\n\t\tassert_eq!(r, RebaseState::Conflicted);\n\t\tassert_eq!(repo_state(repo_path).unwrap(), RepoState::Rebase);\n\t\tassert_eq!(\n\t\t\tget_rebase_progress(&repo).unwrap(),\n\t\t\tRebaseProgress {\n\t\t\t\tcurrent: 0,\n\t\t\t\tsteps: 1,\n\t\t\t\tcurrent_commit: Some(c)\n\t\t\t}\n\t\t);\n\n\t\t// abort\n\n\t\tabort_rebase(&repo).unwrap();\n\n\t\tassert_eq!(repo_state(repo_path).unwrap(), RepoState::Clean);\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/remotes/callbacks.rs",
    "content": "use super::push::ProgressNotification;\nuse crate::{error::Result, sync::cred::BasicAuthCredential};\nuse crossbeam_channel::Sender;\nuse git2::{Cred, Error as GitError, RemoteCallbacks};\nuse std::sync::{\n\tatomic::{AtomicBool, Ordering},\n\tArc, Mutex,\n};\n\n///\n#[derive(Default, Clone)]\npub struct CallbackStats {\n\tpub push_rejected_msg: Option<(String, String)>,\n}\n\n///\n#[derive(Clone)]\npub struct Callbacks {\n\tsender: Option<Sender<ProgressNotification>>,\n\tbasic_credential: Option<BasicAuthCredential>,\n\tstats: Arc<Mutex<CallbackStats>>,\n\tfirst_call_to_credentials: Arc<AtomicBool>,\n}\n\nimpl Callbacks {\n\t///\n\tpub fn new(\n\t\tsender: Option<Sender<ProgressNotification>>,\n\t\tbasic_credential: Option<BasicAuthCredential>,\n\t) -> Self {\n\t\tlet stats = Arc::new(Mutex::new(CallbackStats::default()));\n\n\t\tSelf {\n\t\t\tsender,\n\t\t\tbasic_credential,\n\t\t\tstats,\n\t\t\tfirst_call_to_credentials: Arc::new(AtomicBool::new(\n\t\t\t\ttrue,\n\t\t\t)),\n\t\t}\n\t}\n\n\t///\n\tpub fn get_stats(&self) -> Result<CallbackStats> {\n\t\tlet stats = self.stats.lock()?;\n\t\tOk(stats.clone())\n\t}\n\n\t///\n\tpub fn callbacks<'a>(&self) -> RemoteCallbacks<'a> {\n\t\tlet mut callbacks = RemoteCallbacks::new();\n\n\t\tlet this = self.clone();\n\t\tcallbacks.push_transfer_progress(\n\t\t\tmove |current, total, bytes| {\n\t\t\t\tthis.push_transfer_progress(current, total, bytes);\n\t\t\t},\n\t\t);\n\n\t\tlet this = self.clone();\n\t\tcallbacks.update_tips(move |name, a, b| {\n\t\t\tthis.update_tips(name, a, b);\n\t\t\ttrue\n\t\t});\n\n\t\tlet this = self.clone();\n\t\tcallbacks.transfer_progress(move |p| {\n\t\t\tthis.transfer_progress(&p);\n\t\t\ttrue\n\t\t});\n\n\t\tlet this = self.clone();\n\t\tcallbacks.pack_progress(move |stage, current, total| {\n\t\t\tthis.pack_progress(stage, total, current);\n\t\t});\n\n\t\tlet this = self.clone();\n\t\tcallbacks.push_update_reference(move |reference, msg| {\n\t\t\tthis.push_update_reference(reference, msg);\n\t\t\tOk(())\n\t\t});\n\n\t\tlet this = self.clone();\n\t\tcallbacks.credentials(\n\t\t\tmove |url, username_from_url, allowed_types| {\n\t\t\t\tthis.credentials(\n\t\t\t\t\turl,\n\t\t\t\t\tusername_from_url,\n\t\t\t\t\tallowed_types,\n\t\t\t\t)\n\t\t\t},\n\t\t);\n\n\t\tcallbacks.sideband_progress(move |data| {\n\t\t\tlog::debug!(\n\t\t\t\t\"sideband transfer: '{}'\",\n\t\t\t\tString::from_utf8_lossy(data).trim()\n\t\t\t);\n\t\t\ttrue\n\t\t});\n\n\t\tcallbacks\n\t}\n\n\tfn push_update_reference(\n\t\t&self,\n\t\treference: &str,\n\t\tmsg: Option<&str>,\n\t) {\n\t\tlog::debug!(\"push_update_reference: '{reference}' {msg:?}\");\n\n\t\tif let Ok(mut stats) = self.stats.lock() {\n\t\t\tstats.push_rejected_msg = msg\n\t\t\t\t.map(|msg| (reference.to_string(), msg.to_string()));\n\t\t}\n\t}\n\n\tfn pack_progress(\n\t\t&self,\n\t\tstage: git2::PackBuilderStage,\n\t\ttotal: usize,\n\t\tcurrent: usize,\n\t) {\n\t\tlog::debug!(\"packing: {stage:?} - {current}/{total}\");\n\t\tself.sender.clone().map(|sender| {\n\t\t\tsender.send(ProgressNotification::Packing {\n\t\t\t\tstage,\n\t\t\t\ttotal,\n\t\t\t\tcurrent,\n\t\t\t})\n\t\t});\n\t}\n\n\tfn transfer_progress(&self, p: &git2::Progress) {\n\t\tlog::debug!(\n\t\t\t\"transfer: {}/{}\",\n\t\t\tp.received_objects(),\n\t\t\tp.total_objects()\n\t\t);\n\t\tself.sender.clone().map(|sender| {\n\t\t\tsender.send(ProgressNotification::Transfer {\n\t\t\t\tobjects: p.received_objects(),\n\t\t\t\ttotal_objects: p.total_objects(),\n\t\t\t})\n\t\t});\n\t}\n\n\tfn update_tips(&self, name: &str, a: git2::Oid, b: git2::Oid) {\n\t\tlog::debug!(\"update tips: '{name}' [{a}] [{b}]\");\n\t\tself.sender.clone().map(|sender| {\n\t\t\tsender.send(ProgressNotification::UpdateTips {\n\t\t\t\tname: name.to_string(),\n\t\t\t\ta: a.into(),\n\t\t\t\tb: b.into(),\n\t\t\t})\n\t\t});\n\t}\n\n\tfn push_transfer_progress(\n\t\t&self,\n\t\tcurrent: usize,\n\t\ttotal: usize,\n\t\tbytes: usize,\n\t) {\n\t\tlog::debug!(\"progress: {current}/{total} ({bytes} B)\");\n\t\tself.sender.clone().map(|sender| {\n\t\t\tsender.send(ProgressNotification::PushTransfer {\n\t\t\t\tcurrent,\n\t\t\t\ttotal,\n\t\t\t\tbytes,\n\t\t\t})\n\t\t});\n\t}\n\n\t// 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.\n\t// This behavior is explained in a issue on git2-rs project : https://github.com/rust-lang/git2-rs/issues/347\n\t// An implementation reference is done in cargo : https://github.com/rust-lang/cargo/blob/9fb208dddb12a3081230a5fd8f470e01df8faa25/src/cargo/sources/git/utils.rs#L588\n\t// There is also a guide about libgit2 authentication : https://libgit2.org/docs/guides/authentication/\n\tfn credentials(\n\t\t&self,\n\t\turl: &str,\n\t\tusername_from_url: Option<&str>,\n\t\tallowed_types: git2::CredentialType,\n\t) -> std::result::Result<Cred, GitError> {\n\t\tlog::debug!(\n\t\t\t\"creds: '{url}' {username_from_url:?} ({allowed_types:?})\",\n\t\t);\n\n\t\t// This boolean is used to avoid multiple calls to credentials callback.\n\t\tif self.first_call_to_credentials.load(Ordering::Relaxed) {\n\t\t\tself.first_call_to_credentials\n\t\t\t\t.store(false, Ordering::Relaxed);\n\t\t} else {\n\t\t\treturn Err(GitError::from_str(\"Bad credentials.\"));\n\t\t}\n\n\t\tmatch &self.basic_credential {\n\t\t\t_ if allowed_types.is_ssh_key() => username_from_url\n\t\t\t\t.map_or_else(\n\t\t\t\t\t|| {\n\t\t\t\t\t\tErr(GitError::from_str(\n\t\t\t\t\t\t\t\" Couldn't extract username from url.\",\n\t\t\t\t\t\t))\n\t\t\t\t\t},\n\t\t\t\t\tCred::ssh_key_from_agent,\n\t\t\t\t),\n\t\t\tSome(BasicAuthCredential {\n\t\t\t\tusername: Some(user),\n\t\t\t\tpassword: Some(pwd),\n\t\t\t}) if allowed_types.is_user_pass_plaintext() => {\n\t\t\t\tCred::userpass_plaintext(user, pwd)\n\t\t\t}\n\t\t\tSome(BasicAuthCredential {\n\t\t\t\tusername: Some(user),\n\t\t\t\tpassword: _,\n\t\t\t}) if allowed_types.is_username() => Cred::username(user),\n\t\t\t_ if allowed_types.is_default() => Cred::default(),\n\t\t\t_ => Err(GitError::from_str(\"Couldn't find credentials\")),\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/remotes/mod.rs",
    "content": "//!\n\nmod callbacks;\npub(crate) mod push;\npub(crate) mod tags;\n\nuse crate::{\n\terror::{Error, Result},\n\tsync::{\n\t\tcred::BasicAuthCredential,\n\t\tremotes::push::ProgressNotification, repository::repo, utils,\n\t},\n\tProgressPercent,\n};\nuse crossbeam_channel::Sender;\nuse git2::{\n\tBranchType, FetchOptions, ProxyOptions, Remote, Repository,\n};\nuse scopetime::scope_time;\nuse utils::bytes2string;\n\npub use callbacks::Callbacks;\npub use tags::tags_missing_remote;\n\nuse super::RepoPath;\n\n/// origin\npub const DEFAULT_REMOTE_NAME: &str = \"origin\";\n\n///\npub fn proxy_auto<'a>() -> ProxyOptions<'a> {\n\tlet mut proxy = ProxyOptions::new();\n\tproxy.auto();\n\tproxy\n}\n\n///\npub fn add_remote(\n\trepo_path: &RepoPath,\n\tname: &str,\n\turl: &str,\n) -> Result<()> {\n\tlet repo = repo(repo_path)?;\n\trepo.remote(name, url)?;\n\tOk(())\n}\n\n///\npub fn rename_remote(\n\trepo_path: &RepoPath,\n\tname: &str,\n\tnew_name: &str,\n) -> Result<()> {\n\tlet repo = repo(repo_path)?;\n\trepo.remote_rename(name, new_name)?;\n\tOk(())\n}\n\n///\npub fn update_remote_url(\n\trepo_path: &RepoPath,\n\tname: &str,\n\tnew_url: &str,\n) -> Result<()> {\n\tlet repo = repo(repo_path)?;\n\trepo.remote_set_url(name, new_url)?;\n\tOk(())\n}\n\n///\npub fn delete_remote(\n\trepo_path: &RepoPath,\n\tremote_name: &str,\n) -> Result<()> {\n\tlet repo = repo(repo_path)?;\n\trepo.remote_delete(remote_name)?;\n\tOk(())\n}\n\n///\npub fn validate_remote_name(name: &str) -> bool {\n\tRemote::is_valid_name(name)\n}\n\n///\npub fn get_remotes(repo_path: &RepoPath) -> Result<Vec<String>> {\n\tscope_time!(\"get_remotes\");\n\n\tlet repo = repo(repo_path)?;\n\tlet remotes = repo.remotes()?;\n\tlet remotes: Vec<String> =\n\t\tremotes.iter().flatten().map(String::from).collect();\n\n\tOk(remotes)\n}\n\n///\npub fn get_remote_url(\n\trepo_path: &RepoPath,\n\tremote_name: &str,\n) -> Result<Option<String>> {\n\tlet repo = repo(repo_path)?;\n\tlet remote = repo.find_remote(remote_name)?.clone();\n\tlet url = remote.url();\n\tif let Some(u) = url {\n\t\treturn Ok(Some(u.to_string()));\n\t}\n\tOk(None)\n}\n\n/// tries to find origin or the only remote that is defined if any\n/// in case of multiple remotes and none named *origin* we fail\npub fn get_default_remote(repo_path: &RepoPath) -> Result<String> {\n\tlet repo = repo(repo_path)?;\n\tget_default_remote_in_repo(&repo)\n}\n\n/// Gets the current branch the user is on.\n/// Returns none if they are not on a branch\n/// and Err if there was a problem finding the branch\nfn get_current_branch(\n\trepo: &Repository,\n) -> Result<Option<git2::Branch<'_>>> {\n\tfor b in repo.branches(None)? {\n\t\tlet branch = b?.0;\n\t\tif branch.is_head() {\n\t\t\treturn Ok(Some(branch));\n\t\t}\n\t}\n\tOk(None)\n}\n\n/// Tries to find the default repo to fetch from based on configuration.\n///\n/// > `branch.<name>.remote`\n/// >\n/// > When on branch `<name>`, it tells `git fetch` and `git push` which remote to fetch from or\n/// > push to. [...] If no remote is configured, or if you are not on any branch and there is more\n/// > than one remote defined in the repository, it defaults to `origin` for fetching [...].\n///\n/// [git-config-branch-name-remote]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-branchltnamegtremote\n///\n/// Falls back to `get_default_remote_in_repo`.\npub fn get_default_remote_for_fetch(\n\trepo_path: &RepoPath,\n) -> Result<String> {\n\tlet repo = repo(repo_path)?;\n\tget_default_remote_for_fetch_in_repo(&repo)\n}\n\n// TODO: Very similar to `get_default_remote_for_push_in_repo`. Can probably be refactored.\npub(crate) fn get_default_remote_for_fetch_in_repo(\n\trepo: &Repository,\n) -> Result<String> {\n\tscope_time!(\"get_default_remote_for_fetch_in_repo\");\n\n\tlet config = repo.config()?;\n\n\tlet branch = get_current_branch(repo)?;\n\n\tif let Some(branch) = branch {\n\t\tlet remote_name = bytes2string(branch.name_bytes()?)?;\n\n\t\tlet entry_name = format!(\"branch.{}.remote\", &remote_name);\n\n\t\tif let Ok(entry) = config.get_entry(&entry_name) {\n\t\t\treturn bytes2string(entry.value_bytes());\n\t\t}\n\t}\n\n\tget_default_remote_in_repo(repo)\n}\n\n/// Tries to find the default repo to push to based on configuration.\n///\n/// > `remote.pushDefault`\n/// >\n/// > The remote to push to by default. Overrides `branch.<name>.remote` for all branches, and is\n/// > overridden by `branch.<name>.pushRemote` for specific branches.\n///\n/// > `branch.<name>.remote`\n/// >\n/// > When on branch `<name>`, it tells `git fetch` and `git push` which remote to fetch from or\n/// > push to. The remote to push to may be overridden with `remote.pushDefault` (for all\n/// > branches). The remote to push to, for the current branch, may be further overridden by\n/// > `branch.<name>.pushRemote`. If no remote is configured, or if you are not on any branch and\n/// > there is more than one remote defined in the repository, it defaults to `origin` for fetching\n/// > and `remote.pushDefault` for pushing.\n///\n/// [git-config-remote-push-default]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-remotepushDefault\n/// [git-config-branch-name-remote]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-branchltnamegtremote\n///\n/// Falls back to `get_default_remote_in_repo`.\npub fn get_default_remote_for_push(\n\trepo_path: &RepoPath,\n) -> Result<String> {\n\tlet repo = repo(repo_path)?;\n\tget_default_remote_for_push_in_repo(&repo)\n}\n\n// TODO: Very similar to `get_default_remote_for_fetch_in_repo`. Can probably be refactored.\npub(crate) fn get_default_remote_for_push_in_repo(\n\trepo: &Repository,\n) -> Result<String> {\n\tscope_time!(\"get_default_remote_for_push_in_repo\");\n\n\tlet config = repo.config()?;\n\n\tlet branch = get_current_branch(repo)?;\n\n\tif let Some(branch) = branch {\n\t\tlet remote_name = bytes2string(branch.name_bytes()?)?;\n\n\t\tlet entry_name =\n\t\t\tformat!(\"branch.{}.pushRemote\", &remote_name);\n\n\t\tif let Ok(entry) = config.get_entry(&entry_name) {\n\t\t\treturn bytes2string(entry.value_bytes());\n\t\t}\n\n\t\tif let Ok(entry) = config.get_entry(\"remote.pushDefault\") {\n\t\t\treturn bytes2string(entry.value_bytes());\n\t\t}\n\n\t\tlet entry_name = format!(\"branch.{}.remote\", &remote_name);\n\n\t\tif let Ok(entry) = config.get_entry(&entry_name) {\n\t\t\treturn bytes2string(entry.value_bytes());\n\t\t}\n\t}\n\n\tget_default_remote_in_repo(repo)\n}\n\n/// see `get_default_remote`\npub(crate) fn get_default_remote_in_repo(\n\trepo: &Repository,\n) -> Result<String> {\n\tscope_time!(\"get_default_remote_in_repo\");\n\n\tlet remotes = repo.remotes()?;\n\n\t// if `origin` exists return that\n\tlet found_origin = remotes\n\t\t.iter()\n\t\t.any(|r| r.is_some_and(|r| r == DEFAULT_REMOTE_NAME));\n\tif found_origin {\n\t\treturn Ok(DEFAULT_REMOTE_NAME.into());\n\t}\n\n\t//if only one remote exists pick that\n\tif remotes.len() == 1 {\n\t\tlet first_remote = remotes\n\t\t\t.iter()\n\t\t\t.next()\n\t\t\t.flatten()\n\t\t\t.map(String::from)\n\t\t\t.ok_or_else(|| {\n\t\t\t\tError::Generic(\"no remote found\".into())\n\t\t\t})?;\n\n\t\treturn Ok(first_remote);\n\t}\n\n\t//inconclusive\n\tErr(Error::NoDefaultRemoteFound)\n}\n\n///\nfn fetch_from_remote(\n\trepo_path: &RepoPath,\n\tremote: &str,\n\tbasic_credential: Option<BasicAuthCredential>,\n\tprogress_sender: Option<Sender<ProgressNotification>>,\n) -> Result<()> {\n\tlet repo = repo(repo_path)?;\n\n\tlet mut remote = repo.find_remote(remote)?;\n\n\tlet mut options = FetchOptions::new();\n\tlet callbacks = Callbacks::new(progress_sender, basic_credential);\n\toptions.prune(git2::FetchPrune::On);\n\toptions.proxy_options(proxy_auto());\n\toptions.download_tags(git2::AutotagOption::All);\n\toptions.remote_callbacks(callbacks.callbacks());\n\tremote.fetch(&[] as &[&str], Some(&mut options), None)?;\n\t// fetch tags (also removing remotely deleted ones)\n\tremote.fetch(\n\t\t&[\"refs/tags/*:refs/tags/*\"],\n\t\tSome(&mut options),\n\t\tNone,\n\t)?;\n\n\tOk(())\n}\n\n/// updates/prunes all branches from all remotes\npub fn fetch_all(\n\trepo_path: &RepoPath,\n\tbasic_credential: &Option<BasicAuthCredential>,\n\tprogress_sender: &Option<Sender<ProgressPercent>>,\n) -> Result<()> {\n\tscope_time!(\"fetch_all\");\n\n\tlet repo = repo(repo_path)?;\n\tlet remotes = repo\n\t\t.remotes()?\n\t\t.iter()\n\t\t.flatten()\n\t\t.map(String::from)\n\t\t.collect::<Vec<_>>();\n\tlet remotes_count = remotes.len();\n\n\tfor (idx, remote) in remotes.into_iter().enumerate() {\n\t\tfetch_from_remote(\n\t\t\trepo_path,\n\t\t\t&remote,\n\t\t\tbasic_credential.clone(),\n\t\t\tNone,\n\t\t)?;\n\n\t\tif let Some(sender) = progress_sender {\n\t\t\tlet progress = ProgressPercent::new(idx, remotes_count);\n\t\t\tsender.send(progress)?;\n\t\t}\n\t}\n\n\tOk(())\n}\n\n/// fetches from upstream/remote for local `branch`\npub(crate) fn fetch(\n\trepo_path: &RepoPath,\n\tbranch: &str,\n\tbasic_credential: Option<BasicAuthCredential>,\n\tprogress_sender: Option<Sender<ProgressNotification>>,\n) -> Result<usize> {\n\tscope_time!(\"fetch\");\n\n\tlet repo = repo(repo_path)?;\n\tlet branch_ref = repo\n\t\t.find_branch(branch, BranchType::Local)?\n\t\t.into_reference();\n\tlet branch_ref = bytes2string(branch_ref.name_bytes())?;\n\tlet remote_name = repo.branch_upstream_remote(&branch_ref)?;\n\tlet remote_name = bytes2string(&remote_name)?;\n\tlet mut remote = repo.find_remote(&remote_name)?;\n\n\tlet mut options = FetchOptions::new();\n\toptions.download_tags(git2::AutotagOption::All);\n\tlet callbacks = Callbacks::new(progress_sender, basic_credential);\n\toptions.remote_callbacks(callbacks.callbacks());\n\toptions.proxy_options(proxy_auto());\n\n\tremote.fetch(&[branch], Some(&mut options), None)?;\n\n\tOk(remote.stats().received_bytes())\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::sync::tests::{\n\t\tdebug_cmd_print, repo_clone, repo_init,\n\t};\n\n\t#[test]\n\tfn test_smoke() {\n\t\tlet (remote_dir, _remote) = repo_init().unwrap();\n\t\tlet remote_path = remote_dir.path().to_str().unwrap();\n\t\tlet (repo_dir, _repo) = repo_clone(remote_path).unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&repo_dir.keep().as_os_str().to_str().unwrap().into();\n\n\t\tlet remotes = get_remotes(repo_path).unwrap();\n\n\t\tassert_eq!(remotes, vec![String::from(\"origin\")]);\n\n\t\tfetch(repo_path, \"master\", None, None).unwrap();\n\t}\n\n\t#[test]\n\tfn test_default_remote() {\n\t\tlet (remote_dir, _remote) = repo_init().unwrap();\n\t\tlet remote_path = remote_dir.path().to_str().unwrap();\n\t\tlet (repo_dir, _repo) = repo_clone(remote_path).unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&repo_dir.keep().as_os_str().to_str().unwrap().into();\n\n\t\tdebug_cmd_print(\n\t\t\trepo_path,\n\t\t\t&format!(\"git remote add second {remote_path}\")[..],\n\t\t);\n\n\t\tlet remotes = get_remotes(repo_path).unwrap();\n\n\t\tassert_eq!(\n\t\t\tremotes,\n\t\t\tvec![String::from(\"origin\"), String::from(\"second\")]\n\t\t);\n\n\t\tlet first =\n\t\t\tget_default_remote_in_repo(&repo(repo_path).unwrap())\n\t\t\t\t.unwrap();\n\t\tassert_eq!(first, String::from(\"origin\"));\n\t}\n\n\t#[test]\n\tfn test_default_remote_out_of_order() {\n\t\tlet (remote_dir, _remote) = repo_init().unwrap();\n\t\tlet remote_path = remote_dir.path().to_str().unwrap();\n\t\tlet (repo_dir, _repo) = repo_clone(remote_path).unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&repo_dir.keep().as_os_str().to_str().unwrap().into();\n\n\t\tdebug_cmd_print(\n\t\t\trepo_path,\n\t\t\t\"git remote rename origin alternate\",\n\t\t);\n\n\t\tdebug_cmd_print(\n\t\t\trepo_path,\n\t\t\t&format!(\"git remote add origin {remote_path}\")[..],\n\t\t);\n\n\t\t//NOTE: apparently remotes are not chronolically sorted but alphabetically\n\t\tlet remotes = get_remotes(repo_path).unwrap();\n\n\t\tassert_eq!(\n\t\t\tremotes,\n\t\t\tvec![String::from(\"alternate\"), String::from(\"origin\")]\n\t\t);\n\n\t\tlet first =\n\t\t\tget_default_remote_in_repo(&repo(repo_path).unwrap())\n\t\t\t\t.unwrap();\n\t\tassert_eq!(first, String::from(\"origin\"));\n\t}\n\n\t#[test]\n\tfn test_default_remote_inconclusive() {\n\t\tlet (remote_dir, _remote) = repo_init().unwrap();\n\t\tlet remote_path = remote_dir.path().to_str().unwrap();\n\t\tlet (repo_dir, _repo) = repo_clone(remote_path).unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&repo_dir.keep().as_os_str().to_str().unwrap().into();\n\n\t\tdebug_cmd_print(\n\t\t\trepo_path,\n\t\t\t\"git remote rename origin alternate\",\n\t\t);\n\n\t\tdebug_cmd_print(\n\t\t\trepo_path,\n\t\t\t&format!(\"git remote add someremote {remote_path}\")[..],\n\t\t);\n\n\t\tlet remotes = get_remotes(repo_path).unwrap();\n\t\tassert_eq!(\n\t\t\tremotes,\n\t\t\tvec![\n\t\t\t\tString::from(\"alternate\"),\n\t\t\t\tString::from(\"someremote\")\n\t\t\t]\n\t\t);\n\n\t\tlet default_remote =\n\t\t\tget_default_remote_in_repo(&repo(repo_path).unwrap());\n\n\t\tassert!(matches!(\n\t\t\tdefault_remote,\n\t\t\tErr(Error::NoDefaultRemoteFound)\n\t\t));\n\t}\n\n\t#[test]\n\tfn test_default_remote_for_fetch() {\n\t\tlet (remote_dir, _remote) = repo_init().unwrap();\n\t\tlet remote_path = remote_dir.path().to_str().unwrap();\n\t\tlet (repo_dir, repo) = repo_clone(remote_path).unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&repo_dir.keep().as_os_str().to_str().unwrap().into();\n\n\t\tdebug_cmd_print(\n\t\t\trepo_path,\n\t\t\t\"git remote rename origin alternate\",\n\t\t);\n\n\t\tdebug_cmd_print(\n\t\t\trepo_path,\n\t\t\t&format!(\"git remote add someremote {remote_path}\")[..],\n\t\t);\n\n\t\tlet mut config = repo.config().unwrap();\n\n\t\tconfig\n\t\t\t.set_str(\"branch.master.remote\", \"branchremote\")\n\t\t\t.unwrap();\n\n\t\tlet default_fetch_remote =\n\t\t\tget_default_remote_for_fetch_in_repo(&repo);\n\n\t\tassert!(\n\t\t\tmatches!(default_fetch_remote, Ok(remote_name) if remote_name == \"branchremote\")\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_default_remote_for_push() {\n\t\tlet (remote_dir, _remote) = repo_init().unwrap();\n\t\tlet remote_path = remote_dir.path().to_str().unwrap();\n\t\tlet (repo_dir, repo) = repo_clone(remote_path).unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&repo_dir.keep().as_os_str().to_str().unwrap().into();\n\n\t\tdebug_cmd_print(\n\t\t\trepo_path,\n\t\t\t\"git remote rename origin alternate\",\n\t\t);\n\n\t\tdebug_cmd_print(\n\t\t\trepo_path,\n\t\t\t&format!(\"git remote add someremote {remote_path}\")[..],\n\t\t);\n\n\t\tlet mut config = repo.config().unwrap();\n\n\t\tconfig\n\t\t\t.set_str(\"branch.master.remote\", \"branchremote\")\n\t\t\t.unwrap();\n\n\t\tlet default_push_remote =\n\t\t\tget_default_remote_for_push_in_repo(&repo);\n\n\t\tassert!(\n\t\t\tmatches!(default_push_remote, Ok(remote_name) if remote_name == \"branchremote\")\n\t\t);\n\n\t\tconfig.set_str(\"remote.pushDefault\", \"pushdefault\").unwrap();\n\n\t\tlet default_push_remote =\n\t\t\tget_default_remote_for_push_in_repo(&repo);\n\n\t\tassert!(\n\t\t\tmatches!(default_push_remote, Ok(remote_name) if remote_name == \"pushdefault\")\n\t\t);\n\n\t\tconfig\n\t\t\t.set_str(\"branch.master.pushRemote\", \"branchpushremote\")\n\t\t\t.unwrap();\n\n\t\tlet default_push_remote =\n\t\t\tget_default_remote_for_push_in_repo(&repo);\n\n\t\tassert!(\n\t\t\tmatches!(default_push_remote, Ok(remote_name) if remote_name == \"branchpushremote\")\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/remotes/push.rs",
    "content": "use crate::{\n\terror::{Error, Result},\n\tprogress::ProgressPercent,\n\tsync::{\n\t\tbranch::branch_set_upstream_after_push,\n\t\tconfig::{\n\t\t\tpush_default_strategy_config_repo,\n\t\t\tPushDefaultStrategyConfig,\n\t\t},\n\t\tcred::BasicAuthCredential,\n\t\tget_branch_upstream_merge,\n\t\tremotes::{proxy_auto, Callbacks},\n\t\trepository::repo,\n\t\tCommitId, RepoPath,\n\t},\n};\nuse crossbeam_channel::Sender;\nuse git2::{PackBuilderStage, PushOptions};\nuse scopetime::scope_time;\nuse std::fmt::Write as _;\n\n///\npub trait AsyncProgress: Clone + Send + Sync {\n\t///\n\tfn is_done(&self) -> bool;\n\t///\n\tfn progress(&self) -> ProgressPercent;\n}\n\n///\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum ProgressNotification {\n\t///\n\tUpdateTips {\n\t\t///\n\t\tname: String,\n\t\t///\n\t\ta: CommitId,\n\t\t///\n\t\tb: CommitId,\n\t},\n\t///\n\tTransfer {\n\t\t///\n\t\tobjects: usize,\n\t\t///\n\t\ttotal_objects: usize,\n\t},\n\t///\n\tPushTransfer {\n\t\t///\n\t\tcurrent: usize,\n\t\t///\n\t\ttotal: usize,\n\t\t///\n\t\tbytes: usize,\n\t},\n\t///\n\tPacking {\n\t\t///\n\t\tstage: PackBuilderStage,\n\t\t///\n\t\ttotal: usize,\n\t\t///\n\t\tcurrent: usize,\n\t},\n\t///\n\tDone,\n}\n\nimpl AsyncProgress for ProgressNotification {\n\tfn is_done(&self) -> bool {\n\t\t*self == Self::Done\n\t}\n\tfn progress(&self) -> ProgressPercent {\n\t\tmatch *self {\n\t\t\tSelf::Packing {\n\t\t\t\tstage,\n\t\t\t\tcurrent,\n\t\t\t\ttotal,\n\t\t\t} => match stage {\n\t\t\t\tPackBuilderStage::AddingObjects\n\t\t\t\t| PackBuilderStage::Deltafication => {\n\t\t\t\t\tProgressPercent::new(current, total)\n\t\t\t\t}\n\t\t\t},\n\t\t\tSelf::PushTransfer { current, total, .. } => {\n\t\t\t\tProgressPercent::new(current, total)\n\t\t\t}\n\t\t\tSelf::Transfer {\n\t\t\t\tobjects,\n\t\t\t\ttotal_objects,\n\t\t\t\t..\n\t\t\t} => ProgressPercent::new(objects, total_objects),\n\t\t\t_ => ProgressPercent::full(),\n\t\t}\n\t}\n}\n\n///\n#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]\npub enum PushType {\n\t///\n\t#[default]\n\tBranch,\n\t///\n\tTag,\n}\n\n#[cfg(test)]\npub fn push_branch(\n\trepo_path: &RepoPath,\n\tremote: &str,\n\tbranch: &str,\n\tforce: bool,\n\tdelete: bool,\n\tbasic_credential: Option<BasicAuthCredential>,\n\tprogress_sender: Option<Sender<ProgressNotification>>,\n) -> Result<()> {\n\tpush_raw(\n\t\trepo_path,\n\t\tremote,\n\t\tbranch,\n\t\tPushType::Branch,\n\t\tforce,\n\t\tdelete,\n\t\tbasic_credential,\n\t\tprogress_sender,\n\t)\n}\n\n//TODO: cleanup\n#[allow(clippy::too_many_arguments)]\npub fn push_raw(\n\trepo_path: &RepoPath,\n\tremote: &str,\n\tbranch: &str,\n\tref_type: PushType,\n\tforce: bool,\n\tdelete: bool,\n\tbasic_credential: Option<BasicAuthCredential>,\n\tprogress_sender: Option<Sender<ProgressNotification>>,\n) -> Result<()> {\n\tscope_time!(\"push\");\n\n\tlet repo = repo(repo_path)?;\n\tlet mut remote = repo.find_remote(remote)?;\n\n\tlet push_default_strategy =\n\t\tpush_default_strategy_config_repo(&repo)?;\n\n\tlet mut options = PushOptions::new();\n\toptions.proxy_options(proxy_auto());\n\n\tlet callbacks = Callbacks::new(progress_sender, basic_credential);\n\toptions.remote_callbacks(callbacks.callbacks());\n\toptions.packbuilder_parallelism(0);\n\n\tlet branch_modifier = match (force, delete) {\n\t\t(true, true) => \"+:\",\n\t\t(false, true) => \":\",\n\t\t(true, false) => \"+\",\n\t\t(false, false) => \"\",\n\t};\n\tlet git_ref_type = match ref_type {\n\t\tPushType::Branch => \"heads\",\n\t\tPushType::Tag => \"tags\",\n\t};\n\n\tlet mut push_ref =\n\t\tformat!(\"{branch_modifier}refs/{git_ref_type}/{branch}\");\n\n\tif !delete\n\t\t&& ref_type == PushType::Branch\n\t\t&& push_default_strategy\n\t\t\t== PushDefaultStrategyConfig::Upstream\n\t{\n\t\tif let Ok(Some(branch_upstream_merge)) =\n\t\t\tget_branch_upstream_merge(repo_path, branch)\n\t\t{\n\t\t\tlet _ = write!(push_ref, \":{branch_upstream_merge}\");\n\t\t}\n\t}\n\n\tlog::debug!(\"push to: {push_ref}\");\n\tremote.push(&[push_ref], Some(&mut options))?;\n\n\tif let Some((reference, msg)) =\n\t\tcallbacks.get_stats()?.push_rejected_msg\n\t{\n\t\treturn Err(Error::Generic(format!(\n\t\t\t\"push to '{reference}' rejected: {msg}\"\n\t\t)));\n\t}\n\n\tif !delete {\n\t\tbranch_set_upstream_after_push(&repo, branch)?;\n\t}\n\n\tOk(())\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::sync::{\n\t\tself,\n\t\ttests::{\n\t\t\tget_commit_ids, repo_clone, repo_init, repo_init_bare,\n\t\t\twrite_commit_file,\n\t\t},\n\t};\n\tuse git2::Repository;\n\tuse std::{fs::File, io::Write, path::Path};\n\n\t#[test]\n\tfn test_force_push() {\n\t\t// This test mimics the scenario of 2 people having 2\n\t\t// local branches and both modifying the same file then\n\t\t// both pushing, sequentially\n\t\tlet (tmp_repo_dir, repo) = repo_init().unwrap();\n\t\tlet (tmp_other_repo_dir, other_repo) = repo_init().unwrap();\n\t\tlet (tmp_upstream_dir, _) = repo_init_bare().unwrap();\n\n\t\trepo.remote(\n\t\t\t\"origin\",\n\t\t\ttmp_upstream_dir.path().to_str().unwrap(),\n\t\t)\n\t\t.unwrap();\n\n\t\tother_repo\n\t\t\t.remote(\n\t\t\t\t\"origin\",\n\t\t\t\ttmp_upstream_dir.path().to_str().unwrap(),\n\t\t\t)\n\t\t\t.unwrap();\n\n\t\tlet tmp_repo_file_path =\n\t\t\ttmp_repo_dir.path().join(\"temp_file.txt\");\n\t\tlet mut tmp_repo_file =\n\t\t\tFile::create(tmp_repo_file_path).unwrap();\n\t\twriteln!(tmp_repo_file, \"TempSomething\").unwrap();\n\n\t\tsync::commit(\n\t\t\t&tmp_repo_dir.path().to_str().unwrap().into(),\n\t\t\t\"repo_1_commit\",\n\t\t)\n\t\t.unwrap();\n\n\t\tpush_branch(\n\t\t\t&tmp_repo_dir.path().to_str().unwrap().into(),\n\t\t\t\"origin\",\n\t\t\t\"master\",\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\n\t\tlet tmp_other_repo_file_path =\n\t\t\ttmp_other_repo_dir.path().join(\"temp_file.txt\");\n\t\tlet mut tmp_other_repo_file =\n\t\t\tFile::create(tmp_other_repo_file_path).unwrap();\n\t\twriteln!(tmp_other_repo_file, \"TempElse\").unwrap();\n\n\t\tsync::commit(\n\t\t\t&tmp_other_repo_dir.path().to_str().unwrap().into(),\n\t\t\t\"repo_2_commit\",\n\t\t)\n\t\t.unwrap();\n\n\t\t// Attempt a normal push,\n\t\t// should fail as branches diverged\n\t\tassert!(push_branch(\n\t\t\t&tmp_other_repo_dir.path().to_str().unwrap().into(),\n\t\t\t\"origin\",\n\t\t\t\"master\",\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.is_err());\n\n\t\t// Attempt force push,\n\t\t// should work as it forces the push through\n\t\tassert!(push_branch(\n\t\t\t&tmp_other_repo_dir.path().to_str().unwrap().into(),\n\t\t\t\"origin\",\n\t\t\t\"master\",\n\t\t\ttrue,\n\t\t\tfalse,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.is_ok());\n\t}\n\n\t#[test]\n\tfn test_force_push_rewrites_history() {\n\t\t// This test mimics the scenario of 2 people having 2\n\t\t// local branches and both modifying the same file then\n\t\t// both pushing, sequentially\n\n\t\tlet (tmp_repo_dir, repo) = repo_init().unwrap();\n\t\tlet (tmp_other_repo_dir, other_repo) = repo_init().unwrap();\n\t\tlet (tmp_upstream_dir, upstream) = repo_init_bare().unwrap();\n\n\t\trepo.remote(\n\t\t\t\"origin\",\n\t\t\ttmp_upstream_dir.path().to_str().unwrap(),\n\t\t)\n\t\t.unwrap();\n\n\t\tother_repo\n\t\t\t.remote(\n\t\t\t\t\"origin\",\n\t\t\t\ttmp_upstream_dir.path().to_str().unwrap(),\n\t\t\t)\n\t\t\t.unwrap();\n\n\t\tlet tmp_repo_file_path =\n\t\t\ttmp_repo_dir.path().join(\"temp_file.txt\");\n\t\tlet mut tmp_repo_file =\n\t\t\tFile::create(tmp_repo_file_path).unwrap();\n\t\twriteln!(tmp_repo_file, \"TempSomething\").unwrap();\n\n\t\tsync::stage_add_file(\n\t\t\t&tmp_repo_dir.path().to_str().unwrap().into(),\n\t\t\tPath::new(\"temp_file.txt\"),\n\t\t)\n\t\t.unwrap();\n\n\t\tlet repo_1_commit = sync::commit(\n\t\t\t&tmp_repo_dir.path().to_str().unwrap().into(),\n\t\t\t\"repo_1_commit\",\n\t\t)\n\t\t.unwrap();\n\n\t\t//NOTE: make sure the commit actually contains that file\n\t\tassert_eq!(\n\t\t\tsync::get_commit_files(\n\t\t\t\t&tmp_repo_dir.path().to_str().unwrap().into(),\n\t\t\t\trepo_1_commit,\n\t\t\t\tNone\n\t\t\t)\n\t\t\t.unwrap()[0]\n\t\t\t\t.path,\n\t\t\tString::from(\"temp_file.txt\")\n\t\t);\n\n\t\tlet commits = get_commit_ids(&repo, 1);\n\t\tassert!(commits.contains(&repo_1_commit));\n\n\t\tpush_branch(\n\t\t\t&tmp_repo_dir.path().to_str().unwrap().into(),\n\t\t\t\"origin\",\n\t\t\t\"master\",\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\n\t\tlet tmp_other_repo_file_path =\n\t\t\ttmp_other_repo_dir.path().join(\"temp_file.txt\");\n\t\tlet mut tmp_other_repo_file =\n\t\t\tFile::create(tmp_other_repo_file_path).unwrap();\n\t\twriteln!(tmp_other_repo_file, \"TempElse\").unwrap();\n\n\t\tsync::stage_add_file(\n\t\t\t&tmp_other_repo_dir.path().to_str().unwrap().into(),\n\t\t\tPath::new(\"temp_file.txt\"),\n\t\t)\n\t\t.unwrap();\n\n\t\tlet repo_2_commit = sync::commit(\n\t\t\t&tmp_other_repo_dir.path().to_str().unwrap().into(),\n\t\t\t\"repo_2_commit\",\n\t\t)\n\t\t.unwrap();\n\n\t\tlet repo_2_parent = other_repo\n\t\t\t.find_commit(repo_2_commit.into())\n\t\t\t.unwrap()\n\t\t\t.parents()\n\t\t\t.next()\n\t\t\t.unwrap()\n\t\t\t.id();\n\n\t\tlet commits = get_commit_ids(&other_repo, 1);\n\t\tassert!(commits.contains(&repo_2_commit));\n\n\t\t// Attempt a normal push,\n\t\t// should fail as branches diverged\n\t\tassert!(push_branch(\n\t\t\t&tmp_other_repo_dir.path().to_str().unwrap().into(),\n\t\t\t\"origin\",\n\t\t\t\"master\",\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.is_err());\n\n\t\t// Check that the other commit is not in upstream,\n\t\t// a normal push would not rewrite history\n\t\tlet commits = get_commit_ids(&upstream, 1);\n\t\tassert!(!commits.contains(&repo_2_commit));\n\n\t\t// Attempt force push,\n\t\t// should work as it forces the push through\n\n\t\tpush_branch(\n\t\t\t&tmp_other_repo_dir.path().to_str().unwrap().into(),\n\t\t\t\"origin\",\n\t\t\t\"master\",\n\t\t\ttrue,\n\t\t\tfalse,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\n\t\tlet commits = get_commit_ids(&upstream, 1);\n\t\tassert!(commits.contains(&repo_2_commit));\n\n\t\tlet new_upstream_parent =\n\t\t\tRepository::init_bare(tmp_upstream_dir.path())\n\t\t\t\t.unwrap()\n\t\t\t\t.find_commit(repo_2_commit.into())\n\t\t\t\t.unwrap()\n\t\t\t\t.parents()\n\t\t\t\t.next()\n\t\t\t\t.unwrap()\n\t\t\t\t.id();\n\t\tassert_eq!(new_upstream_parent, repo_2_parent,);\n\t}\n\n\t#[test]\n\tfn test_delete_remote_branch() {\n\t\t// This test mimics the scenario of a user creating a branch, push it, and then remove it on the remote\n\n\t\tlet (upstream_dir, upstream_repo) = repo_init_bare().unwrap();\n\n\t\tlet (tmp_repo_dir, repo) =\n\t\t\trepo_clone(upstream_dir.path().to_str().unwrap())\n\t\t\t\t.unwrap();\n\n\t\t// You need a commit before being able to branch !\n\t\tlet commit_1 = write_commit_file(\n\t\t\t&repo,\n\t\t\t\"temp_file.txt\",\n\t\t\t\"SomeContent\",\n\t\t\t\"Initial commit\",\n\t\t);\n\n\t\tlet commits = get_commit_ids(&repo, 1);\n\t\tassert!(commits.contains(&commit_1));\n\n\t\tpush_branch(\n\t\t\t&tmp_repo_dir.path().to_str().unwrap().into(),\n\t\t\t\"origin\",\n\t\t\t\"master\",\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\n\t\t// Create the local branch\n\t\tsync::create_branch(\n\t\t\t&tmp_repo_dir.path().to_str().unwrap().into(),\n\t\t\t\"test_branch\",\n\t\t)\n\t\t.unwrap();\n\n\t\t// Push the local branch\n\t\tpush_branch(\n\t\t\t&tmp_repo_dir.path().to_str().unwrap().into(),\n\t\t\t\"origin\",\n\t\t\t\"test_branch\",\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\n\t\t// Test if the branch exits on the remote\n\t\tassert!(upstream_repo\n\t\t\t.branches(None)\n\t\t\t.unwrap()\n\t\t\t.map(std::result::Result::unwrap)\n\t\t\t.map(|(i, _)| i.name().unwrap().unwrap().to_string())\n\t\t\t.any(|i| &i == \"test_branch\"));\n\n\t\t// Delete the remote branch\n\t\tassert!(push_branch(\n\t\t\t&tmp_repo_dir.path().to_str().unwrap().into(),\n\t\t\t\"origin\",\n\t\t\t\"test_branch\",\n\t\t\tfalse,\n\t\t\ttrue,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.is_ok());\n\n\t\t// Test that the branch has be remove from the remote\n\t\tassert!(!upstream_repo\n\t\t\t.branches(None)\n\t\t\t.unwrap()\n\t\t\t.map(std::result::Result::unwrap)\n\t\t\t.map(|(i, _)| i.name().unwrap().unwrap().to_string())\n\t\t\t.any(|i| &i == \"test_branch\"));\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/remotes/tags.rs",
    "content": "//!\n\nuse super::push::AsyncProgress;\nuse crate::{\n\terror::Result,\n\tprogress::ProgressPercent,\n\tsync::{\n\t\tcred::BasicAuthCredential,\n\t\tremotes::{proxy_auto, Callbacks},\n\t\trepository::repo,\n\t\tRepoPath,\n\t},\n};\nuse crossbeam_channel::Sender;\nuse git2::{Direction, PushOptions};\nuse scopetime::scope_time;\nuse std::collections::HashSet;\n\n///\n#[derive(Debug, Copy, Clone, PartialEq, Eq)]\npub enum PushTagsProgress {\n\t/// fetching tags from remote to check which local tags need pushing\n\tCheckRemote,\n\t/// pushing local tags that are missing remote\n\tPush {\n\t\t///\n\t\tpushed: usize,\n\t\t///\n\t\ttotal: usize,\n\t},\n\t/// done\n\tDone,\n}\n\nimpl AsyncProgress for PushTagsProgress {\n\tfn progress(&self) -> ProgressPercent {\n\t\tmatch self {\n\t\t\tSelf::CheckRemote => ProgressPercent::empty(),\n\t\t\tSelf::Push { pushed, total } => {\n\t\t\t\tProgressPercent::new(*pushed, *total)\n\t\t\t}\n\t\t\tSelf::Done => ProgressPercent::full(),\n\t\t}\n\t}\n\tfn is_done(&self) -> bool {\n\t\t*self == Self::Done\n\t}\n}\n\n/// lists the remotes tags\nfn remote_tag_refs(\n\trepo_path: &RepoPath,\n\tremote: &str,\n\tbasic_credential: Option<BasicAuthCredential>,\n) -> Result<Vec<String>> {\n\tscope_time!(\"remote_tags\");\n\n\tlet repo = repo(repo_path)?;\n\tlet mut remote = repo.find_remote(remote)?;\n\tlet callbacks = Callbacks::new(None, basic_credential);\n\tlet conn = remote.connect_auth(\n\t\tDirection::Fetch,\n\t\tSome(callbacks.callbacks()),\n\t\tSome(proxy_auto()),\n\t)?;\n\n\tlet remote_heads = conn.list()?;\n\tlet remote_tags = remote_heads\n\t\t.iter()\n\t\t.map(|s| s.name().to_string())\n\t\t.filter(|name| {\n\t\t\tname.starts_with(\"refs/tags/\") && !name.ends_with(\"^{}\")\n\t\t})\n\t\t.collect::<Vec<_>>();\n\n\tOk(remote_tags)\n}\n\n/// lists the remotes tags missing\npub fn tags_missing_remote(\n\trepo_path: &RepoPath,\n\tremote: &str,\n\tbasic_credential: Option<BasicAuthCredential>,\n) -> Result<Vec<String>> {\n\tscope_time!(\"tags_missing_remote\");\n\n\tlet repo = repo(repo_path)?;\n\tlet tags = repo.tag_names(None)?;\n\n\tlet mut local_tags = tags\n\t\t.iter()\n\t\t.filter_map(|tag| tag.map(|tag| format!(\"refs/tags/{tag}\")))\n\t\t.collect::<HashSet<_>>();\n\tlet remote_tags =\n\t\tremote_tag_refs(repo_path, remote, basic_credential)?;\n\n\tfor t in remote_tags {\n\t\tlocal_tags.remove(&t);\n\t}\n\n\tOk(local_tags.into_iter().collect())\n}\n\n///\npub fn push_tags(\n\trepo_path: &RepoPath,\n\tremote: &str,\n\tbasic_credential: Option<BasicAuthCredential>,\n\tprogress_sender: Option<Sender<PushTagsProgress>>,\n) -> Result<()> {\n\tscope_time!(\"push_tags\");\n\n\tprogress_sender\n\t\t.as_ref()\n\t\t.map(|sender| sender.send(PushTagsProgress::CheckRemote));\n\n\tlet tags_missing = tags_missing_remote(\n\t\trepo_path,\n\t\tremote,\n\t\tbasic_credential.clone(),\n\t)?;\n\n\tlet repo = repo(repo_path)?;\n\tlet mut remote = repo.find_remote(remote)?;\n\n\tlet total = tags_missing.len();\n\n\tprogress_sender.as_ref().map(|sender| {\n\t\tsender.send(PushTagsProgress::Push { pushed: 0, total })\n\t});\n\n\tfor (idx, tag) in tags_missing.into_iter().enumerate() {\n\t\tlet mut options = PushOptions::new();\n\t\tlet callbacks =\n\t\t\tCallbacks::new(None, basic_credential.clone());\n\t\toptions.remote_callbacks(callbacks.callbacks());\n\t\toptions.packbuilder_parallelism(0);\n\t\toptions.proxy_options(proxy_auto());\n\t\tremote.push(&[tag.as_str()], Some(&mut options))?;\n\n\t\tprogress_sender.as_ref().map(|sender| {\n\t\t\tsender.send(PushTagsProgress::Push {\n\t\t\t\tpushed: idx + 1,\n\t\t\t\ttotal,\n\t\t\t})\n\t\t});\n\t}\n\n\tdrop(basic_credential);\n\n\tprogress_sender.map(|sender| sender.send(PushTagsProgress::Done));\n\n\tOk(())\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::{\n\t\tsync::{\n\t\t\tself, delete_tag,\n\t\t\tremotes::{\n\t\t\t\tfetch, fetch_all,\n\t\t\t\tpush::{push_branch, push_raw},\n\t\t\t},\n\t\t\ttests::{repo_clone, repo_init_bare},\n\t\t},\n\t\tPushType,\n\t};\n\tuse pretty_assertions::assert_eq;\n\tuse sync::tests::write_commit_file;\n\n\t#[test]\n\tfn test_push_pull_tags() {\n\t\tlet (r1_dir, _repo) = repo_init_bare().unwrap();\n\t\tlet r1_dir = r1_dir.path().to_str().unwrap();\n\n\t\tlet (clone1_dir, clone1) = repo_clone(r1_dir).unwrap();\n\n\t\tlet clone1_dir: &RepoPath =\n\t\t\t&clone1_dir.path().to_str().unwrap().into();\n\n\t\tlet (clone2_dir, clone2) = repo_clone(r1_dir).unwrap();\n\n\t\tlet clone2_dir: &RepoPath =\n\t\t\t&clone2_dir.path().to_str().unwrap().into();\n\n\t\t// clone1\n\n\t\tlet commit1 =\n\t\t\twrite_commit_file(&clone1, \"test.txt\", \"test\", \"commit1\");\n\n\t\tsync::tag_commit(clone1_dir, &commit1, \"tag1\", None).unwrap();\n\n\t\tpush_branch(\n\t\t\tclone1_dir, \"origin\", \"master\", false, false, None, None,\n\t\t)\n\t\t.unwrap();\n\t\tpush_tags(clone1_dir, \"origin\", None, None).unwrap();\n\n\t\t// clone2\n\n\t\tlet _commit2 = write_commit_file(\n\t\t\t&clone2,\n\t\t\t\"test2.txt\",\n\t\t\t\"test\",\n\t\t\t\"commit2\",\n\t\t);\n\n\t\tassert_eq!(sync::get_tags(clone2_dir).unwrap().len(), 0);\n\n\t\t//lets fetch from origin\n\t\tlet bytes = fetch(clone2_dir, \"master\", None, None).unwrap();\n\t\tassert!(bytes > 0);\n\n\t\tsync::merge_upstream_commit(clone2_dir, \"master\").unwrap();\n\n\t\tassert_eq!(sync::get_tags(clone2_dir).unwrap().len(), 1);\n\t}\n\n\t#[test]\n\tfn test_get_remote_tags() {\n\t\tlet (r1_dir, _repo) = repo_init_bare().unwrap();\n\t\tlet r1_dir = r1_dir.path().to_str().unwrap();\n\n\t\tlet (clone1_dir, clone1) = repo_clone(r1_dir).unwrap();\n\n\t\tlet clone1_dir: &RepoPath =\n\t\t\t&clone1_dir.path().to_str().unwrap().into();\n\n\t\tlet (clone2_dir, _clone2) = repo_clone(r1_dir).unwrap();\n\n\t\tlet clone2_dir: &RepoPath =\n\t\t\t&clone2_dir.path().to_str().unwrap().into();\n\n\t\t// clone1\n\n\t\tlet commit1 =\n\t\t\twrite_commit_file(&clone1, \"test.txt\", \"test\", \"commit1\");\n\n\t\tsync::tag_commit(clone1_dir, &commit1, \"tag1\", None).unwrap();\n\n\t\tpush_branch(\n\t\t\tclone1_dir, \"origin\", \"master\", false, false, None, None,\n\t\t)\n\t\t.unwrap();\n\t\tpush_tags(clone1_dir, \"origin\", None, None).unwrap();\n\n\t\t// clone2\n\n\t\tlet tags =\n\t\t\tremote_tag_refs(clone2_dir, \"origin\", None).unwrap();\n\n\t\tassert_eq!(\n\t\t\ttags.as_slice(),\n\t\t\t&[String::from(\"refs/tags/tag1\")]\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_tags_missing_remote() {\n\t\tlet (r1_dir, _repo) = repo_init_bare().unwrap();\n\t\tlet r1_dir = r1_dir.path().to_str().unwrap();\n\n\t\tlet (clone1_dir, clone1) = repo_clone(r1_dir).unwrap();\n\n\t\tlet clone1_dir: &RepoPath =\n\t\t\t&clone1_dir.path().to_str().unwrap().into();\n\n\t\t// clone1\n\n\t\tlet commit1 =\n\t\t\twrite_commit_file(&clone1, \"test.txt\", \"test\", \"commit1\");\n\n\t\tsync::tag_commit(clone1_dir, &commit1, \"tag1\", None).unwrap();\n\n\t\tpush_branch(\n\t\t\tclone1_dir, \"origin\", \"master\", false, false, None, None,\n\t\t)\n\t\t.unwrap();\n\n\t\tlet tags_missing =\n\t\t\ttags_missing_remote(clone1_dir, \"origin\", None).unwrap();\n\n\t\tassert_eq!(\n\t\t\ttags_missing.as_slice(),\n\t\t\t&[String::from(\"refs/tags/tag1\")]\n\t\t);\n\t\tpush_tags(clone1_dir, \"origin\", None, None).unwrap();\n\t\tlet tags_missing =\n\t\t\ttags_missing_remote(clone1_dir, \"origin\", None).unwrap();\n\t\tassert!(tags_missing.is_empty());\n\t}\n\n\t#[test]\n\tfn test_tags_fetch() {\n\t\tlet (r1_dir, _repo) = repo_init_bare().unwrap();\n\t\tlet r1_dir = r1_dir.path().to_str().unwrap();\n\n\t\tlet (clone1_dir, clone1) = repo_clone(r1_dir).unwrap();\n\t\tlet clone1_dir: &RepoPath =\n\t\t\t&clone1_dir.path().to_str().unwrap().into();\n\n\t\tlet commit1 =\n\t\t\twrite_commit_file(&clone1, \"test.txt\", \"test\", \"commit1\");\n\t\tpush_branch(\n\t\t\tclone1_dir, \"origin\", \"master\", false, false, None, None,\n\t\t)\n\t\t.unwrap();\n\n\t\tlet (clone2_dir, _clone2) = repo_clone(r1_dir).unwrap();\n\t\tlet clone2_dir: &RepoPath =\n\t\t\t&clone2_dir.path().to_str().unwrap().into();\n\n\t\t// clone1 - creates tag\n\n\t\tsync::tag_commit(clone1_dir, &commit1, \"tag1\", None).unwrap();\n\n\t\tlet tags1 = sync::get_tags(clone1_dir).unwrap();\n\n\t\tpush_tags(clone1_dir, \"origin\", None, None).unwrap();\n\t\tlet tags_missing =\n\t\t\ttags_missing_remote(clone1_dir, \"origin\", None).unwrap();\n\t\tassert!(tags_missing.is_empty());\n\n\t\t// clone 2 - pull\n\n\t\tfetch(clone2_dir, \"master\", None, None).unwrap();\n\n\t\tlet tags2 = sync::get_tags(clone2_dir).unwrap();\n\n\t\tassert_eq!(tags1, tags2);\n\t}\n\n\t#[test]\n\tfn test_tags_fetch_all() {\n\t\tlet (r1_dir, _repo) = repo_init_bare().unwrap();\n\t\tlet r1_dir = r1_dir.path().to_str().unwrap();\n\n\t\tlet (clone1_dir, clone1) = repo_clone(r1_dir).unwrap();\n\t\tlet clone1_dir: &RepoPath =\n\t\t\t&clone1_dir.path().to_str().unwrap().into();\n\n\t\tlet commit1 =\n\t\t\twrite_commit_file(&clone1, \"test.txt\", \"test\", \"commit1\");\n\t\tpush_branch(\n\t\t\tclone1_dir, \"origin\", \"master\", false, false, None, None,\n\t\t)\n\t\t.unwrap();\n\n\t\tlet (clone2_dir, _clone2) = repo_clone(r1_dir).unwrap();\n\t\tlet clone2_dir: &RepoPath =\n\t\t\t&clone2_dir.path().to_str().unwrap().into();\n\n\t\t// clone1 - creates tag\n\n\t\tsync::tag_commit(clone1_dir, &commit1, \"tag1\", None).unwrap();\n\n\t\tlet tags1 = sync::get_tags(clone1_dir).unwrap();\n\n\t\tpush_tags(clone1_dir, \"origin\", None, None).unwrap();\n\t\tlet tags_missing =\n\t\t\ttags_missing_remote(clone1_dir, \"origin\", None).unwrap();\n\t\tassert!(tags_missing.is_empty());\n\n\t\t// clone 2 - pull\n\n\t\tfetch_all(clone2_dir, &None, &None).unwrap();\n\n\t\tlet tags2 = sync::get_tags(clone2_dir).unwrap();\n\n\t\tassert_eq!(tags1, tags2);\n\t}\n\n\t#[test]\n\tfn test_tags_delete_remote() {\n\t\tlet (r1_dir, _repo) = repo_init_bare().unwrap();\n\t\tlet r1_dir = r1_dir.path().to_str().unwrap();\n\n\t\tlet (clone1_dir, clone1) = repo_clone(r1_dir).unwrap();\n\t\tlet clone1_dir: &RepoPath =\n\t\t\t&clone1_dir.path().to_str().unwrap().into();\n\n\t\tlet commit1 =\n\t\t\twrite_commit_file(&clone1, \"test.txt\", \"test\", \"commit1\");\n\t\tpush_branch(\n\t\t\tclone1_dir, \"origin\", \"master\", false, false, None, None,\n\t\t)\n\t\t.unwrap();\n\n\t\tlet (clone2_dir, _clone2) = repo_clone(r1_dir).unwrap();\n\t\tlet clone2_dir: &RepoPath =\n\t\t\t&clone2_dir.path().to_str().unwrap().into();\n\n\t\t// clone1 - creates tag\n\n\t\tsync::tag_commit(clone1_dir, &commit1, \"tag1\", None).unwrap();\n\t\tpush_tags(clone1_dir, \"origin\", None, None).unwrap();\n\n\t\t// clone 2 - pull\n\n\t\tfetch_all(clone2_dir, &None, &None).unwrap();\n\t\tassert_eq!(sync::get_tags(clone2_dir).unwrap().len(), 1);\n\n\t\t// delete on clone 1\n\n\t\tdelete_tag(clone1_dir, \"tag1\").unwrap();\n\n\t\tpush_raw(\n\t\t\tclone1_dir,\n\t\t\t\"origin\",\n\t\t\t\"tag1\",\n\t\t\tPushType::Tag,\n\t\t\tfalse,\n\t\t\ttrue,\n\t\t\tNone,\n\t\t\tNone,\n\t\t)\n\t\t.unwrap();\n\n\t\tpush_tags(clone1_dir, \"origin\", None, None).unwrap();\n\n\t\t// clone 2\n\n\t\tfetch_all(clone2_dir, &None, &None).unwrap();\n\t\tassert_eq!(sync::get_tags(clone2_dir).unwrap().len(), 0);\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/repository.rs",
    "content": "use std::{\n\tcell::RefCell,\n\tpath::{Path, PathBuf},\n};\n\nuse git2::{Repository, RepositoryOpenFlags};\n\nuse crate::error::Result;\n\n///\npub type RepoPathRef = RefCell<RepoPath>;\n\n///\n#[derive(Clone, Debug)]\npub enum RepoPath {\n\t///\n\tPath(PathBuf),\n\t///\n\tWorkdir {\n\t\t///\n\t\tgitdir: PathBuf,\n\t\t///\n\t\tworkdir: PathBuf,\n\t},\n}\n\nimpl RepoPath {\n\t///\n\tpub fn gitpath(&self) -> &Path {\n\t\tmatch self {\n\t\t\tSelf::Path(p) => p.as_path(),\n\t\t\tSelf::Workdir { gitdir, .. } => gitdir.as_path(),\n\t\t}\n\t}\n\n\t///\n\tpub fn workdir(&self) -> Option<&Path> {\n\t\tmatch self {\n\t\t\tSelf::Path(_) => None,\n\t\t\tSelf::Workdir { workdir, .. } => Some(workdir.as_path()),\n\t\t}\n\t}\n}\n\nimpl From<PathBuf> for RepoPath {\n\tfn from(value: PathBuf) -> Self {\n\t\tSelf::Path(value)\n\t}\n}\n\nimpl From<&str> for RepoPath {\n\tfn from(p: &str) -> Self {\n\t\tSelf::Path(PathBuf::from(p))\n\t}\n}\n\npub fn repo(repo_path: &RepoPath) -> Result<Repository> {\n\tlet repo = Repository::open_ext(\n\t\trepo_path.gitpath(),\n\t\tRepositoryOpenFlags::FROM_ENV,\n\t\tVec::<&Path>::new(),\n\t)?;\n\n\tif let Some(workdir) = repo_path.workdir() {\n\t\trepo.set_workdir(workdir, false)?;\n\t}\n\n\tOk(repo)\n}\n\npub fn gix_repo(repo_path: &RepoPath) -> Result<gix::Repository> {\n\tlet mut repo: gix::Repository = gix::ThreadSafeRepository::discover_with_environment_overrides(\n\t\trepo_path.gitpath(),\n\t)\n\t.map(Into::into)?;\n\n\tif let Some(workdir) = repo_path.workdir() {\n\t\trepo.set_workdir(Some(workdir.into()))?;\n\t}\n\n\tOk(repo)\n}\n"
  },
  {
    "path": "asyncgit/src/sync/reset.rs",
    "content": "use super::{utils::get_head_repo, CommitId, RepoPath};\nuse crate::{error::Result, sync::repository::repo};\nuse git2::{build::CheckoutBuilder, ObjectType, ResetType};\nuse scopetime::scope_time;\n\n///\npub fn reset_stage(repo_path: &RepoPath, path: &str) -> Result<()> {\n\tscope_time!(\"reset_stage\");\n\n\tlet repo = repo(repo_path)?;\n\n\tif let Ok(id) = get_head_repo(&repo) {\n\t\tlet obj =\n\t\t\trepo.find_object(id.into(), Some(ObjectType::Commit))?;\n\n\t\trepo.reset_default(Some(&obj), [path])?;\n\t} else {\n\t\trepo.reset_default(None, [path])?;\n\t}\n\n\tOk(())\n}\n\n///\npub fn reset_workdir(repo_path: &RepoPath, path: &str) -> Result<()> {\n\tscope_time!(\"reset_workdir\");\n\n\tlet repo = repo(repo_path)?;\n\n\tlet mut checkout_opts = CheckoutBuilder::new();\n\tcheckout_opts\n\t\t.update_index(true) // windows: needs this to be true WTF?!\n\t\t.remove_untracked(true)\n\t\t.force()\n\t\t.path(path);\n\n\trepo.checkout_index(None, Some(&mut checkout_opts))?;\n\tOk(())\n}\n\n///\npub fn reset_repo(\n\trepo_path: &RepoPath,\n\tcommit: CommitId,\n\tkind: ResetType,\n) -> Result<()> {\n\tscope_time!(\"reset_repo\");\n\n\tlet repo = repo(repo_path)?;\n\n\tlet c = repo.find_commit(commit.into())?;\n\n\trepo.reset(c.as_object(), kind, None)?;\n\n\tOk(())\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::{reset_stage, reset_workdir};\n\tuse crate::error::Result;\n\tuse crate::sync::{\n\t\tcommit,\n\t\tstatus::{get_status, StatusType},\n\t\ttests::{\n\t\t\tdebug_cmd_print, get_statuses, repo_init, repo_init_empty,\n\t\t},\n\t\tutils::{stage_add_all, stage_add_file},\n\t\tRepoPath,\n\t};\n\tuse std::{\n\t\tfs::{self, File},\n\t\tio::Write,\n\t\tpath::Path,\n\t};\n\n\tstatic HUNK_A: &str = r\"\n1   start\n2\n3\n4\n5\n6   middle\n7\n8\n9\n0\n1   end\";\n\n\tstatic HUNK_B: &str = r\"\n1   start\n2   newa\n3\n4\n5\n6   middle\n7\n8\n9\n0   newb\n1   end\";\n\n\t#[test]\n\tfn test_reset_only_unstaged() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tlet res = get_status(repo_path, StatusType::WorkingDir, None)\n\t\t\t.unwrap();\n\t\tassert_eq!(res.len(), 0);\n\n\t\tlet file_path = root.join(\"bar.txt\");\n\n\t\t{\n\t\t\tFile::create(&file_path)\n\t\t\t\t.unwrap()\n\t\t\t\t.write_all(HUNK_A.as_bytes())\n\t\t\t\t.unwrap();\n\t\t}\n\n\t\tdebug_cmd_print(repo_path, \"git status\");\n\n\t\tstage_add_file(repo_path, Path::new(\"bar.txt\")).unwrap();\n\n\t\tdebug_cmd_print(repo_path, \"git status\");\n\n\t\t// overwrite with next content\n\t\t{\n\t\t\tFile::create(&file_path)\n\t\t\t\t.unwrap()\n\t\t\t\t.write_all(HUNK_B.as_bytes())\n\t\t\t\t.unwrap();\n\t\t}\n\n\t\tdebug_cmd_print(repo_path, \"git status\");\n\n\t\tassert_eq!(get_statuses(repo_path), (1, 1));\n\n\t\treset_workdir(repo_path, \"bar.txt\").unwrap();\n\n\t\tdebug_cmd_print(repo_path, \"git status\");\n\n\t\tassert_eq!(get_statuses(repo_path), (0, 1));\n\t}\n\n\t#[test]\n\tfn test_reset_untracked_in_subdir() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\t{\n\t\t\tfs::create_dir(root.join(\"foo\")).unwrap();\n\t\t\tFile::create(root.join(\"foo/bar.txt\"))\n\t\t\t\t.unwrap()\n\t\t\t\t.write_all(b\"test\\nfoo\")\n\t\t\t\t.unwrap();\n\t\t}\n\n\t\tdebug_cmd_print(repo_path, \"git status\");\n\n\t\tassert_eq!(get_statuses(repo_path), (1, 0));\n\n\t\treset_workdir(repo_path, \"foo/bar.txt\").unwrap();\n\n\t\tdebug_cmd_print(repo_path, \"git status\");\n\n\t\tassert_eq!(get_statuses(repo_path), (0, 0));\n\t}\n\n\t#[test]\n\tfn test_reset_folder() -> Result<()> {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\t{\n\t\t\tfs::create_dir(root.join(\"foo\"))?;\n\t\t\tFile::create(root.join(\"foo/file1.txt\"))?\n\t\t\t\t.write_all(b\"file1\")?;\n\t\t\tFile::create(root.join(\"foo/file2.txt\"))?\n\t\t\t\t.write_all(b\"file1\")?;\n\t\t\tFile::create(root.join(\"file3.txt\"))?\n\t\t\t\t.write_all(b\"file3\")?;\n\t\t}\n\n\t\tstage_add_all(repo_path, \"*\", None).unwrap();\n\t\tcommit(repo_path, \"msg\").unwrap();\n\n\t\t{\n\t\t\tFile::create(root.join(\"foo/file1.txt\"))?\n\t\t\t\t.write_all(b\"file1\\nadded line\")?;\n\t\t\tfs::remove_file(root.join(\"foo/file2.txt\"))?;\n\t\t\tFile::create(root.join(\"foo/file4.txt\"))?\n\t\t\t\t.write_all(b\"file4\")?;\n\t\t\tFile::create(root.join(\"foo/file5.txt\"))?\n\t\t\t\t.write_all(b\"file5\")?;\n\t\t\tFile::create(root.join(\"file3.txt\"))?\n\t\t\t\t.write_all(b\"file3\\nadded line\")?;\n\t\t}\n\n\t\tassert_eq!(get_statuses(repo_path), (5, 0));\n\n\t\tstage_add_file(repo_path, Path::new(\"foo/file5.txt\"))\n\t\t\t.unwrap();\n\n\t\tassert_eq!(get_statuses(repo_path), (4, 1));\n\n\t\treset_workdir(repo_path, \"foo\").unwrap();\n\n\t\tassert_eq!(get_statuses(repo_path), (1, 1));\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_reset_untracked_in_subdir_and_index() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\t\tlet file = \"foo/bar.txt\";\n\n\t\t{\n\t\t\tfs::create_dir(root.join(\"foo\")).unwrap();\n\t\t\tFile::create(root.join(file))\n\t\t\t\t.unwrap()\n\t\t\t\t.write_all(b\"test\\nfoo\")\n\t\t\t\t.unwrap();\n\t\t}\n\n\t\tdebug_cmd_print(repo_path, \"git status\");\n\n\t\tdebug_cmd_print(repo_path, \"git add .\");\n\n\t\tdebug_cmd_print(repo_path, \"git status\");\n\n\t\t{\n\t\t\tFile::create(root.join(file))\n\t\t\t\t.unwrap()\n\t\t\t\t.write_all(b\"test\\nfoo\\nnewend\")\n\t\t\t\t.unwrap();\n\t\t}\n\n\t\tdebug_cmd_print(repo_path, \"git status\");\n\n\t\tassert_eq!(get_statuses(repo_path), (1, 1));\n\n\t\treset_workdir(repo_path, file).unwrap();\n\n\t\tdebug_cmd_print(repo_path, \"git status\");\n\n\t\tassert_eq!(get_statuses(repo_path), (0, 1));\n\t}\n\n\t#[test]\n\tfn unstage_in_empty_repo() {\n\t\tlet file_path = Path::new(\"foo.txt\");\n\t\tlet (_td, repo) = repo_init_empty().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(file_path))\n\t\t\t.unwrap()\n\t\t\t.write_all(b\"test\\nfoo\")\n\t\t\t.unwrap();\n\n\t\tassert_eq!(get_statuses(repo_path), (1, 0));\n\n\t\tstage_add_file(repo_path, file_path).unwrap();\n\n\t\tassert_eq!(get_statuses(repo_path), (0, 1));\n\n\t\treset_stage(repo_path, file_path.to_str().unwrap()).unwrap();\n\n\t\tassert_eq!(get_statuses(repo_path), (1, 0));\n\t}\n\n\t#[test]\n\tfn test_reset_untracked_in_subdir_with_cwd_in_subdir() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\t{\n\t\t\tfs::create_dir(root.join(\"foo\")).unwrap();\n\t\t\tFile::create(root.join(\"foo/bar.txt\"))\n\t\t\t\t.unwrap()\n\t\t\t\t.write_all(b\"test\\nfoo\")\n\t\t\t\t.unwrap();\n\t\t}\n\n\t\tdebug_cmd_print(repo_path, \"git status\");\n\n\t\tassert_eq!(get_statuses(repo_path), (1, 0));\n\n\t\treset_workdir(\n\t\t\t&root.join(\"foo\").as_os_str().to_str().unwrap().into(),\n\t\t\t\"foo/bar.txt\",\n\t\t)\n\t\t.unwrap();\n\n\t\tdebug_cmd_print(repo_path, \"git status\");\n\n\t\tassert_eq!(get_statuses(repo_path), (0, 0));\n\t}\n\n\t#[test]\n\tfn test_reset_untracked_subdir() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\t{\n\t\t\tfs::create_dir_all(root.join(\"foo/bar\")).unwrap();\n\t\t\tFile::create(root.join(\"foo/bar/baz.txt\"))\n\t\t\t\t.unwrap()\n\t\t\t\t.write_all(b\"test\\nfoo\")\n\t\t\t\t.unwrap();\n\t\t}\n\n\t\tdebug_cmd_print(repo_path, \"git status\");\n\n\t\tassert_eq!(get_statuses(repo_path), (1, 0));\n\n\t\treset_workdir(repo_path, \"foo/bar\").unwrap();\n\n\t\tdebug_cmd_print(repo_path, \"git status\");\n\n\t\tassert_eq!(get_statuses(repo_path), (0, 0));\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/reword.rs",
    "content": "use git2::{Oid, RebaseOptions, Repository};\n\nuse super::{\n\tcommit::signature_allow_undefined_name,\n\trepo,\n\tutils::{bytes2string, get_head_refname, get_head_repo},\n\tCommitId, RepoPath,\n};\nuse crate::error::{Error, Result};\n\n/// This is the same as reword, but will abort and fix the repo if something goes wrong\npub fn reword(\n\trepo_path: &RepoPath,\n\tcommit: CommitId,\n\tmessage: &str,\n) -> Result<CommitId> {\n\tlet repo = repo(repo_path)?;\n\tlet config = repo.config()?;\n\n\tif config.get_bool(\"commit.gpgsign\").unwrap_or(false) {\n\t\t// HACK: we undo the last commit and create a new one\n\t\tuse crate::sync::utils::undo_last_commit;\n\n\t\tlet head = get_head_repo(&repo)?;\n\t\tif head == commit {\n\t\t\t// Check if there are any staged changes\n\t\t\tlet parent = repo.find_commit(head.into())?;\n\t\t\tlet tree = parent.tree()?;\n\t\t\tif repo\n\t\t\t\t.diff_tree_to_index(Some(&tree), None, None)?\n\t\t\t\t.deltas()\n\t\t\t\t.len() == 0\n\t\t\t{\n\t\t\t\tundo_last_commit(repo_path)?;\n\t\t\t\treturn super::commit(repo_path, message);\n\t\t\t}\n\n\t\t\treturn Err(Error::SignRewordLastCommitStaged);\n\t\t}\n\n\t\treturn Err(Error::SignRewordNonLastCommit);\n\t}\n\n\tlet cur_branch_ref = get_head_refname(&repo)?;\n\n\tmatch reword_internal(&repo, commit.get_oid(), message) {\n\t\tOk(id) => Ok(id.into()),\n\t\t// Something went wrong, checkout the previous branch then error\n\t\tErr(e) => {\n\t\t\tif let Ok(mut rebase) = repo.open_rebase(None) {\n\t\t\t\trebase.abort()?;\n\t\t\t\trepo.set_head(&cur_branch_ref)?;\n\t\t\t\trepo.checkout_head(None)?;\n\t\t\t}\n\t\t\tErr(e)\n\t\t}\n\t}\n}\n\n/// Gets the current branch the user is on.\n/// Returns none if they are not on a branch\n/// and Err if there was a problem finding the branch\nfn get_current_branch(\n\trepo: &Repository,\n) -> Result<Option<git2::Branch<'_>>> {\n\tfor b in repo.branches(None)? {\n\t\tlet branch = b?.0;\n\t\tif branch.is_head() {\n\t\t\treturn Ok(Some(branch));\n\t\t}\n\t}\n\tOk(None)\n}\n\n/// Changes the commit message of a commit with a specified oid\n///\n/// While this function is most commonly associated with doing a\n/// reword operation in an interactive rebase, that is not how it\n/// is implemented in git2rs\n///\n/// This is dangerous if it errors, as the head will be detached so this should\n/// always be wrapped by another function which aborts the rebase if something goes wrong\nfn reword_internal(\n\trepo: &Repository,\n\tcommit: Oid,\n\tmessage: &str,\n) -> Result<Oid> {\n\tlet sig = signature_allow_undefined_name(repo)?;\n\n\tlet parent_commit_oid = repo\n\t\t.find_commit(commit)?\n\t\t.parent(0)\n\t\t.map_or(None, |parent_commit| Some(parent_commit.id()));\n\n\tlet commit_to_change = if let Some(pc_oid) = parent_commit_oid {\n\t\t// Need to start at one previous to the commit, so\n\t\t// first rebase.next() points to the actual commit we want to change\n\t\trepo.find_annotated_commit(pc_oid)?\n\t} else {\n\t\treturn Err(Error::NoParent);\n\t};\n\n\t// If we are on a branch\n\tif let Ok(Some(branch)) = get_current_branch(repo) {\n\t\tlet cur_branch_ref = bytes2string(branch.get().name_bytes())?;\n\t\tlet cur_branch_name = bytes2string(branch.name_bytes()?)?;\n\t\tlet top_branch_commit = repo.find_annotated_commit(\n\t\t\tbranch.get().peel_to_commit()?.id(),\n\t\t)?;\n\n\t\tlet mut rebase = repo.rebase(\n\t\t\tSome(&top_branch_commit),\n\t\t\tSome(&commit_to_change),\n\t\t\tNone,\n\t\t\tSome(&mut RebaseOptions::default()),\n\t\t)?;\n\n\t\tlet mut target;\n\n\t\trebase.next();\n\t\tif parent_commit_oid.is_none() {\n\t\t\treturn Err(Error::NoParent);\n\t\t}\n\t\ttarget = rebase.commit(None, &sig, Some(message))?;\n\t\tlet reworded_commit = target;\n\n\t\t// Set target to top commit, don't know when the rebase will end\n\t\t// so have to loop till end\n\t\twhile rebase.next().is_some() {\n\t\t\ttarget = rebase.commit(None, &sig, None)?;\n\t\t}\n\t\trebase.finish(None)?;\n\n\t\t// Now override the previous branch\n\t\trepo.branch(\n\t\t\t&cur_branch_name,\n\t\t\t&repo.find_commit(target)?,\n\t\t\ttrue,\n\t\t)?;\n\n\t\t// Reset the head back to the branch then checkout head\n\t\trepo.set_head(&cur_branch_ref)?;\n\t\trepo.checkout_head(None)?;\n\t\treturn Ok(reworded_commit);\n\t}\n\t// Repo is not on a branch, possibly detached head\n\tErr(Error::NoBranch)\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::sync::{\n\t\tget_commit_info,\n\t\ttests::{repo_init_empty, write_commit_file},\n\t};\n\tuse pretty_assertions::assert_eq;\n\n\t#[test]\n\tfn test_reword() {\n\t\tlet (_td, repo) = repo_init_empty().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\twrite_commit_file(&repo, \"foo\", \"a\", \"commit1\");\n\n\t\tlet oid2 = write_commit_file(&repo, \"foo\", \"ab\", \"commit2\");\n\n\t\tlet branch =\n\t\t\trepo.branches(None).unwrap().next().unwrap().unwrap().0;\n\t\tlet branch_ref = branch.get();\n\t\tlet commit_ref = branch_ref.peel_to_commit().unwrap();\n\t\tlet message = commit_ref.message().unwrap();\n\n\t\tassert_eq!(message, \"commit2\");\n\n\t\tlet reworded =\n\t\t\treword(repo_path, oid2, \"NewCommitMessage\").unwrap();\n\n\t\t// Need to get the branch again as top oid has changed\n\t\tlet branch =\n\t\t\trepo.branches(None).unwrap().next().unwrap().unwrap().0;\n\t\tlet branch_ref = branch.get();\n\t\tlet commit_ref_new = branch_ref.peel_to_commit().unwrap();\n\t\tlet message_new = commit_ref_new.message().unwrap();\n\t\tassert_eq!(message_new, \"NewCommitMessage\");\n\n\t\tassert_eq!(\n\t\t\tmessage_new,\n\t\t\tget_commit_info(repo_path, &reworded).unwrap().message\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/sign.rs",
    "content": "//! Sign commit data.\n\nuse ssh_key::{HashAlg, LineEnding, PrivateKey};\nuse std::path::PathBuf;\n\n/// Error type for [`SignBuilder`], used to create [`Sign`]'s\n#[derive(thiserror::Error, Debug)]\npub enum SignBuilderError {\n\t/// The given format is invalid\n\t#[error(\"Failed to derive a commit signing method from git configuration 'gpg.format': {0}\")]\n\tInvalidFormat(String),\n\n\t/// The GPG signing key could\n\t#[error(\"Failed to retrieve 'user.signingkey' from the git configuration: {0}\")]\n\tGPGSigningKey(String),\n\n\t/// The SSH signing key could\n\t#[error(\"Failed to retrieve 'user.signingkey' from the git configuration: {0}\")]\n\tSSHSigningKey(String),\n\n\t/// No signing signature could be built from the configuration data present\n\t#[error(\"Failed to build signing signature: {0}\")]\n\tSignature(String),\n\n\t/// Failure on unimplemented signing methods\n\t/// to be removed once all methods have been implemented\n\t#[error(\"Select signing method '{0}' has not been implemented\")]\n\tMethodNotImplemented(String),\n}\n\n/// Error type for [`Sign`], used to sign data\n#[derive(thiserror::Error, Debug)]\npub enum SignError {\n\t/// Unable to spawn process\n\t#[error(\"Failed to spawn signing process: {0}\")]\n\tSpawn(String),\n\n\t/// Unable to acquire the child process' standard input to write the commit data for signing\n\t#[error(\"Failed to acquire standard input handler\")]\n\tStdin,\n\n\t/// Unable to write commit data to sign to standard input of the child process\n\t#[error(\"Failed to write buffer to standard input of signing process: {0}\")]\n\tWriteBuffer(String),\n\n\t/// Unable to retrieve the signed data from the child process\n\t#[error(\"Failed to get output of signing process call: {0}\")]\n\tOutput(String),\n\n\t/// Failure of the child process\n\t#[error(\"Failed to execute signing process: {0}\")]\n\tShellout(String),\n}\n\n/// Sign commit data using various methods\npub trait Sign {\n\t/// Sign commit with the respective implementation.\n\t///\n\t/// Retrieve an implementation using [`SignBuilder::from_gitconfig`].\n\t///\n\t/// The `commit` buffer can be created using the following steps:\n\t/// - create a buffer using [`git2::Repository::commit_create_buffer`]\n\t///\n\t/// The function returns a tuple of `signature` and `signature_field`.\n\t/// These values can then be passed into [`git2::Repository::commit_signed`].\n\t/// Finally, the repository head needs to be advanced to the resulting commit ID\n\t/// using [`git2::Reference::set_target`].\n\tfn sign(\n\t\t&self,\n\t\tcommit: &[u8],\n\t) -> Result<(String, Option<String>), SignError>;\n\n\t/// only available in `#[cfg(test)]` helping to diagnose issues\n\t#[cfg(test)]\n\tfn program(&self) -> &String;\n\n\t/// only available in `#[cfg(test)]` helping to diagnose issues\n\t#[cfg(test)]\n\tfn signing_key(&self) -> &String;\n}\n\n/// A builder to facilitate the creation of a signing method ([`Sign`]) by examining the git configuration.\npub struct SignBuilder;\n\nimpl SignBuilder {\n\t/// Get a [`Sign`] from the given repository configuration to sign commit data\n\t///\n\t///\n\t/// ```no_run\n\t/// use asyncgit::sync::sign::SignBuilder;\n\t/// # fn main() -> Result<(), Box<dyn std::error::Error>> {\n\t///\n\t/// /// Repo in a temporary directory for demonstration\n\t/// let dir = std::env::temp_dir();\n\t/// let repo = git2::Repository::init(dir)?;\n\t///\n\t/// /// Get the config from the repository\n\t/// let config = repo.config()?;\n\t///\n\t/// /// Retrieve a `Sign` implementation\n\t/// let sign = SignBuilder::from_gitconfig(&repo, &config)?;\n\t/// # Ok(())\n\t/// # }\n\t/// ```\n\tpub fn from_gitconfig(\n\t\trepo: &git2::Repository,\n\t\tconfig: &git2::Config,\n\t) -> Result<Box<dyn Sign>, SignBuilderError> {\n\t\tlet format = config\n\t\t\t.get_string(\"gpg.format\")\n\t\t\t.unwrap_or_else(|_| \"openpgp\".to_string());\n\n\t\t// Variants are described in the git config documentation\n\t\t// https://git-scm.com/docs/git-config#Documentation/git-config.txt-gpgformat\n\t\tmatch format.as_str() {\n\t\t\t\"openpgp\" => {\n\t\t\t\t// Try to retrieve the gpg program from the git configuration,\n\t\t\t\t// moving from the least to the most specific config key,\n\t\t\t\t// defaulting to \"gpg\" if nothing is explicitly defined (per git's implementation)\n\t\t\t\t// https://git-scm.com/docs/git-config#Documentation/git-config.txt-gpgprogram\n\t\t\t\t// https://git-scm.com/docs/git-config#Documentation/git-config.txt-gpgprogram\n\t\t\t\tlet program = config\n\t\t\t\t\t.get_string(\"gpg.openpgp.program\")\n\t\t\t\t\t.or_else(|_| config.get_string(\"gpg.program\"))\n\t\t\t\t\t.unwrap_or_else(|_| \"gpg\".to_string());\n\n\t\t\t\t// Optional signing key.\n\t\t\t\t// If 'user.signingKey' is not set, we'll use 'user.name' and 'user.email'\n\t\t\t\t// to build a default signature in the format 'name <email>'.\n\t\t\t\t// https://git-scm.com/docs/git-config#Documentation/git-config.txt-usersigningKey\n\t\t\t\tlet signing_key = config\n\t\t\t\t\t.get_string(\"user.signingKey\")\n\t\t\t\t\t.or_else(\n\t\t\t\t\t\t|_| -> Result<String, SignBuilderError> {\n\t\t\t\t\t\t\tOk(crate::sync::commit::signature_allow_undefined_name(repo)\n                                .map_err(|err| {\n                                    SignBuilderError::Signature(\n                                        err.to_string(),\n                                    )\n                                })?\n                                .to_string())\n\t\t\t\t\t\t},\n\t\t\t\t\t)\n\t\t\t\t\t.map_err(|err| {\n\t\t\t\t\t\tSignBuilderError::GPGSigningKey(\n\t\t\t\t\t\t\terr.to_string(),\n\t\t\t\t\t\t)\n\t\t\t\t\t})?;\n\n\t\t\t\tOk(Box::new(GPGSign {\n\t\t\t\t\tprogram,\n\t\t\t\t\tsigning_key,\n\t\t\t\t}))\n\t\t\t}\n\t\t\t\"x509\" => Err(SignBuilderError::MethodNotImplemented(\n\t\t\t\tString::from(\"x509\"),\n\t\t\t)),\n\t\t\t\"ssh\" => {\n\t\t\t\tlet ssh_signer = config\n\t\t\t\t\t.get_string(\"user.signingKey\")\n\t\t\t\t\t.ok()\n\t\t\t\t\t.and_then(|key_path| {\n\t\t\t\t\t\tkey_path.strip_prefix('~').map_or_else(\n\t\t\t\t\t\t\t|| Some(PathBuf::from(&key_path)),\n\t\t\t\t\t\t\t|ssh_key_path| {\n\t\t\t\t\t\t\t\tdirs::home_dir().map(|home| {\n\t\t\t\t\t\t\t\t\thome.join(\n\t\t\t\t\t\t\t\t\t\tssh_key_path\n\t\t\t\t\t\t\t\t\t\t\t.strip_prefix('/')\n\t\t\t\t\t\t\t\t\t\t\t.unwrap_or(ssh_key_path),\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t)\n\t\t\t\t\t})\n\t\t\t\t\t.ok_or_else(|| {\n\t\t\t\t\t\tSignBuilderError::SSHSigningKey(String::from(\n\t\t\t\t\t\t\t\"ssh key setting absent\",\n\t\t\t\t\t\t))\n\t\t\t\t\t})\n\t\t\t\t\t.and_then(SSHSign::new)?;\n\t\t\t\tlet signer: Box<dyn Sign> = Box::new(ssh_signer);\n\t\t\t\tOk(signer)\n\t\t\t}\n\t\t\t_ => Err(SignBuilderError::InvalidFormat(format)),\n\t\t}\n\t}\n}\n\n/// Sign commit data using `OpenPGP`\npub struct GPGSign {\n\tprogram: String,\n\tsigning_key: String,\n}\n\nimpl GPGSign {\n\t/// Create new [`GPGSign`] using given program and signing key.\n\tpub fn new(program: &str, signing_key: &str) -> Self {\n\t\tSelf {\n\t\t\tprogram: program.to_string(),\n\t\t\tsigning_key: signing_key.to_string(),\n\t\t}\n\t}\n}\n\nimpl Sign for GPGSign {\n\tfn sign(\n\t\t&self,\n\t\tcommit: &[u8],\n\t) -> Result<(String, Option<String>), SignError> {\n\t\tuse std::io::Write;\n\t\tuse std::process::{Command, Stdio};\n\n\t\tlet mut cmd = Command::new(&self.program);\n\t\tcmd.stdin(Stdio::piped())\n\t\t\t.stdout(Stdio::piped())\n\t\t\t.stderr(Stdio::piped())\n\t\t\t.arg(\"--status-fd=2\")\n\t\t\t.arg(\"-bsau\")\n\t\t\t.arg(&self.signing_key);\n\n\t\tlog::trace!(\"signing command: {cmd:?}\");\n\n\t\tlet mut child = cmd\n\t\t\t.spawn()\n\t\t\t.map_err(|e| SignError::Spawn(e.to_string()))?;\n\n\t\tlet mut stdin = child.stdin.take().ok_or(SignError::Stdin)?;\n\n\t\tstdin\n\t\t\t.write_all(commit)\n\t\t\t.map_err(|e| SignError::WriteBuffer(e.to_string()))?;\n\t\tdrop(stdin); // close stdin to not block indefinitely\n\n\t\tlet output = child\n\t\t\t.wait_with_output()\n\t\t\t.map_err(|e| SignError::Output(e.to_string()))?;\n\n\t\tif !output.status.success() {\n\t\t\treturn Err(SignError::Shellout(format!(\n\t\t\t\t\"failed to sign data, program '{}' exited non-zero: {}\",\n\t\t\t\t&self.program,\n\t\t\t\tstd::str::from_utf8(&output.stderr)\n\t\t\t\t\t.unwrap_or(\"[error could not be read from stderr]\")\n\t\t\t)));\n\t\t}\n\n\t\tlet stderr = std::str::from_utf8(&output.stderr)\n\t\t\t.map_err(|e| SignError::Shellout(e.to_string()))?;\n\n\t\tif !stderr.contains(\"\\n[GNUPG:] SIG_CREATED \") {\n\t\t\treturn Err(SignError::Shellout(\n\t\t\t\tformat!(\"failed to sign data, program '{}' failed, SIG_CREATED not seen in stderr\", &self.program),\n\t\t\t));\n\t\t}\n\n\t\tlet signed_commit = std::str::from_utf8(&output.stdout)\n\t\t\t.map_err(|e| SignError::Shellout(e.to_string()))?;\n\n\t\tOk((signed_commit.to_string(), Some(\"gpgsig\".to_string())))\n\t}\n\n\t#[cfg(test)]\n\tfn program(&self) -> &String {\n\t\t&self.program\n\t}\n\n\t#[cfg(test)]\n\tfn signing_key(&self) -> &String {\n\t\t&self.signing_key\n\t}\n}\n\n/// Sign commit data using `SSHDiskKeySign`\npub struct SSHSign {\n\t#[cfg(test)]\n\tprogram: String,\n\t#[cfg(test)]\n\tkey_path: String,\n\tsecret_key: PrivateKey,\n}\n\nimpl SSHSign {\n\t/// Create new `SSHDiskKeySign` for sign.\n\tpub fn new(mut key: PathBuf) -> Result<Self, SignBuilderError> {\n\t\tkey.set_extension(\"\");\n\t\tif key.is_file() {\n\t\t\t#[cfg(test)]\n\t\t\tlet key_path = format!(\"{}\", &key.display());\n\t\t\tstd::fs::read(key)\n\t\t\t\t.ok()\n\t\t\t\t.and_then(|bytes| {\n\t\t\t\t\tPrivateKey::from_openssh(bytes).ok()\n\t\t\t\t})\n\t\t\t\t.map(|secret_key| Self {\n\t\t\t\t\t#[cfg(test)]\n\t\t\t\t\tprogram: \"ssh\".to_string(),\n\t\t\t\t\t#[cfg(test)]\n\t\t\t\t\tkey_path,\n\t\t\t\t\tsecret_key,\n\t\t\t\t})\n\t\t\t\t.ok_or_else(|| {\n\t\t\t\t\tSignBuilderError::SSHSigningKey(String::from(\n\t\t\t\t\t\t\"Fail to read the private key for sign.\",\n\t\t\t\t\t))\n\t\t\t\t})\n\t\t} else {\n\t\t\tErr(SignBuilderError::SSHSigningKey(\n\t\t\t\tString::from(\"Currently, we only support a pair of ssh key in disk.\"),\n\t\t\t))\n\t\t}\n\t}\n}\n\nimpl Sign for SSHSign {\n\tfn sign(\n\t\t&self,\n\t\tcommit: &[u8],\n\t) -> Result<(String, Option<String>), SignError> {\n\t\tlet sig = self\n\t\t\t.secret_key\n\t\t\t.sign(\"git\", HashAlg::Sha256, commit)\n\t\t\t.map_err(|err| SignError::Spawn(err.to_string()))?\n\t\t\t.to_pem(LineEnding::LF)\n\t\t\t.map_err(|err| SignError::Spawn(err.to_string()))?;\n\t\tOk((sig, None))\n\t}\n\n\t#[cfg(test)]\n\tfn program(&self) -> &String {\n\t\t&self.program\n\t}\n\n\t#[cfg(test)]\n\tfn signing_key(&self) -> &String {\n\t\t&self.key_path\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::error::Result;\n\tuse crate::sync::tests::repo_init_empty;\n\n\t#[test]\n\tfn test_invalid_signing_format() -> Result<()> {\n\t\tlet (_temp_dir, repo) = repo_init_empty()?;\n\n\t\t{\n\t\t\tlet mut config = repo.config()?;\n\t\t\tconfig.set_str(\"gpg.format\", \"INVALID_SIGNING_FORMAT\")?;\n\t\t}\n\n\t\tlet sign =\n\t\t\tSignBuilder::from_gitconfig(&repo, &repo.config()?);\n\n\t\tassert!(sign.is_err());\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_program_and_signing_key_defaults() -> Result<()> {\n\t\tlet (_tmp_dir, repo) = repo_init_empty()?;\n\t\tlet sign =\n\t\t\tSignBuilder::from_gitconfig(&repo, &repo.config()?)?;\n\n\t\tassert_eq!(\"gpg\", sign.program());\n\t\tassert_eq!(\"name <email>\", sign.signing_key());\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_gpg_program_configs() -> Result<()> {\n\t\tlet (_tmp_dir, repo) = repo_init_empty()?;\n\n\t\t{\n\t\t\tlet mut config = repo.config()?;\n\t\t\tconfig.set_str(\"gpg.program\", \"GPG_PROGRAM_TEST\")?;\n\t\t}\n\n\t\tlet sign =\n\t\t\tSignBuilder::from_gitconfig(&repo, &repo.config()?)?;\n\n\t\t// we get gpg.program, because gpg.openpgp.program is not set\n\t\tassert_eq!(\"GPG_PROGRAM_TEST\", sign.program());\n\n\t\t{\n\t\t\tlet mut config = repo.config()?;\n\t\t\tconfig.set_str(\n\t\t\t\t\"gpg.openpgp.program\",\n\t\t\t\t\"GPG_OPENPGP_PROGRAM_TEST\",\n\t\t\t)?;\n\t\t}\n\n\t\tlet sign =\n\t\t\tSignBuilder::from_gitconfig(&repo, &repo.config()?)?;\n\n\t\t// since gpg.openpgp.program is now set as well, it is more specific than\n\t\t// gpg.program and therefore takes precedence\n\t\tassert_eq!(\"GPG_OPENPGP_PROGRAM_TEST\", sign.program());\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_user_signingkey() -> Result<()> {\n\t\tlet (_tmp_dir, repo) = repo_init_empty()?;\n\n\t\t{\n\t\t\tlet mut config = repo.config()?;\n\t\t\tconfig.set_str(\"user.signingKey\", \"FFAA\")?;\n\t\t}\n\n\t\tlet sign =\n\t\t\tSignBuilder::from_gitconfig(&repo, &repo.config()?)?;\n\n\t\tassert_eq!(\"FFAA\", sign.signing_key());\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_ssh_program_configs() -> Result<()> {\n\t\tlet (_tmp_dir, repo) = repo_init_empty()?;\n\n\t\t{\n\t\t\tlet mut config = repo.config()?;\n\t\t\tconfig.set_str(\"gpg.program\", \"ssh\")?;\n\t\t\tconfig.set_str(\"user.signingKey\", \"/tmp/key.pub\")?;\n\t\t}\n\n\t\tlet sign =\n\t\t\tSignBuilder::from_gitconfig(&repo, &repo.config()?)?;\n\n\t\tassert_eq!(\"ssh\", sign.program());\n\t\tassert_eq!(\"/tmp/key.pub\", sign.signing_key());\n\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/staging/discard_tracked.rs",
    "content": "use super::{apply_selection, load_file};\nuse crate::{\n\terror::Result,\n\tsync::{\n\t\tdiff::DiffLinePosition, patches::get_file_diff_patch,\n\t\tpatches::patch_get_hunklines, repository::repo,\n\t\tutils::repo_write_file, RepoPath,\n\t},\n};\nuse scopetime::scope_time;\n\n/// discards specific lines in an unstaged hunk of a diff\npub fn discard_lines(\n\trepo_path: &RepoPath,\n\tfile_path: &str,\n\tlines: &[DiffLinePosition],\n) -> Result<()> {\n\tscope_time!(\"discard_lines\");\n\n\tif lines.is_empty() {\n\t\treturn Ok(());\n\t}\n\n\tlet repo = repo(repo_path)?;\n\trepo.index()?.read(true)?;\n\n\t//TODO: check that file is not new (status modified)\n\n\tlet new_content = {\n\t\tlet patch =\n\t\t\tget_file_diff_patch(&repo, file_path, false, false)?;\n\t\tlet hunks = patch_get_hunklines(&patch)?;\n\n\t\tlet working_content = load_file(&repo, file_path)?;\n\t\tlet old_lines = working_content.lines().collect::<Vec<_>>();\n\n\t\tapply_selection(lines, &hunks, &old_lines, false, true)?\n\t};\n\n\trepo_write_file(&repo, file_path, new_content.as_str())?;\n\n\tOk(())\n}\n\n#[cfg(test)]\nmod test {\n\tuse super::*;\n\tuse crate::sync::tests::{repo_init, write_commit_file};\n\n\t#[test]\n\tfn test_discard() {\n\t\tstatic FILE_1: &str = r\"0\n1\n2\n3\n4\n\";\n\n\t\tstatic FILE_2: &str = r\"0\n\n\n3\n4\n\";\n\n\t\tstatic FILE_3: &str = r\"0\n2\n\n3\n4\n\";\n\n\t\tlet (path, repo) = repo_init().unwrap();\n\t\tlet path: &RepoPath = &path.path().to_str().unwrap().into();\n\n\t\twrite_commit_file(&repo, \"test.txt\", FILE_1, \"c1\");\n\n\t\trepo_write_file(&repo, \"test.txt\", FILE_2).unwrap();\n\n\t\tdiscard_lines(\n\t\t\tpath,\n\t\t\t\"test.txt\",\n\t\t\t&[\n\t\t\t\tDiffLinePosition {\n\t\t\t\t\told_lineno: Some(3),\n\t\t\t\t\tnew_lineno: None,\n\t\t\t\t},\n\t\t\t\tDiffLinePosition {\n\t\t\t\t\told_lineno: None,\n\t\t\t\t\tnew_lineno: Some(2),\n\t\t\t\t},\n\t\t\t],\n\t\t)\n\t\t.unwrap();\n\n\t\tlet result_file = load_file(&repo, \"test.txt\").unwrap();\n\n\t\tassert_eq!(result_file.as_str(), FILE_3);\n\t}\n\n\t#[test]\n\tfn test_discard2() {\n\t\tstatic FILE_1: &str = r\"start\nend\n\";\n\n\t\tstatic FILE_2: &str = r\"start\n1\n2\nend\n\";\n\n\t\tstatic FILE_3: &str = r\"start\n1\nend\n\";\n\n\t\tlet (path, repo) = repo_init().unwrap();\n\t\tlet path: &RepoPath = &path.path().to_str().unwrap().into();\n\n\t\twrite_commit_file(&repo, \"test.txt\", FILE_1, \"c1\");\n\n\t\trepo_write_file(&repo, \"test.txt\", FILE_2).unwrap();\n\n\t\tdiscard_lines(\n\t\t\tpath,\n\t\t\t\"test.txt\",\n\t\t\t&[DiffLinePosition {\n\t\t\t\told_lineno: None,\n\t\t\t\tnew_lineno: Some(3),\n\t\t\t}],\n\t\t)\n\t\t.unwrap();\n\n\t\tlet result_file = load_file(&repo, \"test.txt\").unwrap();\n\n\t\tassert_eq!(result_file.as_str(), FILE_3);\n\t}\n\n\t#[test]\n\tfn test_discard3() {\n\t\tstatic FILE_1: &str = r\"start\n1\nend\n\";\n\n\t\tstatic FILE_2: &str = r\"start\n2\nend\n\";\n\n\t\tstatic FILE_3: &str = r\"start\n1\nend\n\";\n\n\t\tlet (path, repo) = repo_init().unwrap();\n\t\tlet path: &RepoPath = &path.path().to_str().unwrap().into();\n\n\t\twrite_commit_file(&repo, \"test.txt\", FILE_1, \"c1\");\n\n\t\trepo_write_file(&repo, \"test.txt\", FILE_2).unwrap();\n\n\t\tdiscard_lines(\n\t\t\tpath,\n\t\t\t\"test.txt\",\n\t\t\t&[\n\t\t\t\tDiffLinePosition {\n\t\t\t\t\told_lineno: Some(2),\n\t\t\t\t\tnew_lineno: None,\n\t\t\t\t},\n\t\t\t\tDiffLinePosition {\n\t\t\t\t\told_lineno: None,\n\t\t\t\t\tnew_lineno: Some(2),\n\t\t\t\t},\n\t\t\t],\n\t\t)\n\t\t.unwrap();\n\n\t\tlet result_file = load_file(&repo, \"test.txt\").unwrap();\n\n\t\tassert_eq!(result_file.as_str(), FILE_3);\n\t}\n\n\t#[test]\n\tfn test_discard4() {\n\t\tstatic FILE_1: &str = r\"start\nmid\nend\n\";\n\n\t\tstatic FILE_2: &str = r\"start\n1\nmid\n2\nend\n\";\n\n\t\tstatic FILE_3: &str = r\"start\nmid\nend\n\";\n\n\t\tlet (path, repo) = repo_init().unwrap();\n\t\tlet path: &RepoPath = &path.path().to_str().unwrap().into();\n\n\t\twrite_commit_file(&repo, \"test.txt\", FILE_1, \"c1\");\n\n\t\trepo_write_file(&repo, \"test.txt\", FILE_2).unwrap();\n\n\t\tdiscard_lines(\n\t\t\tpath,\n\t\t\t\"test.txt\",\n\t\t\t&[\n\t\t\t\tDiffLinePosition {\n\t\t\t\t\told_lineno: None,\n\t\t\t\t\tnew_lineno: Some(2),\n\t\t\t\t},\n\t\t\t\tDiffLinePosition {\n\t\t\t\t\told_lineno: None,\n\t\t\t\t\tnew_lineno: Some(4),\n\t\t\t\t},\n\t\t\t],\n\t\t)\n\t\t.unwrap();\n\n\t\tlet result_file = load_file(&repo, \"test.txt\").unwrap();\n\n\t\tassert_eq!(result_file.as_str(), FILE_3);\n\t}\n\n\t#[test]\n\tfn test_discard_if_first_selected_line_is_not_in_any_hunk() {\n\t\tstatic FILE_1: &str = r\"start\nend\n\";\n\n\t\tstatic FILE_2: &str = r\"start\n1\nend\n\";\n\n\t\tstatic FILE_3: &str = r\"start\nend\n\";\n\n\t\tlet (path, repo) = repo_init().unwrap();\n\t\tlet path: &RepoPath = &path.path().to_str().unwrap().into();\n\n\t\twrite_commit_file(&repo, \"test.txt\", FILE_1, \"c1\");\n\n\t\trepo_write_file(&repo, \"test.txt\", FILE_2).unwrap();\n\n\t\tdiscard_lines(\n\t\t\tpath,\n\t\t\t\"test.txt\",\n\t\t\t&[\n\t\t\t\tDiffLinePosition {\n\t\t\t\t\told_lineno: None,\n\t\t\t\t\tnew_lineno: Some(1),\n\t\t\t\t},\n\t\t\t\tDiffLinePosition {\n\t\t\t\t\told_lineno: None,\n\t\t\t\t\tnew_lineno: Some(2),\n\t\t\t\t},\n\t\t\t],\n\t\t)\n\t\t.unwrap();\n\n\t\tlet result_file = load_file(&repo, \"test.txt\").unwrap();\n\n\t\tassert_eq!(result_file.as_str(), FILE_3);\n\t}\n\n\t//this test shows that we require at least a diff context around add/removes of 1\n\t#[test]\n\tfn test_discard_deletions_filestart_breaking_with_zero_context() {\n\t\tstatic FILE_1: &str = r\"start\nmid\nend\n\";\n\n\t\tstatic FILE_2: &str = r\"start\nend\n\";\n\n\t\tstatic FILE_3: &str = r\"start\nmid\nend\n\";\n\n\t\tlet (path, repo) = repo_init().unwrap();\n\t\tlet path: &RepoPath = &path.path().to_str().unwrap().into();\n\n\t\twrite_commit_file(&repo, \"test.txt\", FILE_1, \"c1\");\n\n\t\trepo_write_file(&repo, \"test.txt\", FILE_2).unwrap();\n\n\t\tdiscard_lines(\n\t\t\tpath,\n\t\t\t\"test.txt\",\n\t\t\t&[DiffLinePosition {\n\t\t\t\told_lineno: Some(2),\n\t\t\t\tnew_lineno: None,\n\t\t\t}],\n\t\t)\n\t\t.unwrap();\n\n\t\tlet result_file = load_file(&repo, \"test.txt\").unwrap();\n\n\t\tassert_eq!(result_file.as_str(), FILE_3);\n\t}\n\n\t#[test]\n\tfn test_discard5() {\n\t\tstatic FILE_1: &str = r\"start\n\";\n\n\t\tstatic FILE_2: &str = r\"start\n1\";\n\n\t\tstatic FILE_3: &str = r\"start\n\";\n\n\t\tlet (path, repo) = repo_init().unwrap();\n\t\tlet path: &RepoPath = &path.path().to_str().unwrap().into();\n\n\t\twrite_commit_file(&repo, \"test.txt\", FILE_1, \"c1\");\n\n\t\trepo_write_file(&repo, \"test.txt\", FILE_2).unwrap();\n\n\t\tdiscard_lines(\n\t\t\tpath,\n\t\t\t\"test.txt\",\n\t\t\t&[DiffLinePosition {\n\t\t\t\told_lineno: None,\n\t\t\t\tnew_lineno: Some(2),\n\t\t\t}],\n\t\t)\n\t\t.unwrap();\n\n\t\tlet result_file = load_file(&repo, \"test.txt\").unwrap();\n\n\t\tassert_eq!(result_file.as_str(), FILE_3);\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/staging/mod.rs",
    "content": "mod discard_tracked;\nmod stage_tracked;\n\npub use discard_tracked::discard_lines;\npub use stage_tracked::stage_lines;\n\nuse super::{\n\tdiff::DiffLinePosition, patches::HunkLines, utils::work_dir,\n};\nuse crate::error::Result;\nuse git2::{DiffLine, DiffLineType, Repository};\nuse std::{collections::HashSet, fs::File, io::Read};\n\nconst NEWLINE: char = '\\n';\n\n#[derive(Default)]\nstruct NewFromOldContent {\n\tlines: Vec<String>,\n\told_index: usize,\n}\n\nimpl NewFromOldContent {\n\tfn add_from_hunk(&mut self, line: &DiffLine) -> Result<()> {\n\t\tlet line = String::from_utf8(line.content().into())?;\n\n\t\tlet line = if line.ends_with(NEWLINE) {\n\t\t\tline[0..line.len() - 1].to_string()\n\t\t} else {\n\t\t\tline\n\t\t};\n\n\t\tself.lines.push(line);\n\n\t\tOk(())\n\t}\n\n\tconst fn skip_old_line(&mut self) {\n\t\tself.old_index += 1;\n\t}\n\n\tfn add_old_line(&mut self, old_lines: &[&str]) {\n\t\tself.lines.push(old_lines[self.old_index].to_string());\n\t\tself.old_index += 1;\n\t}\n\n\tfn catchup_to_hunkstart(\n\t\t&mut self,\n\t\thunk_start: usize,\n\t\told_lines: &[&str],\n\t) {\n\t\twhile hunk_start > self.old_index + 1 {\n\t\t\tself.add_old_line(old_lines);\n\t\t}\n\t}\n\n\tfn finish(mut self, old_lines: &[&str]) -> String {\n\t\tfor line in old_lines.iter().skip(self.old_index) {\n\t\t\tself.lines.push((*line).to_string());\n\t\t}\n\t\tlet lines = self.lines.join(\"\\n\");\n\t\tif lines.ends_with(NEWLINE) {\n\t\t\tlines\n\t\t} else {\n\t\t\tlet mut lines = lines;\n\t\t\tlines.push(NEWLINE);\n\t\t\tlines\n\t\t}\n\t}\n}\n\n// this is the heart of the per line discard,stage,unstage. heavily inspired by the great work in\n// nodegit: https://github.com/nodegit/nodegit\npub fn apply_selection(\n\tlines: &[DiffLinePosition],\n\thunks: &[HunkLines],\n\told_lines: &[&str],\n\tis_staged: bool,\n\treverse: bool,\n) -> Result<String> {\n\tlet mut new_content = NewFromOldContent::default();\n\tlet lines = lines.iter().collect::<HashSet<_>>();\n\n\tlet added = if reverse {\n\t\tDiffLineType::Deletion\n\t} else {\n\t\tDiffLineType::Addition\n\t};\n\tlet deleted = if reverse {\n\t\tDiffLineType::Addition\n\t} else {\n\t\tDiffLineType::Deletion\n\t};\n\n\tlet mut first_hunk_encountered = false;\n\tfor hunk in hunks {\n\t\tlet hunk_start = if is_staged || reverse {\n\t\t\tusize::try_from(hunk.hunk.new_start)?\n\t\t} else {\n\t\t\tusize::try_from(hunk.hunk.old_start)?\n\t\t};\n\n\t\tif !first_hunk_encountered {\n\t\t\tlet any_selection_in_hunk =\n\t\t\t\thunk.lines.iter().any(|line| {\n\t\t\t\t\tlet line: DiffLinePosition = line.into();\n\t\t\t\t\tlines.contains(&line)\n\t\t\t\t});\n\n\t\t\tfirst_hunk_encountered = any_selection_in_hunk;\n\t\t}\n\n\t\tif first_hunk_encountered {\n\t\t\tnew_content.catchup_to_hunkstart(hunk_start, old_lines);\n\n\t\t\tfor hunk_line in &hunk.lines {\n\t\t\t\tlet hunk_line_pos: DiffLinePosition =\n\t\t\t\t\thunk_line.into();\n\t\t\t\tlet selected_line = lines.contains(&hunk_line_pos);\n\n\t\t\t\tlog::debug!(\n\t\t\t\t\t// println!(\n\t\t\t\t\t\"{} line: {} [{:?} old, {:?} new] -> {}\",\n\t\t\t\t\tif selected_line { \"*\" } else { \" \" },\n\t\t\t\t\thunk_line.origin(),\n\t\t\t\t\thunk_line.old_lineno(),\n\t\t\t\t\thunk_line.new_lineno(),\n\t\t\t\t\tString::from_utf8_lossy(hunk_line.content())\n\t\t\t\t\t\t.trim()\n\t\t\t\t);\n\n\t\t\t\tif hunk_line.origin_value()\n\t\t\t\t\t== DiffLineType::DeleteEOFNL\n\t\t\t\t\t|| hunk_line.origin_value()\n\t\t\t\t\t\t== DiffLineType::AddEOFNL\n\t\t\t\t{\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tif (is_staged && !selected_line)\n\t\t\t\t\t|| (!is_staged && selected_line)\n\t\t\t\t{\n\t\t\t\t\tif hunk_line.origin_value() == added {\n\t\t\t\t\t\tnew_content.add_from_hunk(hunk_line)?;\n\t\t\t\t\t\tif is_staged {\n\t\t\t\t\t\t\tnew_content.skip_old_line();\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if hunk_line.origin_value() == deleted {\n\t\t\t\t\t\tif !is_staged {\n\t\t\t\t\t\t\tnew_content.skip_old_line();\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tnew_content.add_old_line(old_lines);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif hunk_line.origin_value() != added {\n\t\t\t\t\t\tnew_content.add_from_hunk(hunk_line)?;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (is_staged\n\t\t\t\t\t\t&& hunk_line.origin_value() != deleted)\n\t\t\t\t\t\t|| (!is_staged\n\t\t\t\t\t\t\t&& hunk_line.origin_value() != added)\n\t\t\t\t\t{\n\t\t\t\t\t\tnew_content.skip_old_line();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tOk(new_content.finish(old_lines))\n}\n\npub fn load_file(\n\trepo: &Repository,\n\tfile_path: &str,\n) -> Result<String> {\n\tlet repo_path = work_dir(repo)?;\n\tlet mut file = File::open(repo_path.join(file_path).as_path())?;\n\tlet mut res = String::new();\n\tfile.read_to_string(&mut res)?;\n\n\tOk(res)\n}\n"
  },
  {
    "path": "asyncgit/src/sync/staging/stage_tracked.rs",
    "content": "use super::apply_selection;\nuse crate::{\n\terror::{Error, Result},\n\tsync::{\n\t\tdiff::DiffLinePosition, patches::get_file_diff_patch,\n\t\tpatches::patch_get_hunklines, repository::repo, RepoPath,\n\t},\n};\nuse easy_cast::Conv;\nuse scopetime::scope_time;\nuse std::path::Path;\n\n///\npub fn stage_lines(\n\trepo_path: &RepoPath,\n\tfile_path: &str,\n\tis_stage: bool,\n\tlines: &[DiffLinePosition],\n) -> Result<()> {\n\tscope_time!(\"stage_lines\");\n\n\tif lines.is_empty() {\n\t\treturn Ok(());\n\t}\n\n\tlet repo = repo(repo_path)?;\n\t// log::debug!(\"stage_lines: {:?}\", lines);\n\n\tlet mut index = repo.index()?;\n\tindex.read(true)?;\n\tlet mut idx =\n\t\tindex.get_path(Path::new(file_path), 0).ok_or_else(|| {\n\t\t\tError::Generic(String::from(\n\t\t\t\t\"only non new files supported\",\n\t\t\t))\n\t\t})?;\n\tlet blob = repo.find_blob(idx.id)?;\n\tlet indexed_content = String::from_utf8(blob.content().into())?;\n\n\tlet new_content = {\n\t\tlet patch =\n\t\t\tget_file_diff_patch(&repo, file_path, is_stage, false)?;\n\t\tlet hunks = patch_get_hunklines(&patch)?;\n\n\t\tlet old_lines = indexed_content.lines().collect::<Vec<_>>();\n\n\t\tapply_selection(lines, &hunks, &old_lines, is_stage, false)?\n\t};\n\n\tlet blob_id = repo.blob(new_content.as_bytes())?;\n\n\tidx.id = blob_id;\n\tidx.file_size = u32::try_conv(new_content.len())?;\n\tindex.add(&idx)?;\n\n\tindex.write()?;\n\tindex.read(true)?;\n\n\tOk(())\n}\n\n#[cfg(test)]\nmod test {\n\tuse super::*;\n\tuse crate::sync::{\n\t\tdiff::get_diff,\n\t\ttests::{get_statuses, repo_init, write_commit_file},\n\t\tutils::{repo_write_file, stage_add_file},\n\t};\n\n\t#[test]\n\tfn test_stage() {\n\t\tstatic FILE_1: &str = r\"0\n\";\n\n\t\tstatic FILE_2: &str = r\"0\n1\n2\n3\n\";\n\n\t\tlet (path, repo) = repo_init().unwrap();\n\t\tlet path: &RepoPath = &path.path().to_str().unwrap().into();\n\n\t\twrite_commit_file(&repo, \"test.txt\", FILE_1, \"c1\");\n\n\t\trepo_write_file(&repo, \"test.txt\", FILE_2).unwrap();\n\n\t\tstage_lines(\n\t\t\tpath,\n\t\t\t\"test.txt\",\n\t\t\tfalse,\n\t\t\t&[DiffLinePosition {\n\t\t\t\told_lineno: None,\n\t\t\t\tnew_lineno: Some(2),\n\t\t\t}],\n\t\t)\n\t\t.unwrap();\n\n\t\tlet diff = get_diff(path, \"test.txt\", true, None).unwrap();\n\n\t\tassert_eq!(diff.lines, 3);\n\t\tassert_eq!(&*diff.hunks[0].lines[0].content, \"@@ -1 +1,2 @@\");\n\t}\n\n\t#[test]\n\tfn test_panic_stage_no_newline() {\n\t\tstatic FILE_1: &str = r\"a = 1\nb = 2\";\n\n\t\tstatic FILE_2: &str = r\"a = 2\nb = 3\nc = 4\";\n\n\t\tlet (path, repo) = repo_init().unwrap();\n\t\tlet path: &RepoPath = &path.path().to_str().unwrap().into();\n\n\t\twrite_commit_file(&repo, \"test.txt\", FILE_1, \"c1\");\n\n\t\trepo_write_file(&repo, \"test.txt\", FILE_2).unwrap();\n\n\t\tstage_lines(\n\t\t\tpath,\n\t\t\t\"test.txt\",\n\t\t\tfalse,\n\t\t\t&[\n\t\t\t\tDiffLinePosition {\n\t\t\t\t\told_lineno: Some(1),\n\t\t\t\t\tnew_lineno: None,\n\t\t\t\t},\n\t\t\t\tDiffLinePosition {\n\t\t\t\t\told_lineno: Some(2),\n\t\t\t\t\tnew_lineno: None,\n\t\t\t\t},\n\t\t\t],\n\t\t)\n\t\t.unwrap();\n\n\t\tlet diff = get_diff(path, \"test.txt\", true, None).unwrap();\n\n\t\tassert_eq!(diff.lines, 5);\n\t\tassert_eq!(&*diff.hunks[0].lines[0].content, \"@@ -1,2 +1 @@\");\n\t}\n\n\t#[test]\n\tfn test_unstage() {\n\t\tstatic FILE_1: &str = r\"0\n\";\n\n\t\tstatic FILE_2: &str = r\"0\n1\n2\n3\n\";\n\n\t\tlet (path, repo) = repo_init().unwrap();\n\t\tlet path: &RepoPath = &path.path().to_str().unwrap().into();\n\n\t\twrite_commit_file(&repo, \"test.txt\", FILE_1, \"c1\");\n\n\t\trepo_write_file(&repo, \"test.txt\", FILE_2).unwrap();\n\n\t\tassert_eq!(get_statuses(path), (1, 0));\n\n\t\tstage_add_file(path, Path::new(\"test.txt\")).unwrap();\n\n\t\tassert_eq!(get_statuses(path), (0, 1));\n\n\t\tlet diff_before =\n\t\t\tget_diff(path, \"test.txt\", true, None).unwrap();\n\n\t\tassert_eq!(diff_before.lines, 5);\n\n\t\tstage_lines(\n\t\t\tpath,\n\t\t\t\"test.txt\",\n\t\t\ttrue,\n\t\t\t&[DiffLinePosition {\n\t\t\t\told_lineno: None,\n\t\t\t\tnew_lineno: Some(2),\n\t\t\t}],\n\t\t)\n\t\t.unwrap();\n\n\t\tassert_eq!(get_statuses(path), (1, 1));\n\n\t\tlet diff = get_diff(path, \"test.txt\", true, None).unwrap();\n\n\t\tassert_eq!(diff.lines, 4);\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/stash.rs",
    "content": "use super::{CommitId, RepoPath};\nuse crate::{\n\terror::{Error, Result},\n\tsync::repository::repo,\n};\nuse git2::{\n\tbuild::CheckoutBuilder, Oid, Repository, StashApplyOptions,\n\tStashFlags,\n};\nuse scopetime::scope_time;\n\n///\npub fn get_stashes(repo_path: &RepoPath) -> Result<Vec<CommitId>> {\n\tscope_time!(\"get_stashes\");\n\n\tlet mut repo = repo(repo_path)?;\n\tlet mut list = Vec::new();\n\trepo.stash_foreach(|_index, _msg, id| {\n\t\tlist.push((*id).into());\n\t\ttrue\n\t})?;\n\n\tOk(list)\n}\n\n///\npub fn stash_drop(\n\trepo_path: &RepoPath,\n\tstash_id: CommitId,\n) -> Result<()> {\n\tscope_time!(\"stash_drop\");\n\n\tlet mut repo = repo(repo_path)?;\n\n\tlet index = get_stash_index(&mut repo, stash_id.into())?;\n\n\trepo.stash_drop(index)?;\n\n\tOk(())\n}\n\n///\npub fn stash_pop(\n\trepo_path: &RepoPath,\n\tstash_id: CommitId,\n) -> Result<()> {\n\tscope_time!(\"stash_pop\");\n\n\tlet mut repo = repo(repo_path)?;\n\n\tlet index = get_stash_index(&mut repo, stash_id.into())?;\n\n\trepo.stash_pop(index, None)?;\n\n\tOk(())\n}\n\n///\npub fn stash_apply(\n\trepo_path: &RepoPath,\n\tstash_id: CommitId,\n\tallow_conflicts: bool,\n) -> Result<()> {\n\tscope_time!(\"stash_apply\");\n\n\tlet mut repo = repo(repo_path)?;\n\n\tlet index = get_stash_index(&mut repo, stash_id.get_oid())?;\n\n\tlet mut checkout = CheckoutBuilder::new();\n\tcheckout.allow_conflicts(allow_conflicts);\n\n\tlet mut opt = StashApplyOptions::default();\n\topt.checkout_options(checkout);\n\trepo.stash_apply(index, Some(&mut opt))?;\n\n\tOk(())\n}\n\nfn get_stash_index(\n\trepo: &mut Repository,\n\tstash_id: Oid,\n) -> Result<usize> {\n\tlet mut idx = None;\n\n\trepo.stash_foreach(|index, _msg, id| {\n\t\tif *id == stash_id {\n\t\t\tidx = Some(index);\n\t\t\tfalse\n\t\t} else {\n\t\t\ttrue\n\t\t}\n\t})?;\n\n\tidx.ok_or_else(|| {\n\t\tError::Generic(\"stash commit not found\".to_string())\n\t})\n}\n\n///\npub fn stash_save(\n\trepo_path: &RepoPath,\n\tmessage: Option<&str>,\n\tinclude_untracked: bool,\n\tkeep_index: bool,\n) -> Result<CommitId> {\n\tscope_time!(\"stash_save\");\n\n\tlet mut repo = repo(repo_path)?;\n\n\tlet sig = repo.signature()?;\n\n\tlet mut options = StashFlags::DEFAULT;\n\n\tif include_untracked {\n\t\toptions.insert(StashFlags::INCLUDE_UNTRACKED);\n\t}\n\tif keep_index {\n\t\toptions.insert(StashFlags::KEEP_INDEX);\n\t}\n\n\tlet id = repo.stash_save2(&sig, message, Some(options))?;\n\n\tOk(CommitId::new(id))\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::sync::{\n\t\tcommit, get_commit_files, get_commits_info, stage_add_file,\n\t\ttests::{\n\t\t\tdebug_cmd_print, get_statuses, repo_init,\n\t\t\twrite_commit_file,\n\t\t},\n\t\tutils::{repo_read_file, repo_write_file},\n\t};\n\tuse std::{fs::File, io::Write, path::Path};\n\n\t#[test]\n\tfn test_smoke() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tassert!(stash_save(repo_path, None, true, false).is_err());\n\n\t\tassert!(get_stashes(repo_path).unwrap().is_empty());\n\t}\n\n\t#[test]\n\tfn test_stashing() -> Result<()> {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(\"foo.txt\"))?\n\t\t\t.write_all(b\"test\\nfoo\")?;\n\n\t\tassert_eq!(get_statuses(repo_path), (1, 0));\n\n\t\tstash_save(repo_path, None, true, false)?;\n\n\t\tassert_eq!(get_statuses(repo_path), (0, 0));\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_stashes() -> Result<()> {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(\"foo.txt\"))?\n\t\t\t.write_all(b\"test\\nfoo\")?;\n\n\t\tstash_save(repo_path, Some(\"foo\"), true, false)?;\n\n\t\tlet res = get_stashes(repo_path)?;\n\n\t\tassert_eq!(res.len(), 1);\n\n\t\tlet infos =\n\t\t\tget_commits_info(repo_path, &[res[0]], 100).unwrap();\n\n\t\tassert_eq!(infos[0].message, \"On master: foo\");\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_stash_nothing_untracked() -> Result<()> {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(\"foo.txt\"))?\n\t\t\t.write_all(b\"test\\nfoo\")?;\n\n\t\tassert!(\n\t\t\tstash_save(repo_path, Some(\"foo\"), false, false).is_err()\n\t\t);\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_stash_without_second_parent() -> Result<()> {\n\t\tlet file_path1 = Path::new(\"file1.txt\");\n\t\tlet (_td, repo) = repo_init()?;\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(file_path1))?.write_all(b\"test\")?;\n\t\tstage_add_file(repo_path, file_path1)?;\n\t\tcommit(repo_path, \"c1\")?;\n\n\t\tFile::create(root.join(file_path1))?\n\t\t\t.write_all(b\"modified\")?;\n\n\t\t//NOTE: apparently `libgit2` works differently to git stash in\n\t\t//always creating the third parent for untracked files while the\n\t\t//cli skips that step when no new files exist\n\t\tdebug_cmd_print(repo_path, \"git stash\");\n\n\t\tlet stash = get_stashes(repo_path)?[0];\n\n\t\tlet diff = get_commit_files(repo_path, stash, None)?;\n\n\t\tassert_eq!(diff.len(), 1);\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_stash_apply_conflict() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\trepo_write_file(&repo, \"test.txt\", \"test\").unwrap();\n\n\t\tlet id =\n\t\t\tstash_save(repo_path, Some(\"foo\"), true, false).unwrap();\n\n\t\trepo_write_file(&repo, \"test.txt\", \"foo\").unwrap();\n\n\t\tlet res = stash_apply(repo_path, id, false);\n\n\t\tassert!(res.is_err());\n\t}\n\n\t#[test]\n\tfn test_stash_apply_conflict2() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\twrite_commit_file(&repo, \"test.txt\", \"test\", \"c1\");\n\n\t\trepo_write_file(&repo, \"test.txt\", \"test2\").unwrap();\n\n\t\tlet id =\n\t\t\tstash_save(repo_path, Some(\"foo\"), true, false).unwrap();\n\n\t\trepo_write_file(&repo, \"test.txt\", \"test3\").unwrap();\n\n\t\tlet res = stash_apply(repo_path, id, false);\n\n\t\tassert!(res.is_err());\n\t}\n\n\t#[test]\n\tfn test_stash_apply_creating_conflict() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\twrite_commit_file(&repo, \"test.txt\", \"test\", \"c1\");\n\n\t\trepo_write_file(&repo, \"test.txt\", \"test2\").unwrap();\n\n\t\tlet id =\n\t\t\tstash_save(repo_path, Some(\"foo\"), true, false).unwrap();\n\n\t\trepo_write_file(&repo, \"test.txt\", \"test3\").unwrap();\n\n\t\tlet res = stash_apply(repo_path, id, false);\n\n\t\tassert!(res.is_err());\n\n\t\tlet res = stash_apply(repo_path, id, true);\n\n\t\tassert!(res.is_ok());\n\t}\n\n\t#[test]\n\tfn test_stash_pop_no_conflict() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\twrite_commit_file(&repo, \"test.txt\", \"test\", \"c1\");\n\n\t\trepo_write_file(&repo, \"test.txt\", \"test2\").unwrap();\n\n\t\tlet id =\n\t\t\tstash_save(repo_path, Some(\"foo\"), true, false).unwrap();\n\n\t\tlet res = stash_pop(repo_path, id);\n\n\t\tassert!(res.is_ok());\n\t\tassert_eq!(\n\t\t\trepo_read_file(&repo, \"test.txt\").unwrap(),\n\t\t\t\"test2\"\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_stash_pop_conflict() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\trepo_write_file(&repo, \"test.txt\", \"test\").unwrap();\n\n\t\tlet id =\n\t\t\tstash_save(repo_path, Some(\"foo\"), true, false).unwrap();\n\n\t\trepo_write_file(&repo, \"test.txt\", \"test2\").unwrap();\n\n\t\tlet res = stash_pop(repo_path, id);\n\n\t\tassert!(res.is_err());\n\t\tassert_eq!(\n\t\t\trepo_read_file(&repo, \"test.txt\").unwrap(),\n\t\t\t\"test2\"\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_stash_pop_conflict_after_commit() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\twrite_commit_file(&repo, \"test.txt\", \"test\", \"c1\");\n\n\t\trepo_write_file(&repo, \"test.txt\", \"test2\").unwrap();\n\n\t\tlet id =\n\t\t\tstash_save(repo_path, Some(\"foo\"), true, false).unwrap();\n\n\t\trepo_write_file(&repo, \"test.txt\", \"test3\").unwrap();\n\n\t\tlet res = stash_pop(repo_path, id);\n\n\t\tassert!(res.is_err());\n\t\tassert_eq!(\n\t\t\trepo_read_file(&repo, \"test.txt\").unwrap(),\n\t\t\t\"test3\"\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/state.rs",
    "content": "use super::RepoPath;\nuse crate::{error::Result, sync::repository::repo};\nuse git2::RepositoryState;\nuse scopetime::scope_time;\n\n///\n#[derive(Debug, PartialEq, Eq)]\npub enum RepoState {\n\t///\n\tClean,\n\t///\n\tMerge,\n\t///\n\tRebase,\n\t///\n\tRevert,\n\t///\n\tOther,\n}\n\nimpl From<RepositoryState> for RepoState {\n\tfn from(state: RepositoryState) -> Self {\n\t\tmatch state {\n\t\t\tRepositoryState::Clean => Self::Clean,\n\t\t\tRepositoryState::Merge => Self::Merge,\n\t\t\tRepositoryState::Revert => Self::Revert,\n\t\t\tRepositoryState::RebaseMerge => Self::Rebase,\n\t\t\t_ => {\n\t\t\t\tlog::warn!(\"state not supported yet: {state:?}\");\n\t\t\t\tSelf::Other\n\t\t\t}\n\t\t}\n\t}\n}\n\n///\npub fn repo_state(repo_path: &RepoPath) -> Result<RepoState> {\n\tscope_time!(\"repo_state\");\n\n\tlet repo = repo(repo_path)?;\n\n\tlet state = repo.state();\n\n\tOk(state.into())\n}\n"
  },
  {
    "path": "asyncgit/src/sync/status.rs",
    "content": "//! sync git api for fetching a status\n\nuse crate::{\n\terror::Result,\n\tsync::{\n\t\tconfig::untracked_files_config_repo,\n\t\trepository::{gix_repo, repo},\n\t},\n};\nuse git2::{Delta, Status, StatusOptions, StatusShow};\nuse scopetime::scope_time;\nuse std::path::Path;\n\nuse super::{RepoPath, ShowUntrackedFilesConfig};\n\n///\n#[derive(Copy, Clone, Hash, PartialEq, Eq, Debug)]\npub enum StatusItemType {\n\t///\n\tNew,\n\t///\n\tModified,\n\t///\n\tDeleted,\n\t///\n\tRenamed,\n\t///\n\tTypechange,\n\t///\n\tConflicted,\n}\n\nimpl From<gix::status::index_worktree::iter::Summary>\n\tfor StatusItemType\n{\n\tfn from(\n\t\tsummary: gix::status::index_worktree::iter::Summary,\n\t) -> Self {\n\t\tuse gix::status::index_worktree::iter::Summary;\n\n\t\tmatch summary {\n\t\t\tSummary::Removed => Self::Deleted,\n\t\t\tSummary::Added\n\t\t\t| Summary::Copied\n\t\t\t| Summary::IntentToAdd => Self::New,\n\t\t\tSummary::Modified => Self::Modified,\n\t\t\tSummary::TypeChange => Self::Typechange,\n\t\t\tSummary::Renamed => Self::Renamed,\n\t\t\tSummary::Conflict => Self::Conflicted,\n\t\t}\n\t}\n}\n\nimpl From<gix::diff::index::ChangeRef<'_, '_>> for StatusItemType {\n\tfn from(change_ref: gix::diff::index::ChangeRef) -> Self {\n\t\tuse gix::diff::index::ChangeRef;\n\n\t\tmatch change_ref {\n\t\t\tChangeRef::Addition { .. } => Self::New,\n\t\t\tChangeRef::Deletion { .. } => Self::Deleted,\n\t\t\tChangeRef::Modification { .. }\n\t\t\t| ChangeRef::Rewrite { .. } => Self::Modified,\n\t\t}\n\t}\n}\n\nimpl From<Status> for StatusItemType {\n\tfn from(s: Status) -> Self {\n\t\tif s.is_index_new() || s.is_wt_new() {\n\t\t\tSelf::New\n\t\t} else if s.is_index_deleted() || s.is_wt_deleted() {\n\t\t\tSelf::Deleted\n\t\t} else if s.is_index_renamed() || s.is_wt_renamed() {\n\t\t\tSelf::Renamed\n\t\t} else if s.is_index_typechange() || s.is_wt_typechange() {\n\t\t\tSelf::Typechange\n\t\t} else if s.is_conflicted() {\n\t\t\tSelf::Conflicted\n\t\t} else {\n\t\t\tSelf::Modified\n\t\t}\n\t}\n}\n\nimpl From<Delta> for StatusItemType {\n\tfn from(d: Delta) -> Self {\n\t\tmatch d {\n\t\t\tDelta::Added => Self::New,\n\t\t\tDelta::Deleted => Self::Deleted,\n\t\t\tDelta::Renamed => Self::Renamed,\n\t\t\tDelta::Typechange => Self::Typechange,\n\t\t\t_ => Self::Modified,\n\t\t}\n\t}\n}\n\n///\n#[derive(Clone, Hash, PartialEq, Eq, Debug)]\npub struct StatusItem {\n\t///\n\tpub path: String,\n\t///\n\tpub status: StatusItemType,\n}\n\n///\n#[derive(Copy, Clone, Default, Hash, PartialEq, Eq, Debug)]\npub enum StatusType {\n\t///\n\t#[default]\n\tWorkingDir,\n\t///\n\tStage,\n\t///\n\tBoth,\n}\n\nimpl From<StatusType> for StatusShow {\n\tfn from(s: StatusType) -> Self {\n\t\tmatch s {\n\t\t\tStatusType::WorkingDir => Self::Workdir,\n\t\t\tStatusType::Stage => Self::Index,\n\t\t\tStatusType::Both => Self::IndexAndWorkdir,\n\t\t}\n\t}\n}\n\n///\npub fn is_workdir_clean(\n\trepo_path: &RepoPath,\n\tshow_untracked: Option<ShowUntrackedFilesConfig>,\n) -> Result<bool> {\n\tlet repo = repo(repo_path)?;\n\n\tif repo.is_bare() && !repo.is_worktree() {\n\t\treturn Ok(true);\n\t}\n\n\tlet show_untracked = if let Some(config) = show_untracked {\n\t\tconfig\n\t} else {\n\t\tuntracked_files_config_repo(&repo)?\n\t};\n\n\tlet mut options = StatusOptions::default();\n\toptions\n\t\t.show(StatusShow::Workdir)\n\t\t.update_index(true)\n\t\t.include_untracked(show_untracked.include_untracked())\n\t\t.renames_head_to_index(true)\n\t\t.recurse_untracked_dirs(\n\t\t\tshow_untracked.recurse_untracked_dirs(),\n\t\t);\n\n\tlet statuses = repo.statuses(Some(&mut options))?;\n\n\tOk(statuses.is_empty())\n}\n\nimpl From<ShowUntrackedFilesConfig> for gix::status::UntrackedFiles {\n\tfn from(value: ShowUntrackedFilesConfig) -> Self {\n\t\tmatch value {\n\t\t\tShowUntrackedFilesConfig::All => Self::Files,\n\t\t\tShowUntrackedFilesConfig::Normal => Self::Collapsed,\n\t\t\tShowUntrackedFilesConfig::No => Self::None,\n\t\t}\n\t}\n}\n\n/// guarantees sorting\npub fn get_status(\n\trepo_path: &RepoPath,\n\tstatus_type: StatusType,\n\tshow_untracked: Option<ShowUntrackedFilesConfig>,\n) -> Result<Vec<StatusItem>> {\n\tscope_time!(\"get_status\");\n\n\tlet repo: gix::Repository = gix_repo(repo_path)?;\n\n\tlet show_untracked = if let Some(config) = show_untracked {\n\t\tconfig\n\t} else {\n\t\tlet git2_repo = crate::sync::repository::repo(repo_path)?;\n\n\t\t// Calling `untracked_files_config_repo` ensures compatibility with `gitui` <= 0.27.\n\t\t// `untracked_files_config_repo` defaults to `All` while both `libgit2` and `gix` default to\n\t\t// `Normal`. According to [show-untracked-files], `normal` is the default value that `git`\n\t\t// chooses.\n\t\t//\n\t\t// [show-untracked-files]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-statusshowUntrackedFiles\n\t\tuntracked_files_config_repo(&git2_repo)?\n\t};\n\n\tlet status = repo\n\t\t.status(gix::progress::Discard)?\n\t\t.untracked_files(show_untracked.into());\n\n\tlet mut res = Vec::new();\n\n\tmatch status_type {\n\t\tStatusType::WorkingDir => {\n\t\t\tlet iter = status.into_index_worktree_iter(Vec::new())?;\n\n\t\t\tfor item in iter {\n\t\t\t\tlet Ok(item) = item else {\n\t\t\t\t\tlog::warn!(\"[status] the status iter returned an error for an item: {item:?}\");\n\n\t\t\t\t\tcontinue;\n\t\t\t\t};\n\n\t\t\t\tlet status = item.summary().map(Into::into);\n\n\t\t\t\tif let Some(status) = status {\n\t\t\t\t\tlet path = item.rela_path().to_string();\n\n\t\t\t\t\tres.push(StatusItem { path, status });\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tStatusType::Stage => {\n\t\t\tlet tree_id: gix::ObjectId =\n\t\t\t\trepo.head_tree_id_or_empty()?.into();\n\t\t\tlet worktree_index =\n\t\t\t\tgix::worktree::IndexPersistedOrInMemory::Persisted(\n\t\t\t\t\trepo.index_or_empty()?,\n\t\t\t\t);\n\n\t\t\tlet mut pathspec = repo.pathspec(\n\t\t\t\tfalse, /* empty patterns match prefix */\n\t\t\t\tNone::<&str>,\n\t\t\t\ttrue, /* inherit ignore case */\n\t\t\t\t&gix::index::State::new(repo.object_hash()),\n\t\t\t\tgix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping\n\t\t\t)?;\n\n\t\t\tlet cb =\n\t\t\t\t|change_ref: gix::diff::index::ChangeRef<'_, '_>,\n\t\t\t\t _: &gix::index::State,\n\t\t\t\t _: &gix::index::State|\n\t\t\t\t -> Result<gix::diff::index::Action> {\n\t\t\t\t\tlet path = change_ref.fields().0.to_string();\n\t\t\t\t\tlet status = change_ref.into();\n\n\t\t\t\t\tres.push(StatusItem { path, status });\n\n\t\t\t\t\tOk(gix::diff::index::Action::Continue(()))\n\t\t\t\t};\n\n\t\t\trepo.tree_index_status(\n\t\t\t\t&tree_id,\n\t\t\t\t&worktree_index,\n\t\t\t\tSome(&mut pathspec),\n\t\t\t\tgix::status::tree_index::TrackRenames::default(),\n\t\t\t\tcb,\n\t\t\t)?;\n\t\t}\n\t\tStatusType::Both => {\n\t\t\tlet iter = status.into_iter(Vec::new())?;\n\n\t\t\tfor item in iter {\n\t\t\t\tlet item = item?;\n\n\t\t\t\tlet path = item.location().to_string();\n\n\t\t\t\tlet status = match item {\n\t\t\t\t\tgix::status::Item::IndexWorktree(item) => {\n\t\t\t\t\t\titem.summary().map(Into::into)\n\t\t\t\t\t}\n\t\t\t\t\tgix::status::Item::TreeIndex(change_ref) => {\n\t\t\t\t\t\tSome(change_ref.into())\n\t\t\t\t\t}\n\t\t\t\t};\n\n\t\t\t\tif let Some(status) = status {\n\t\t\t\t\tres.push(StatusItem { path, status });\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tres.sort_by(|a, b| {\n\t\tPath::new(a.path.as_str()).cmp(Path::new(b.path.as_str()))\n\t});\n\n\tOk(res)\n}\n\n/// discard all changes in the working directory\npub fn discard_status(repo_path: &RepoPath) -> Result<bool> {\n\tlet repo = repo(repo_path)?;\n\tlet commit = repo.head()?.peel_to_commit()?;\n\n\trepo.reset(commit.as_object(), git2::ResetType::Hard, None)?;\n\n\tOk(true)\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::{\n\t\tsync::{\n\t\t\tcommit, stage_add_file,\n\t\t\tstatus::{get_status, StatusType},\n\t\t\ttests::{repo_init, repo_init_bare},\n\t\t\tRepoPath,\n\t\t},\n\t\tStatusItem, StatusItemType,\n\t};\n\tuse std::{fs::File, io::Write, path::Path};\n\tuse tempfile::TempDir;\n\n\t#[test]\n\tfn test_discard_status() {\n\t\tlet file_path = Path::new(\"README.md\");\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tlet mut file = File::create(root.join(file_path)).unwrap();\n\n\t\t// initial commit\n\t\tstage_add_file(repo_path, file_path).unwrap();\n\t\tcommit(repo_path, \"commit msg\").unwrap();\n\n\t\twriteln!(file, \"Test for discard_status\").unwrap();\n\n\t\tlet statuses =\n\t\t\tget_status(repo_path, StatusType::WorkingDir, None)\n\t\t\t\t.unwrap();\n\t\tassert_eq!(statuses.len(), 1);\n\n\t\tdiscard_status(repo_path).unwrap();\n\n\t\tlet statuses =\n\t\t\tget_status(repo_path, StatusType::WorkingDir, None)\n\t\t\t\t.unwrap();\n\t\tassert_eq!(statuses.len(), 0);\n\t}\n\n\t#[test]\n\tfn test_get_status_with_workdir() {\n\t\tlet (git_dir, _repo) = repo_init_bare().unwrap();\n\n\t\tlet separate_workdir = TempDir::new().unwrap();\n\n\t\tlet file_path = Path::new(\"foo\");\n\t\tFile::create(separate_workdir.path().join(file_path))\n\t\t\t.unwrap()\n\t\t\t.write_all(b\"a\")\n\t\t\t.unwrap();\n\n\t\tlet repo_path = RepoPath::Workdir {\n\t\t\tgitdir: git_dir.path().into(),\n\t\t\tworkdir: separate_workdir.path().into(),\n\t\t};\n\n\t\tlet status =\n\t\t\tget_status(&repo_path, StatusType::WorkingDir, None)\n\t\t\t\t.unwrap();\n\n\t\tassert_eq!(\n\t\t\tstatus,\n\t\t\tvec![StatusItem {\n\t\t\t\tpath: \"foo\".into(),\n\t\t\t\tstatus: StatusItemType::New\n\t\t\t}]\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/submodules.rs",
    "content": "use std::path::{Path, PathBuf};\n\nuse git2::{\n\tRepository, RepositoryOpenFlags, Submodule,\n\tSubmoduleUpdateOptions,\n};\nuse scopetime::scope_time;\n\nuse super::{repo, CommitId, RepoPath};\nuse crate::{error::Result, sync::utils::work_dir, Error};\n\npub use git2::SubmoduleStatus;\n\n///\n#[derive(Debug)]\npub struct SubmoduleInfo {\n\t///\n\tpub name: String,\n\t///\n\tpub path: PathBuf,\n\t///\n\tpub url: Option<String>,\n\t///\n\tpub id: Option<CommitId>,\n\t///\n\tpub head_id: Option<CommitId>,\n\t///\n\tpub status: SubmoduleStatus,\n}\n\n///\n#[derive(Debug)]\npub struct SubmoduleParentInfo {\n\t/// where to find parent repo\n\tpub parent_gitpath: PathBuf,\n\t/// where to find submodule git path\n\tpub submodule_gitpath: PathBuf,\n\t/// `submodule_info` from perspective of parent repo\n\tpub submodule_info: SubmoduleInfo,\n}\n\nimpl SubmoduleInfo {\n\t///\n\tpub fn get_repo_path(\n\t\t&self,\n\t\trepo_path: &RepoPath,\n\t) -> Result<RepoPath> {\n\t\tlet repo = repo(repo_path)?;\n\t\tlet wd = repo.workdir().ok_or(Error::NoWorkDir)?;\n\n\t\tOk(RepoPath::Path(wd.join(self.path.clone())))\n\t}\n}\n\nfn submodule_to_info(s: &Submodule, r: &Repository) -> SubmoduleInfo {\n\tlet status = r\n\t\t.submodule_status(\n\t\t\ts.name().unwrap_or_default(),\n\t\t\tgit2::SubmoduleIgnore::None,\n\t\t)\n\t\t.unwrap_or(SubmoduleStatus::empty());\n\n\tSubmoduleInfo {\n\t\tname: s.name().unwrap_or_default().into(),\n\t\tpath: s.path().to_path_buf(),\n\t\tid: s.workdir_id().map(CommitId::from),\n\t\thead_id: s.head_id().map(CommitId::from),\n\t\turl: s.url().map(String::from),\n\t\tstatus,\n\t}\n}\n\n///\npub fn get_submodules(\n\trepo_path: &RepoPath,\n) -> Result<Vec<SubmoduleInfo>> {\n\tscope_time!(\"get_submodules\");\n\n\tlet (r, repo2) = (repo(repo_path)?, repo(repo_path)?);\n\n\tlet res = r\n\t\t.submodules()?\n\t\t.iter()\n\t\t.map(|s| submodule_to_info(s, &repo2))\n\t\t.collect();\n\n\tOk(res)\n}\n\n///\npub fn update_submodule(\n\trepo_path: &RepoPath,\n\tname: &str,\n) -> Result<()> {\n\tscope_time!(\"update_submodule\");\n\n\tlet repo = repo(repo_path)?;\n\n\tlet mut submodule = repo.find_submodule(name)?;\n\n\tlet mut options = SubmoduleUpdateOptions::new();\n\toptions.allow_fetch(true);\n\n\tsubmodule.update(true, Some(&mut options))?;\n\n\tOk(())\n}\n\n/// query whether `repo_path` points to a repo that is part of a parent git which contains it as a submodule\npub fn submodule_parent_info(\n\trepo_path: &RepoPath,\n) -> Result<Option<SubmoduleParentInfo>> {\n\tscope_time!(\"submodule_parent_info\");\n\n\tlet repo = repo(repo_path)?;\n\tlet repo_wd = work_dir(&repo)?.to_path_buf();\n\n\tlog::trace!(\"[sub] repo_wd: {repo_wd:?}\");\n\tlog::trace!(\"[sub] repo_path: {:?}\", repo.path());\n\n\tif let Some(parent_path) = repo_wd.parent() {\n\t\tlog::trace!(\"[sub] parent_path: {parent_path:?}\");\n\n\t\tif let Ok(parent) = Repository::open_ext(\n\t\t\tparent_path,\n\t\t\tRepositoryOpenFlags::FROM_ENV,\n\t\t\tVec::<&Path>::new(),\n\t\t) {\n\t\t\tlet parent_wd = work_dir(&parent)?.to_path_buf();\n\t\t\tlog::trace!(\"[sub] parent_wd: {parent_wd:?}\");\n\n\t\t\tlet submodule_name = repo_wd\n\t\t\t\t.strip_prefix(parent_wd)?\n\t\t\t\t.to_string_lossy()\n\t\t\t\t.to_string();\n\n\t\t\tlog::trace!(\"[sub] submodule_name: {submodule_name:?}\");\n\n\t\t\tif let Ok(submodule) =\n\t\t\t\tparent.find_submodule(&submodule_name)\n\t\t\t{\n\t\t\t\treturn Ok(Some(SubmoduleParentInfo {\n\t\t\t\t\tparent_gitpath: parent.path().to_path_buf(),\n\t\t\t\t\tsubmodule_gitpath: repo.path().to_path_buf(),\n\t\t\t\t\tsubmodule_info: submodule_to_info(\n\t\t\t\t\t\t&submodule, &parent,\n\t\t\t\t\t),\n\t\t\t\t}));\n\t\t\t}\n\t\t}\n\t}\n\n\tOk(None)\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::get_submodules;\n\tuse crate::sync::{\n\t\tsubmodules::submodule_parent_info, tests::repo_init, RepoPath,\n\t};\n\tuse git2::Repository;\n\tuse pretty_assertions::assert_eq;\n\tuse std::path::Path;\n\n\t#[test]\n\tfn test_smoke() {\n\t\tlet (dir, _r) = repo_init().unwrap();\n\n\t\t{\n\t\t\tlet r = Repository::open(dir.path()).unwrap();\n\t\t\tlet mut s = r\n\t\t\t\t.submodule(\n\t\t\t\t\t//TODO: use local git\n\t\t\t\t\t\"https://github.com/extrawurst/brewdump.git\",\n\t\t\t\t\tPath::new(\"foo/bar\"),\n\t\t\t\t\tfalse,\n\t\t\t\t)\n\t\t\t\t.unwrap();\n\n\t\t\tlet _sub_r = s.clone(None).unwrap();\n\t\t\ts.add_finalize().unwrap();\n\t\t}\n\n\t\tlet repo_p = RepoPath::Path(dir.keep());\n\t\tlet subs = get_submodules(&repo_p).unwrap();\n\n\t\tassert_eq!(subs.len(), 1);\n\t\tassert_eq!(&subs[0].name, \"foo/bar\");\n\n\t\tlet info = submodule_parent_info(\n\t\t\t&subs[0].get_repo_path(&repo_p).unwrap(),\n\t\t)\n\t\t.unwrap()\n\t\t.unwrap();\n\n\t\tdbg!(&info);\n\n\t\tassert_eq!(&info.submodule_info.name, \"foo/bar\");\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/tags.rs",
    "content": "use super::{get_commits_info, CommitId, RepoPath};\nuse crate::{\n\terror::Result,\n\tsync::{gix_repo, repository::repo},\n};\nuse scopetime::scope_time;\nuse std::collections::{BTreeMap, HashMap, HashSet};\n\n///\n#[derive(Clone, Hash, PartialEq, Eq, Debug)]\npub struct Tag {\n\t/// tag name\n\tpub name: String,\n\t/// tag annotation\n\tpub annotation: Option<String>,\n}\n\nimpl Tag {\n\t///\n\tpub fn new(name: &str) -> Self {\n\t\tSelf {\n\t\t\tname: name.into(),\n\t\t\tannotation: None,\n\t\t}\n\t}\n}\n\n/// all tags pointing to a single commit\npub type CommitTags = Vec<Tag>;\n/// hashmap of tag target commit hash to tag names\npub type Tags = BTreeMap<CommitId, CommitTags>;\n\n///\npub struct TagWithMetadata {\n\t///\n\tpub name: String,\n\t///\n\tpub author: String,\n\t///\n\tpub time: i64,\n\t///\n\tpub message: String,\n\t///\n\tpub commit_id: CommitId,\n\t///\n\tpub annotation: Option<String>,\n}\n\nstatic MAX_MESSAGE_WIDTH: usize = 100;\n\n/// returns `Tags` type filled with all tags found in repo\npub fn get_tags(repo_path: &RepoPath) -> Result<Tags> {\n\tscope_time!(\"get_tags\");\n\n\tlet mut res = Tags::new();\n\tlet mut adder = |key, value: Tag| {\n\t\tif let Some(key) = res.get_mut(&key) {\n\t\t\tkey.push(value);\n\t\t} else {\n\t\t\tres.insert(key, vec![value]);\n\t\t}\n\t};\n\n\tlet repo: gix::Repository = gix_repo(repo_path)?;\n\tlet platform = repo.references()?;\n\tfor mut reference in (platform.tags()?).flatten() {\n\t\tlet commit = reference.peel_to_commit();\n\t\tlet tag = reference.peel_to_tag();\n\n\t\tif let Ok(commit) = commit {\n\t\t\tlet tag_ref = tag.as_ref().map(gix::Tag::decode);\n\n\t\t\tlet name = match tag_ref {\n\t\t\t\tOk(Ok(tag)) => tag.name.to_string(),\n\t\t\t\t_ => reference.name().shorten().to_string(),\n\t\t\t};\n\t\t\tlet annotation = match tag_ref {\n\t\t\t\tOk(Ok(tag)) => Some(tag.message.to_string()),\n\t\t\t\t_ => None,\n\t\t\t};\n\n\t\t\tadder(commit.into(), Tag { name, annotation });\n\t\t}\n\t}\n\n\tOk(res)\n}\n\n///\npub fn get_tags_with_metadata(\n\trepo_path: &RepoPath,\n) -> Result<Vec<TagWithMetadata>> {\n\tscope_time!(\"get_tags_with_metadata\");\n\n\tlet tags_grouped_by_commit_id = get_tags(repo_path)?;\n\n\tlet tags_with_commit_id: Vec<(&str, Option<&str>, &CommitId)> =\n\t\ttags_grouped_by_commit_id\n\t\t\t.iter()\n\t\t\t.flat_map(|(commit_id, tags)| {\n\t\t\t\ttags.iter()\n\t\t\t\t\t.map(|tag| {\n\t\t\t\t\t\t(\n\t\t\t\t\t\t\ttag.name.as_ref(),\n\t\t\t\t\t\t\ttag.annotation.as_deref(),\n\t\t\t\t\t\t\tcommit_id,\n\t\t\t\t\t\t)\n\t\t\t\t\t})\n\t\t\t\t\t.collect::<Vec<_>>()\n\t\t\t})\n\t\t\t.collect();\n\n\tlet unique_commit_ids: HashSet<_> = tags_with_commit_id\n\t\t.iter()\n\t\t.copied()\n\t\t.map(|(_, _, &commit_id)| commit_id)\n\t\t.collect();\n\tlet mut commit_ids = Vec::with_capacity(unique_commit_ids.len());\n\tcommit_ids.extend(unique_commit_ids);\n\n\tlet commit_infos =\n\t\tget_commits_info(repo_path, &commit_ids, MAX_MESSAGE_WIDTH)?;\n\tlet unique_commit_infos: HashMap<_, _> = commit_infos\n\t\t.iter()\n\t\t.map(|commit_info| (commit_info.id, commit_info))\n\t\t.collect();\n\n\tlet mut tags: Vec<TagWithMetadata> = tags_with_commit_id\n\t\t.into_iter()\n\t\t.filter_map(|(tag, annotation, commit_id)| {\n\t\t\tunique_commit_infos.get(commit_id).map(|commit_info| {\n\t\t\t\tTagWithMetadata {\n\t\t\t\t\tname: String::from(tag),\n\t\t\t\t\tauthor: commit_info.author.clone(),\n\t\t\t\t\ttime: commit_info.time,\n\t\t\t\t\tmessage: commit_info.message.clone(),\n\t\t\t\t\tcommit_id: *commit_id,\n\t\t\t\t\tannotation: annotation.map(String::from),\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\t\t.collect();\n\n\ttags.sort_unstable_by_key(|b| std::cmp::Reverse(b.time));\n\n\tOk(tags)\n}\n\n///\npub fn delete_tag(\n\trepo_path: &RepoPath,\n\ttag_name: &str,\n) -> Result<()> {\n\tscope_time!(\"delete_tag\");\n\n\tlet repo = repo(repo_path)?;\n\trepo.tag_delete(tag_name)?;\n\n\tOk(())\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::sync::tests::repo_init;\n\tuse git2::ObjectType;\n\n\t#[test]\n\tfn test_smoke() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tassert!(get_tags(repo_path).unwrap().is_empty());\n\t}\n\n\t#[test]\n\tfn test_multitags() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tlet sig = repo.signature().unwrap();\n\t\tlet head_id = repo.head().unwrap().target().unwrap();\n\t\tlet target = repo\n\t\t\t.find_object(\n\t\t\t\trepo.head().unwrap().target().unwrap(),\n\t\t\t\tSome(ObjectType::Commit),\n\t\t\t)\n\t\t\t.unwrap();\n\n\t\trepo.tag(\"a\", &target, &sig, \"\", false).unwrap();\n\t\trepo.tag(\"b\", &target, &sig, \"\", false).unwrap();\n\n\t\tassert_eq!(\n\t\t\tget_tags(repo_path).unwrap()[&CommitId::new(head_id)]\n\t\t\t\t.iter()\n\t\t\t\t.map(|t| &t.name)\n\t\t\t\t.collect::<Vec<_>>(),\n\t\t\tvec![\"a\", \"b\"]\n\t\t);\n\n\t\tlet tags = get_tags_with_metadata(repo_path).unwrap();\n\n\t\tassert_eq!(tags.len(), 2);\n\t\tassert_eq!(tags[0].name, \"a\");\n\t\tassert_eq!(tags[0].message, \"initial\");\n\t\tassert_eq!(tags[1].name, \"b\");\n\t\tassert_eq!(tags[1].message, \"initial\");\n\t\tassert_eq!(tags[0].commit_id, tags[1].commit_id);\n\n\t\tdelete_tag(repo_path, \"a\").unwrap();\n\n\t\tlet tags = get_tags(repo_path).unwrap();\n\n\t\tassert_eq!(tags.len(), 1);\n\n\t\tdelete_tag(repo_path, \"b\").unwrap();\n\n\t\tlet tags = get_tags(repo_path).unwrap();\n\n\t\tassert_eq!(tags.len(), 0);\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/tree.rs",
    "content": "use super::{CommitId, RepoPath};\nuse crate::{\n\terror::{Error, Result},\n\tsync::repository::repo,\n};\nuse git2::{Oid, Repository, Tree};\nuse scopetime::scope_time;\nuse std::{\n\tcmp::Ordering,\n\tpath::{Path, PathBuf},\n};\n\n/// `tree_files` returns a list of `FileTree`\n#[derive(Debug, PartialEq, Eq, Clone)]\npub struct TreeFile {\n\t/// path of this file\n\tpub path: PathBuf,\n\t/// unix filemode\n\tpub filemode: i32,\n\t// internal object id\n\tid: Oid,\n}\n\n/// guarantees sorting the result\npub fn tree_files(\n\trepo_path: &RepoPath,\n\tcommit: CommitId,\n) -> Result<Vec<TreeFile>> {\n\tscope_time!(\"tree_files\");\n\n\tlet repo = repo(repo_path)?;\n\n\tlet commit = repo.find_commit(commit.into())?;\n\tlet tree = commit.tree()?;\n\n\tlet mut files: Vec<TreeFile> = Vec::new();\n\n\ttree_recurse(&repo, &PathBuf::from(\"./\"), &tree, &mut files)?;\n\n\tsort_file_list(&mut files);\n\n\tOk(files)\n}\n\nfn sort_file_list(files: &mut [TreeFile]) {\n\tfiles.sort_by(|a, b| path_cmp(&a.path, &b.path));\n}\n\n// applies topologically order on paths sorting\nfn path_cmp(a: &Path, b: &Path) -> Ordering {\n\tlet mut comp_a = a.components().peekable();\n\tlet mut comp_b = b.components().peekable();\n\n\tloop {\n\t\tlet a = comp_a.next();\n\t\tlet b = comp_b.next();\n\n\t\tlet a_is_file = comp_a.peek().is_none();\n\t\tlet b_is_file = comp_b.peek().is_none();\n\n\t\tif a_is_file && !b_is_file {\n\t\t\treturn Ordering::Greater;\n\t\t} else if !a_is_file && b_is_file {\n\t\t\treturn Ordering::Less;\n\t\t}\n\n\t\tlet cmp = a.cmp(&b);\n\t\tif cmp != Ordering::Equal {\n\t\t\treturn cmp;\n\t\t}\n\t}\n}\n\n/// will only work on utf8 content\npub fn tree_file_content(\n\trepo_path: &RepoPath,\n\tfile: &TreeFile,\n) -> Result<String> {\n\tscope_time!(\"tree_file_content\");\n\n\tlet repo = repo(repo_path)?;\n\n\tlet blob = repo.find_blob(file.id)?;\n\n\tif blob.is_binary() {\n\t\treturn Err(Error::BinaryFile);\n\t}\n\n\tlet content = String::from_utf8_lossy(blob.content()).to_string();\n\n\tOk(content)\n}\n\n///\nfn tree_recurse(\n\trepo: &Repository,\n\tpath: &Path,\n\ttree: &Tree,\n\tout: &mut Vec<TreeFile>,\n) -> Result<()> {\n\tout.reserve(tree.len());\n\n\tfor e in tree {\n\t\tlet p = String::from_utf8_lossy(e.name_bytes());\n\t\tlet path = path.join(p.to_string());\n\t\tmatch e.kind() {\n\t\t\tSome(git2::ObjectType::Blob) => {\n\t\t\t\tlet id = e.id();\n\t\t\t\tlet filemode = e.filemode();\n\t\t\t\tout.push(TreeFile { path, filemode, id });\n\t\t\t}\n\t\t\tSome(git2::ObjectType::Tree) => {\n\t\t\t\tlet obj = e.to_object(repo)?;\n\t\t\t\tlet tree = obj.peel_to_tree()?;\n\t\t\t\ttree_recurse(repo, &path, &tree, out)?;\n\t\t\t}\n\t\t\tSome(_) | None => (),\n\t\t}\n\t}\n\tOk(())\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::sync::tests::{repo_init, write_commit_file};\n\tuse pretty_assertions::{assert_eq, assert_ne};\n\n\t#[test]\n\tfn test_smoke() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tlet c1 =\n\t\t\twrite_commit_file(&repo, \"test.txt\", \"content\", \"c1\");\n\n\t\tlet files = tree_files(repo_path, c1).unwrap();\n\n\t\tassert_eq!(files.len(), 1);\n\t\tassert_eq!(files[0].path, PathBuf::from(\"./test.txt\"));\n\n\t\tlet c2 =\n\t\t\twrite_commit_file(&repo, \"test.txt\", \"content2\", \"c2\");\n\n\t\tlet content =\n\t\t\ttree_file_content(repo_path, &files[0]).unwrap();\n\t\tassert_eq!(&content, \"content\");\n\n\t\tlet files_c2 = tree_files(repo_path, c2).unwrap();\n\n\t\tassert_eq!(files_c2.len(), 1);\n\t\tassert_ne!(files_c2[0], files[0]);\n\t}\n\n\t#[test]\n\tfn test_sorting() {\n\t\tlet mut list = [\"file\", \"folder/file\", \"folder/afile\"]\n\t\t\t.iter()\n\t\t\t.map(|f| TreeFile {\n\t\t\t\tpath: PathBuf::from(f),\n\t\t\t\tfilemode: 0,\n\t\t\t\tid: Oid::zero(),\n\t\t\t})\n\t\t\t.collect::<Vec<_>>();\n\n\t\tsort_file_list(&mut list);\n\n\t\tassert_eq!(\n\t\t\tlist.iter()\n\t\t\t\t.map(|f| f.path.to_string_lossy())\n\t\t\t\t.collect::<Vec<_>>(),\n\t\t\tvec![\n\t\t\t\tString::from(\"folder/afile\"),\n\t\t\t\tString::from(\"folder/file\"),\n\t\t\t\tString::from(\"file\")\n\t\t\t]\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_sorting_folders() {\n\t\tlet mut list = [\"bfolder/file\", \"afolder/file\"]\n\t\t\t.iter()\n\t\t\t.map(|f| TreeFile {\n\t\t\t\tpath: PathBuf::from(f),\n\t\t\t\tfilemode: 0,\n\t\t\t\tid: Oid::zero(),\n\t\t\t})\n\t\t\t.collect::<Vec<_>>();\n\n\t\tsort_file_list(&mut list);\n\n\t\tassert_eq!(\n\t\t\tlist.iter()\n\t\t\t\t.map(|f| f.path.to_string_lossy())\n\t\t\t\t.collect::<Vec<_>>(),\n\t\t\tvec![\n\t\t\t\tString::from(\"afolder/file\"),\n\t\t\t\tString::from(\"bfolder/file\"),\n\t\t\t]\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_sorting_folders2() {\n\t\tlet mut list = [\"bfolder/sub/file\", \"afolder/file\"]\n\t\t\t.iter()\n\t\t\t.map(|f| TreeFile {\n\t\t\t\tpath: PathBuf::from(f),\n\t\t\t\tfilemode: 0,\n\t\t\t\tid: Oid::zero(),\n\t\t\t})\n\t\t\t.collect::<Vec<_>>();\n\n\t\tsort_file_list(&mut list);\n\n\t\tassert_eq!(\n\t\t\tlist.iter()\n\t\t\t\t.map(|f| f.path.to_string_lossy())\n\t\t\t\t.collect::<Vec<_>>(),\n\t\t\tvec![\n\t\t\t\tString::from(\"afolder/file\"),\n\t\t\t\tString::from(\"bfolder/sub/file\"),\n\t\t\t]\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_path_cmp() {\n\t\tassert_eq!(\n\t\t\tpath_cmp(\n\t\t\t\t&PathBuf::from(\"bfolder/sub/file\"),\n\t\t\t\t&PathBuf::from(\"afolder/file\")\n\t\t\t),\n\t\t\tOrdering::Greater\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_path_file_cmp() {\n\t\tassert_eq!(\n\t\t\tpath_cmp(\n\t\t\t\t&PathBuf::from(\"a\"),\n\t\t\t\t&PathBuf::from(\"afolder/file\")\n\t\t\t),\n\t\t\tOrdering::Greater\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/sync/utils.rs",
    "content": "//! sync git api (various methods)\n\nuse super::{\n\trepository::repo, CommitId, RepoPath, ShowUntrackedFilesConfig,\n};\nuse crate::{\n\terror::{Error, Result},\n\tsync::config::untracked_files_config_repo,\n};\nuse git2::{IndexAddOption, Repository, RepositoryOpenFlags};\nuse scopetime::scope_time;\nuse std::{\n\tfs::File,\n\tio::Write,\n\tpath::{Path, PathBuf},\n};\n\n///\n#[derive(PartialEq, Eq, Debug, Clone)]\npub struct Head {\n\t///\n\tpub name: String,\n\t///\n\tpub id: CommitId,\n}\n\n///\npub fn repo_open_error(repo_path: &RepoPath) -> Option<String> {\n\tif let Err(e) = Repository::open_ext(\n\t\trepo_path.gitpath(),\n\t\tRepositoryOpenFlags::FROM_ENV,\n\t\tVec::<&Path>::new(),\n\t) {\n\t\treturn Some(e.to_string());\n\t}\n\n\tgix::ThreadSafeRepository::discover_with_environment_overrides(\n\t\trepo_path.gitpath(),\n\t)\n\t.map_or_else(|e| Some(e.to_string()), |_| None)\n}\n\n///\npub(crate) fn work_dir(repo: &Repository) -> Result<&Path> {\n\trepo.workdir().ok_or(Error::NoWorkDir)\n}\n\n/// path to .git folder\npub fn repo_dir(repo_path: &RepoPath) -> Result<PathBuf> {\n\tlet repo = repo(repo_path)?;\n\tOk(repo.path().to_owned())\n}\n\n///\npub fn repo_work_dir(repo_path: &RepoPath) -> Result<String> {\n\tlet repo = repo(repo_path)?;\n\twork_dir(&repo)?.to_str().map_or_else(\n\t\t|| Err(Error::Generic(\"invalid workdir\".to_string())),\n\t\t|workdir| Ok(workdir.to_string()),\n\t)\n}\n\n///\npub fn get_head(repo_path: &RepoPath) -> Result<CommitId> {\n\tlet repo = repo(repo_path)?;\n\tget_head_repo(&repo)\n}\n\n///\npub fn get_head_tuple(repo_path: &RepoPath) -> Result<Head> {\n\tlet repo = repo(repo_path)?;\n\tlet id = get_head_repo(&repo)?;\n\tlet name = get_head_refname(&repo)?;\n\n\tOk(Head { name, id })\n}\n\n///\npub fn get_head_refname(repo: &Repository) -> Result<String> {\n\tlet head = repo.head()?;\n\tlet ref_name = bytes2string(head.name_bytes())?;\n\n\tOk(ref_name)\n}\n\n///\npub fn get_head_repo(repo: &Repository) -> Result<CommitId> {\n\tscope_time!(\"get_head_repo\");\n\n\tlet head = repo.head()?.target();\n\n\thead.map_or(Err(Error::NoHead), |head_id| Ok(head_id.into()))\n}\n\n/// add a file diff from workingdir to stage (will not add removed files see `stage_addremoved`)\npub fn stage_add_file(\n\trepo_path: &RepoPath,\n\tpath: &Path,\n) -> Result<()> {\n\tscope_time!(\"stage_add_file\");\n\n\tlet repo = repo(repo_path)?;\n\n\tlet mut index = repo.index()?;\n\n\tindex.add_path(path)?;\n\tindex.write()?;\n\n\tOk(())\n}\n\n/// like `stage_add_file` but uses a pattern to match/glob multiple files/folders\npub fn stage_add_all(\n\trepo_path: &RepoPath,\n\tpattern: &str,\n\tstage_untracked: Option<ShowUntrackedFilesConfig>,\n) -> Result<()> {\n\tscope_time!(\"stage_add_all\");\n\n\tlet repo = repo(repo_path)?;\n\n\tlet mut index = repo.index()?;\n\n\tlet stage_untracked = if let Some(config) = stage_untracked {\n\t\tconfig\n\t} else {\n\t\tuntracked_files_config_repo(&repo)?\n\t};\n\n\tif stage_untracked.include_untracked() {\n\t\tindex.add_all(\n\t\t\tvec![pattern],\n\t\t\tIndexAddOption::DEFAULT,\n\t\t\tNone,\n\t\t)?;\n\t} else {\n\t\tindex.update_all(vec![pattern], None)?;\n\t}\n\n\tindex.write()?;\n\n\tOk(())\n}\n\n/// Undo last commit in repo\npub fn undo_last_commit(repo_path: &RepoPath) -> Result<()> {\n\tlet repo = repo(repo_path)?;\n\tlet previous_commit = repo.revparse_single(\"HEAD~\")?;\n\n\tRepository::reset(\n\t\t&repo,\n\t\t&previous_commit,\n\t\tgit2::ResetType::Soft,\n\t\tNone,\n\t)?;\n\n\tOk(())\n}\n\n/// stage a removed file\npub fn stage_addremoved(\n\trepo_path: &RepoPath,\n\tpath: &Path,\n) -> Result<()> {\n\tscope_time!(\"stage_addremoved\");\n\n\tlet repo = repo(repo_path)?;\n\n\tlet mut index = repo.index()?;\n\n\tindex.remove_path(path)?;\n\tindex.write()?;\n\n\tOk(())\n}\n\npub(crate) fn bytes2string(bytes: &[u8]) -> Result<String> {\n\tOk(String::from_utf8(bytes.to_vec())?)\n}\n\n/// write a file in repo\npub(crate) fn repo_write_file(\n\trepo: &Repository,\n\tfile: &str,\n\tcontent: &str,\n) -> Result<()> {\n\tlet dir = work_dir(repo)?.join(file);\n\tlet file_path = dir.to_str().ok_or_else(|| {\n\t\tError::Generic(String::from(\"invalid file path\"))\n\t})?;\n\tlet mut file = File::create(file_path)?;\n\tfile.write_all(content.as_bytes())?;\n\tOk(())\n}\n\n///\npub fn read_file(path: &Path) -> Result<String> {\n\tuse std::io::Read;\n\n\tlet mut file = File::open(path)?;\n\tlet mut buffer = Vec::new();\n\tfile.read_to_end(&mut buffer)?;\n\n\tOk(String::from_utf8(buffer)?)\n}\n\n#[cfg(test)]\npub(crate) fn repo_read_file(\n\trepo: &Repository,\n\tfile: &str,\n) -> Result<String> {\n\tuse std::io::Read;\n\n\tlet dir = work_dir(repo)?.join(file);\n\tlet file_path = dir.to_str().ok_or_else(|| {\n\t\tError::Generic(String::from(\"invalid file path\"))\n\t})?;\n\n\tlet mut file = File::open(file_path)?;\n\tlet mut buffer = Vec::new();\n\tfile.read_to_end(&mut buffer)?;\n\n\tOk(String::from_utf8(buffer)?)\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::sync::{\n\t\tcommit,\n\t\tdiff::get_diff,\n\t\tstatus::{get_status, StatusType},\n\t\ttests::{\n\t\t\tdebug_cmd_print, get_statuses, repo_init,\n\t\t\trepo_init_empty, write_commit_file,\n\t\t},\n\t};\n\tuse std::{\n\t\tenv,\n\t\tfs::{self, remove_file, File},\n\t\tio::Write,\n\t\tpath::Path,\n\t};\n\n\t#[test]\n\tfn test_stage_add_smoke() {\n\t\tlet file_path = Path::new(\"foo\");\n\t\tlet (_td, repo) = repo_init_empty().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path = root.as_os_str().to_str().unwrap();\n\n\t\tassert!(stage_add_file(&repo_path.into(), file_path).is_err());\n\t}\n\n\t#[test]\n\tfn test_staging_one_file() {\n\t\tlet file_path = Path::new(\"file1.txt\");\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tFile::create(root.join(file_path))\n\t\t\t.unwrap()\n\t\t\t.write_all(b\"test file1 content\")\n\t\t\t.unwrap();\n\n\t\tFile::create(root.join(Path::new(\"file2.txt\")))\n\t\t\t.unwrap()\n\t\t\t.write_all(b\"test file2 content\")\n\t\t\t.unwrap();\n\n\t\tassert_eq!(get_statuses(repo_path), (2, 0));\n\n\t\tstage_add_file(repo_path, file_path).unwrap();\n\n\t\tassert_eq!(get_statuses(repo_path), (1, 1));\n\t}\n\n\t#[test]\n\tfn test_staging_one_file_from_different_sub_directory() {\n\t\t// This test case covers an interaction between current working directory and the way\n\t\t// `gitoxide` handles pathspecs.\n\t\t//\n\t\t// When staging a new file in one sub-directory, then running running `get_status` in a\n\t\t// different sub-directory, `repo.pathspec` in `get_status` has to initialized with\n\t\t// `empty_patterns_match_prefix` set to `false` for `get_status` to report the staged file’s\n\t\t// status.\n\t\tlet file_path = Path::new(\"untracked/file1.txt\");\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tfs::create_dir(root.join(\"untracked\")).unwrap();\n\n\t\tFile::create(root.join(file_path))\n\t\t\t.unwrap()\n\t\t\t.write_all(b\"test file1 content\")\n\t\t\t.unwrap();\n\n\t\tlet sub_dir_path = root.join(\"unrelated\");\n\n\t\tfs::create_dir(root.join(\"unrelated\")).unwrap();\n\n\t\tlet current_dir = env::current_dir().unwrap();\n\t\tenv::set_current_dir(sub_dir_path).unwrap();\n\n\t\tassert_eq!(get_statuses(repo_path), (1, 0));\n\n\t\tstage_add_file(repo_path, file_path).unwrap();\n\n\t\tassert_eq!(get_statuses(repo_path), (0, 1));\n\n\t\tenv::set_current_dir(current_dir).unwrap();\n\t}\n\n\t#[test]\n\tfn test_staging_folder() -> Result<()> {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tlet status_count = |s: StatusType| -> usize {\n\t\t\tget_status(repo_path, s, None).unwrap().len()\n\t\t};\n\n\t\tfs::create_dir_all(root.join(\"a/d\"))?;\n\t\tFile::create(root.join(Path::new(\"a/d/f1.txt\")))?\n\t\t\t.write_all(b\"foo\")?;\n\t\tFile::create(root.join(Path::new(\"a/d/f2.txt\")))?\n\t\t\t.write_all(b\"foo\")?;\n\t\tFile::create(root.join(Path::new(\"a/f3.txt\")))?\n\t\t\t.write_all(b\"foo\")?;\n\n\t\trepo.config()?.set_str(\"status.showUntrackedFiles\", \"all\")?;\n\n\t\tassert_eq!(status_count(StatusType::WorkingDir), 3);\n\n\t\tstage_add_all(repo_path, \"a/d\", None).unwrap();\n\n\t\tassert_eq!(status_count(StatusType::WorkingDir), 1);\n\t\tassert_eq!(status_count(StatusType::Stage), 2);\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_undo_commit_empty_repo() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\t// expect to fail\n\t\tassert!(undo_last_commit(repo_path).is_err());\n\t}\n\n\t#[test]\n\tfn test_undo_commit() {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\t// write commit file test.txt\n\t\tlet c1 =\n\t\t\twrite_commit_file(&repo, \"test.txt\", \"content1\", \"c1\");\n\t\tlet _c2 =\n\t\t\twrite_commit_file(&repo, \"test.txt\", \"content2\", \"c2\");\n\t\tassert!(undo_last_commit(repo_path).is_ok());\n\n\t\t// Make sure that HEAD points to c1\n\t\tassert_eq!(c1, get_head_repo(&repo).unwrap());\n\n\t\t// Make sure that now we have 1 file staged\n\t\tassert_eq!(get_statuses(repo_path), (0, 1));\n\n\t\t// And that file is test.txt\n\t\tlet diff =\n\t\t\tget_diff(repo_path, \"test.txt\", true, None).unwrap();\n\t\tassert_eq!(&*diff.hunks[0].lines[0].content, \"@@ -1 +1 @@\");\n\t}\n\n\t#[test]\n\tfn test_not_staging_untracked_folder() -> Result<()> {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tfs::create_dir_all(root.join(\"a/d\"))?;\n\t\tFile::create(root.join(Path::new(\"a/d/f1.txt\")))?\n\t\t\t.write_all(b\"foo\")?;\n\t\tFile::create(root.join(Path::new(\"a/d/f2.txt\")))?\n\t\t\t.write_all(b\"foo\")?;\n\t\tFile::create(root.join(Path::new(\"f3.txt\")))?\n\t\t\t.write_all(b\"foo\")?;\n\n\t\trepo.config()?.set_str(\"status.showUntrackedFiles\", \"all\")?;\n\n\t\tassert_eq!(get_statuses(repo_path), (3, 0));\n\n\t\trepo.config()?.set_str(\"status.showUntrackedFiles\", \"no\")?;\n\n\t\tassert_eq!(get_statuses(repo_path), (0, 0));\n\n\t\tstage_add_all(repo_path, \"*\", None).unwrap();\n\n\t\tassert_eq!(get_statuses(repo_path), (0, 0));\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_staging_deleted_file() {\n\t\tlet file_path = Path::new(\"file1.txt\");\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tlet status_count = |s: StatusType| -> usize {\n\t\t\tget_status(repo_path, s, None).unwrap().len()\n\t\t};\n\n\t\tlet full_path = &root.join(file_path);\n\n\t\tFile::create(full_path)\n\t\t\t.unwrap()\n\t\t\t.write_all(b\"test file1 content\")\n\t\t\t.unwrap();\n\n\t\tstage_add_file(repo_path, file_path).unwrap();\n\n\t\tcommit(repo_path, \"commit msg\").unwrap();\n\n\t\t// delete the file now\n\t\tassert!(remove_file(full_path).is_ok());\n\n\t\t// deleted file in diff now\n\t\tassert_eq!(status_count(StatusType::WorkingDir), 1);\n\n\t\tstage_addremoved(repo_path, file_path).unwrap();\n\n\t\tassert_eq!(status_count(StatusType::WorkingDir), 0);\n\t\tassert_eq!(status_count(StatusType::Stage), 1);\n\t}\n\n\t// see https://github.com/gitui-org/gitui/issues/108\n\t#[test]\n\tfn test_staging_sub_git_folder() -> Result<()> {\n\t\tlet (_td, repo) = repo_init().unwrap();\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tlet status_count = |s: StatusType| -> usize {\n\t\t\tget_status(repo_path, s, None).unwrap().len()\n\t\t};\n\n\t\tlet sub = &root.join(\"sub\");\n\n\t\tfs::create_dir_all(sub)?;\n\n\t\tdebug_cmd_print(\n\t\t\t&sub.to_str().unwrap().into(),\n\t\t\t\"git init subgit\",\n\t\t);\n\n\t\tFile::create(sub.join(\"subgit/foo.txt\"))\n\t\t\t.unwrap()\n\t\t\t.write_all(b\"content\")\n\t\t\t.unwrap();\n\n\t\tassert_eq!(status_count(StatusType::WorkingDir), 1);\n\n\t\t//expect to fail\n\t\tassert!(stage_add_all(repo_path, \"sub\", None).is_err());\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_head_empty() -> Result<()> {\n\t\tlet (_td, repo) = repo_init_empty()?;\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tassert!(get_head(repo_path).is_err());\n\n\t\tOk(())\n\t}\n\n\t#[test]\n\tfn test_head() -> Result<()> {\n\t\tlet (_td, repo) = repo_init()?;\n\t\tlet root = repo.path().parent().unwrap();\n\t\tlet repo_path: &RepoPath =\n\t\t\t&root.as_os_str().to_str().unwrap().into();\n\n\t\tassert!(get_head(repo_path).is_ok());\n\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/tags.rs",
    "content": "use crate::{\n\tasyncjob::{AsyncJob, AsyncSingleJob, RunParams},\n\terror::Result,\n\thash,\n\tsync::{self, RepoPath},\n\tAsyncGitNotification,\n};\nuse crossbeam_channel::Sender;\nuse std::{\n\tsync::{Arc, Mutex},\n\ttime::{Duration, Instant},\n};\nuse sync::Tags;\n\n///\n#[derive(Default, Clone)]\npub struct TagsResult {\n\thash: u64,\n\ttags: Tags,\n}\n\n///\npub struct AsyncTags {\n\tlast: Option<(Instant, TagsResult)>,\n\tsender: Sender<AsyncGitNotification>,\n\tjob: AsyncSingleJob<AsyncTagsJob>,\n\trepo: RepoPath,\n}\n\nimpl AsyncTags {\n\t///\n\tpub fn new(\n\t\trepo: RepoPath,\n\t\tsender: &Sender<AsyncGitNotification>,\n\t) -> Self {\n\t\tSelf {\n\t\t\trepo,\n\t\t\tlast: None,\n\t\t\tsender: sender.clone(),\n\t\t\tjob: AsyncSingleJob::new(sender.clone()),\n\t\t}\n\t}\n\n\t/// last fetched result\n\tpub fn last(&self) -> Result<Option<Tags>> {\n\t\tOk(self.last.as_ref().map(|result| result.1.tags.clone()))\n\t}\n\n\t///\n\tpub fn is_pending(&self) -> bool {\n\t\tself.job.is_pending()\n\t}\n\n\t///\n\tfn is_outdated(&self, dur: Duration) -> bool {\n\t\tself.last\n\t\t\t.as_ref()\n\t\t\t.is_none_or(|(last_time, _)| last_time.elapsed() > dur)\n\t}\n\n\t///\n\tpub fn request(\n\t\t&mut self,\n\t\tdur: Duration,\n\t\tforce: bool,\n\t) -> Result<()> {\n\t\tlog::trace!(\"request\");\n\n\t\tif !force && self.job.is_pending() {\n\t\t\treturn Ok(());\n\t\t}\n\n\t\tlet outdated = self.is_outdated(dur);\n\n\t\tif !force && !outdated {\n\t\t\treturn Ok(());\n\t\t}\n\n\t\tlet repo = self.repo.clone();\n\n\t\tif outdated {\n\t\t\tself.job.spawn(AsyncTagsJob::new(\n\t\t\t\tself.last\n\t\t\t\t\t.as_ref()\n\t\t\t\t\t.map_or(0, |(_, result)| result.hash),\n\t\t\t\trepo,\n\t\t\t));\n\n\t\t\tif let Some(job) = self.job.take_last() {\n\t\t\t\tif let Some(Ok(result)) = job.result() {\n\t\t\t\t\tself.last = Some(result);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tself.sender\n\t\t\t\t.send(AsyncGitNotification::FinishUnchanged)?;\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nenum JobState {\n\tRequest(u64, RepoPath),\n\tResponse(Result<(Instant, TagsResult)>),\n}\n\n///\n#[derive(Clone, Default)]\npub struct AsyncTagsJob {\n\tstate: Arc<Mutex<Option<JobState>>>,\n}\n\n///\nimpl AsyncTagsJob {\n\t///\n\tpub fn new(last_hash: u64, repo: RepoPath) -> Self {\n\t\tSelf {\n\t\t\tstate: Arc::new(Mutex::new(Some(JobState::Request(\n\t\t\t\tlast_hash, repo,\n\t\t\t)))),\n\t\t}\n\t}\n\n\t///\n\tpub fn result(&self) -> Option<Result<(Instant, TagsResult)>> {\n\t\tif let Ok(mut state) = self.state.lock() {\n\t\t\tif let Some(state) = state.take() {\n\t\t\t\treturn match state {\n\t\t\t\t\tJobState::Request(_, _) => None,\n\t\t\t\t\tJobState::Response(result) => Some(result),\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tNone\n\t}\n}\n\nimpl AsyncJob for AsyncTagsJob {\n\ttype Notification = AsyncGitNotification;\n\ttype Progress = ();\n\n\tfn run(\n\t\t&mut self,\n\t\t_params: RunParams<Self::Notification, Self::Progress>,\n\t) -> Result<Self::Notification> {\n\t\tlet mut notification = AsyncGitNotification::FinishUnchanged;\n\t\tif let Ok(mut state) = self.state.lock() {\n\t\t\t*state = state.take().map(|state| match state {\n\t\t\t\tJobState::Request(last_hash, repo) => {\n\t\t\t\t\tlet tags = sync::get_tags(&repo);\n\n\t\t\t\t\tJobState::Response(tags.map(|tags| {\n\t\t\t\t\t\tlet hash = hash(&tags);\n\t\t\t\t\t\tif last_hash != hash {\n\t\t\t\t\t\t\tnotification = AsyncGitNotification::Tags;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t(Instant::now(), TagsResult { hash, tags })\n\t\t\t\t\t}))\n\t\t\t\t}\n\t\t\t\tJobState::Response(result) => {\n\t\t\t\t\tJobState::Response(result)\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\tOk(notification)\n\t}\n}\n"
  },
  {
    "path": "asyncgit/src/treefiles.rs",
    "content": "use crate::{\n\tasyncjob::{AsyncJob, RunParams},\n\terror::Result,\n\tsync::{tree_files, CommitId, RepoPath, TreeFile},\n\tAsyncGitNotification,\n};\nuse std::sync::{Arc, Mutex};\n\n///\npub struct FileTreeResult {\n\t///\n\tpub commit: CommitId,\n\t///\n\tpub result: Result<Vec<TreeFile>>,\n}\n\nenum JobState {\n\tRequest { commit: CommitId, repo: RepoPath },\n\tResponse(FileTreeResult),\n}\n\n///\n#[derive(Clone, Default)]\npub struct AsyncTreeFilesJob {\n\tstate: Arc<Mutex<Option<JobState>>>,\n}\n\n///\nimpl AsyncTreeFilesJob {\n\t///\n\tpub fn new(repo: RepoPath, commit: CommitId) -> Self {\n\t\tSelf {\n\t\t\tstate: Arc::new(Mutex::new(Some(JobState::Request {\n\t\t\t\trepo,\n\t\t\t\tcommit,\n\t\t\t}))),\n\t\t}\n\t}\n\n\t///\n\tpub fn result(&self) -> Option<FileTreeResult> {\n\t\tif let Ok(mut state) = self.state.lock() {\n\t\t\tif let Some(state) = state.take() {\n\t\t\t\treturn match state {\n\t\t\t\t\tJobState::Request { .. } => None,\n\t\t\t\t\tJobState::Response(result) => Some(result),\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tNone\n\t}\n}\n\nimpl AsyncJob for AsyncTreeFilesJob {\n\ttype Notification = AsyncGitNotification;\n\ttype Progress = ();\n\n\tfn run(\n\t\t&mut self,\n\t\t_params: RunParams<Self::Notification, Self::Progress>,\n\t) -> Result<Self::Notification> {\n\t\tif let Ok(mut state) = self.state.lock() {\n\t\t\t*state = state.take().map(|state| match state {\n\t\t\t\tJobState::Request { commit, repo } => {\n\t\t\t\t\tlet files = tree_files(&repo, commit);\n\n\t\t\t\t\tJobState::Response(FileTreeResult {\n\t\t\t\t\t\tcommit,\n\t\t\t\t\t\tresult: files,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\tJobState::Response(result) => {\n\t\t\t\t\tJobState::Response(result)\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\tOk(AsyncGitNotification::TreeFiles)\n\t}\n}\n"
  },
  {
    "path": "build.rs",
    "content": "use chrono::TimeZone;\n\nfn get_git_hash() -> String {\n\tuse std::process::Command;\n\n\t// Allow builds from `git archive` generated tarballs if output of `git get-tar-commit-id` is\n\t// set in an env var.\n\tif let Ok(commit) = std::env::var(\"BUILD_GIT_COMMIT_ID\") {\n\t\treturn commit[..7].to_string();\n\t};\n\tlet commit = Command::new(\"git\")\n\t\t.arg(\"rev-parse\")\n\t\t.arg(\"--short=7\")\n\t\t.arg(\"--verify\")\n\t\t.arg(\"HEAD\")\n\t\t.output();\n\tif let Ok(commit_output) = commit {\n\t\tlet commit_string =\n\t\t\tString::from_utf8_lossy(&commit_output.stdout);\n\n\t\treturn commit_string.lines().next().unwrap_or(\"\").into();\n\t}\n\n\tpanic!(\"Can not get git commit: {}\", commit.unwrap_err());\n}\n\nfn main() {\n\tlet now = match std::env::var(\"SOURCE_DATE_EPOCH\") {\n\t\tOk(val) => chrono::Local\n\t\t\t.timestamp_opt(val.parse::<i64>().unwrap(), 0)\n\t\t\t.unwrap(),\n\t\tErr(_) => chrono::Local::now(),\n\t};\n\tlet build_date = now.date_naive();\n\n\tlet build_name = if std::env::var(\"GITUI_RELEASE\").is_ok() {\n\t\tenv!(\"CARGO_PKG_VERSION\").to_string()\n\t} else {\n\t\tformat!(\n\t\t\t\"{}-nightly {} ({})\",\n\t\t\tenv!(\"CARGO_PKG_VERSION\"),\n\t\t\tbuild_date,\n\t\t\tget_git_hash()\n\t\t)\n\t};\n\n\tprintln!(\"cargo:warning=buildname '{build_name}'\");\n\tprintln!(\"cargo:rustc-env=GITUI_BUILD_NAME={build_name}\");\n}\n"
  },
  {
    "path": "deny.toml",
    "content": "[licenses]\nallow = [\n    \"MIT\",\n    \"Apache-2.0\",\n    \"BSD-2-Clause\",\n    \"BSD-3-Clause\",\n    \"CC0-1.0\",\n    \"ISC\",\n    \"MPL-2.0\",\n    \"Unicode-3.0\",\n    \"Zlib\",\n]\n\n[advisories]\nversion = 2\nignore = [\n    # No fix for RSA, and this is a dependency from ssh_key crate to handle rsa ssh key.\n    # https://rustsec.org/advisories/RUSTSEC-2023-0071\n    \"RUSTSEC-2023-0071\",\n    # Crate paste is unmaintained. The dependency is already removed in\n    # ratatui:master. Until a new release is available, ignore this in\n    # order to pass CI. (https://github.com/gitui-org/gitui/issues/2554)\n    { id = \"RUSTSEC-2024-0436\", reason = \"The paste dependency is already removed from ratatui.\" },\n    # See https://github.com/trishume/syntect/issues/606\n    { id = \"RUSTSEC-2025-0141\", reason = \"Only brought in via syntect\" },\n]\n\n[bans]\nmultiple-versions = \"deny\"\nskip-tree = [\n    # currently needed due to:\n    # * `dirs-sys v0.4.1` (https://github.com/dirs-dev/dirs-sys-rs/issues/29)\n    { name = \"windows-sys\" },\n    # this is needed for:\n    #  `bwrap v1.3.0` (https://github.com/micl2e2/bwrap/pull/4)\n    { name = \"unicode-width\" },\n    # currently needed due to `ratatui v0.29.0`\n    { name = \"unicode-truncate\" },\n    # currently needed due to:\n    # * `redox_users v0.4.6`\n    # * `syntect v5.2.0`\n    { name = \"thiserror\" },\n    # currently needed due to:\n    # * `windows v0.57.0`\n    # * `iana-time-zone v0.1.60`\n    { name = \"windows-core\" },\n    # currently needed due to:\n    # * `parking_lot_core v0.9.10`\n    # * `filetime v0.2.23`\n    { name = \"redox_syscall\" },\n    # currently needed due to:\n    # * `gix-hashtable v0.6.0`\n    { name = \"hashbrown\" },\n    # 2022-10-26 `getrandom` and `rustix` were added when `gitoxide` was\n    # upgraded from 0.71.0 to 0.74.1.\n    # currently needed due to:\n    # * `tempfile v3.23.0`\n    # * `rand_core v0.6.4`\n    # * `redox_users v0.5.0`\n    { name = \"getrandom\" },\n    # currently needed due to:\n    # * `crossterm v0.28.1`\n    # * `which v7.0.2`\n    # * `gix-index v0.42.1`\n    # * `tempfile v3.23.0`\n    { name = \"rustix\" },\n]\n"
  },
  {
    "path": "filetreelist/Cargo.toml",
    "content": "[package]\nname = \"filetreelist\"\nversion = \"0.5.3\"\nauthors = [\"extrawurst <mail@rusticorn.com>\"]\nedition = \"2021\"\ndescription = \"filetree abstraction based on a sorted path list, supports key based navigation events, folding, scrolling and more\"\nhomepage = \"https://github.com/gitui-org/gitui\"\nrepository = \"https://github.com/gitui-org/gitui\"\nreadme = \"README.md\"\nlicense = \"MIT\"\ncategories = [\"command-line-utilities\"]\nkeywords = [\"gui\", \"cli\", \"terminal\", \"ui\", \"tui\"]\nexclude = [\"/demo.gif\"]\n\n[dependencies]\nthiserror = \"2.0\"\n\n[dev-dependencies]\npretty_assertions = \"1.4\"\n"
  },
  {
    "path": "filetreelist/README.md",
    "content": "# filetreelist\n\nThis crate is designed as part of the [gitui](http://gitui.org) project.\n\n`filetreelist` provides a very common functionality of `gitui`: lists of files visualized as a tree. It allows efficient iteration of only the visual (non collapsed) elements and change the tree state correctly given well defined inputs like `Up`/`Down`/`Collapse`.\n\nIt is the main driver behind the file tree feature:\n\n![demo](./demo.gif)"
  },
  {
    "path": "filetreelist/src/error.rs",
    "content": "use std::{num::TryFromIntError, path::PathBuf};\nuse thiserror::Error;\n\n#[derive(Error, Debug)]\npub enum Error {\n\t#[error(\"InvalidPath: `{0}`\")]\n\tInvalidPath(PathBuf),\n\n\t#[error(\"TryFromInt error:{0}\")]\n\tIntConversion(#[from] TryFromIntError),\n}\n\npub type Result<T> = std::result::Result<T, Error>;\n"
  },
  {
    "path": "filetreelist/src/filetree.rs",
    "content": "use crate::{\n\terror::Result, filetreeitems::FileTreeItems,\n\ttree_iter::TreeIterator, TreeItemInfo,\n};\nuse std::{cell::Cell, collections::BTreeSet, path::Path};\n\n///\n#[derive(Copy, Clone, Debug)]\npub enum MoveSelection {\n\tUp,\n\tDown,\n\tLeft,\n\tRight,\n\tTop,\n\tEnd,\n\tPageDown,\n\tPageUp,\n}\n\n#[derive(Clone, Copy, PartialEq)]\nenum Direction {\n\tUp,\n\tDown,\n}\n\n#[derive(Debug, Clone, Copy)]\npub struct VisualSelection {\n\tpub count: usize,\n\tpub index: usize,\n}\n\n/// wraps `FileTreeItems` as a datastore and adds selection functionality\n#[derive(Default)]\npub struct FileTree {\n\titems: FileTreeItems,\n\tselection: Option<usize>,\n\t// caches the absolute selection translated to visual index\n\tvisual_selection: Option<VisualSelection>,\n\tpub window_height: Cell<Option<usize>>,\n}\n\nimpl FileTree {\n\t///\n\tpub fn new(\n\t\tlist: &[&Path],\n\t\tcollapsed: &BTreeSet<&String>,\n\t) -> Result<Self> {\n\t\tlet mut new_self = Self {\n\t\t\titems: FileTreeItems::new(list, collapsed)?,\n\t\t\tselection: if list.is_empty() { None } else { Some(0) },\n\t\t\tvisual_selection: None,\n\t\t\twindow_height: None.into(),\n\t\t};\n\t\tnew_self.visual_selection = new_self.calc_visual_selection();\n\n\t\tOk(new_self)\n\t}\n\n\t///\n\tpub const fn is_empty(&self) -> bool {\n\t\tself.items.file_count() == 0\n\t}\n\n\t///\n\tpub const fn selection(&self) -> Option<usize> {\n\t\tself.selection\n\t}\n\n\t///\n\tpub fn collapse_but_root(&mut self) {\n\t\tif !self.is_empty() {\n\t\t\tself.items.collapse(0, true);\n\t\t\tself.items.expand(0, false);\n\t\t}\n\t}\n\n\t/// iterates visible elements starting from `start_index_visual`\n\tpub fn iterate(\n\t\t&self,\n\t\tstart_index_visual: usize,\n\t\tmax_amount: usize,\n\t) -> TreeIterator<'_> {\n\t\tlet start = self\n\t\t\t.visual_index_to_absolute(start_index_visual)\n\t\t\t.unwrap_or_default();\n\t\tTreeIterator::new(\n\t\t\tself.items.iterate(start, max_amount),\n\t\t\tself.selection,\n\t\t)\n\t}\n\n\t///\n\tpub const fn visual_selection(&self) -> Option<&VisualSelection> {\n\t\tself.visual_selection.as_ref()\n\t}\n\n\t///\n\tpub fn selected_file(&self) -> Option<&TreeItemInfo> {\n\t\tself.selection.and_then(|index| {\n\t\t\tlet item = &self.items.tree_items[index];\n\t\t\tif item.kind().is_path() {\n\t\t\t\tNone\n\t\t\t} else {\n\t\t\t\tSome(item.info())\n\t\t\t}\n\t\t})\n\t}\n\n\t///\n\tpub fn collapse_recursive(&mut self) {\n\t\tif let Some(selection) = self.selection {\n\t\t\tself.items.collapse(selection, true);\n\t\t}\n\t}\n\n\t///\n\tpub fn expand_recursive(&mut self) {\n\t\tif let Some(selection) = self.selection {\n\t\t\tself.items.expand(selection, true);\n\t\t}\n\t}\n\n\tfn selection_page_updown(\n\t\t&self,\n\t\tcurrent_index: usize,\n\t\tdirection: Direction,\n\t) -> Option<usize> {\n\t\tlet page_size = self.window_height.get().unwrap_or(0);\n\n\t\tif direction == Direction::Up {\n\t\t\tself.get_new_selection(\n\t\t\t\t(0..=current_index).rev(),\n\t\t\t\tpage_size,\n\t\t\t)\n\t\t} else {\n\t\t\tself.get_new_selection(\n\t\t\t\tcurrent_index..(self.items.len()),\n\t\t\t\tpage_size,\n\t\t\t)\n\t\t}\n\t}\n\n\t///\n\tpub fn move_selection(&mut self, dir: MoveSelection) -> bool {\n\t\tself.selection.is_some_and(|selection| {\n\t\t\tlet new_index = match dir {\n\t\t\t\tMoveSelection::Up => {\n\t\t\t\t\tself.selection_updown(selection, Direction::Up)\n\t\t\t\t}\n\t\t\t\tMoveSelection::Down => {\n\t\t\t\t\tself.selection_updown(selection, Direction::Down)\n\t\t\t\t}\n\t\t\t\tMoveSelection::Left => self.selection_left(selection),\n\t\t\t\tMoveSelection::Right => {\n\t\t\t\t\tself.selection_right(selection)\n\t\t\t\t}\n\t\t\t\tMoveSelection::Top => Some(0),\n\t\t\t\tMoveSelection::End => self.selection_end(),\n\t\t\t\tMoveSelection::PageUp => self\n\t\t\t\t\t.selection_page_updown(selection, Direction::Up),\n\t\t\t\tMoveSelection::PageDown => self\n\t\t\t\t\t.selection_page_updown(\n\t\t\t\t\t\tselection,\n\t\t\t\t\t\tDirection::Down,\n\t\t\t\t\t),\n\t\t\t};\n\n\t\t\tlet changed_index =\n\t\t\t\tnew_index.is_some_and(|i| i != selection);\n\n\t\t\tif changed_index {\n\t\t\t\tself.selection = new_index;\n\t\t\t\tself.visual_selection = self.calc_visual_selection();\n\t\t\t}\n\n\t\t\tchanged_index || new_index.is_some()\n\t\t})\n\t}\n\n\tpub fn select_file(&mut self, path: &Path) -> bool {\n\t\tlet new_selection = self\n\t\t\t.items\n\t\t\t.tree_items\n\t\t\t.iter()\n\t\t\t.position(|item| item.info().full_path() == path);\n\n\t\tif new_selection == self.selection {\n\t\t\treturn false;\n\t\t}\n\n\t\tself.selection = new_selection;\n\t\tif let Some(selection) = self.selection {\n\t\t\tself.items.show_element(selection);\n\t\t}\n\t\tself.visual_selection = self.calc_visual_selection();\n\t\ttrue\n\t}\n\n\tfn visual_index_to_absolute(\n\t\t&self,\n\t\tvisual_index: usize,\n\t) -> Option<usize> {\n\t\tself.items\n\t\t\t.iterate(0, self.items.len())\n\t\t\t.enumerate()\n\t\t\t.find_map(|(i, (abs, _))| {\n\t\t\t\tif i == visual_index {\n\t\t\t\t\tSome(abs)\n\t\t\t\t} else {\n\t\t\t\t\tNone\n\t\t\t\t}\n\t\t\t})\n\t}\n\n\tfn calc_visual_selection(&self) -> Option<VisualSelection> {\n\t\tself.selection.map(|selection_absolute| {\n\t\t\tlet mut count = 0;\n\t\t\tlet mut visual_index = 0;\n\t\t\tfor (index, _item) in\n\t\t\t\tself.items.iterate(0, self.items.len())\n\t\t\t{\n\t\t\t\tif selection_absolute == index {\n\t\t\t\t\tvisual_index = count;\n\t\t\t\t}\n\n\t\t\t\tcount += 1;\n\t\t\t}\n\n\t\t\tVisualSelection {\n\t\t\t\tindex: visual_index,\n\t\t\t\tcount,\n\t\t\t}\n\t\t})\n\t}\n\n\tfn selection_end(&self) -> Option<usize> {\n\t\tlet items_max = self.items.len().saturating_sub(1);\n\n\t\tself.get_new_selection((0..=items_max).rev(), 1)\n\t}\n\n\tfn get_new_selection(\n\t\t&self,\n\t\trange: impl Iterator<Item = usize>,\n\t\ttake: usize,\n\t) -> Option<usize> {\n\t\trange\n\t\t\t.filter(|index| self.is_visible_index(*index))\n\t\t\t.take(take)\n\t\t\t.last()\n\t}\n\n\tfn selection_updown(\n\t\t&self,\n\t\tcurrent_index: usize,\n\t\tdirection: Direction,\n\t) -> Option<usize> {\n\t\tif direction == Direction::Up {\n\t\t\tself.get_new_selection(\n\t\t\t\t(0..=current_index.saturating_sub(1)).rev(),\n\t\t\t\t1,\n\t\t\t)\n\t\t} else {\n\t\t\tself.get_new_selection(\n\t\t\t\t(current_index + 1)..(self.items.len()),\n\t\t\t\t1,\n\t\t\t)\n\t\t}\n\t}\n\n\tfn select_parent(&self, current_index: usize) -> Option<usize> {\n\t\tlet current_indent =\n\t\t\tself.items.tree_items[current_index].info().indent();\n\n\t\tlet range = (0..=current_index).rev();\n\n\t\trange.filter(|index| self.is_visible_index(*index)).find(\n\t\t\t|index| {\n\t\t\t\tself.items.tree_items[*index].info().indent()\n\t\t\t\t\t< current_indent\n\t\t\t},\n\t\t)\n\t}\n\n\tfn selection_left(\n\t\t&mut self,\n\t\tcurrent_index: usize,\n\t) -> Option<usize> {\n\t\tlet item = &mut self.items.tree_items[current_index];\n\n\t\tif item.kind().is_path() && !item.kind().is_path_collapsed() {\n\t\t\tself.items.collapse(current_index, false);\n\t\t\treturn Some(current_index);\n\t\t}\n\n\t\tself.select_parent(current_index)\n\t}\n\n\tfn selection_right(\n\t\t&mut self,\n\t\tcurrent_selection: usize,\n\t) -> Option<usize> {\n\t\tlet item = &mut self.items.tree_items[current_selection];\n\n\t\tif item.kind().is_path() {\n\t\t\tif item.kind().is_path_collapsed() {\n\t\t\t\tself.items.expand(current_selection, false);\n\t\t\t\treturn Some(current_selection);\n\t\t\t}\n\t\t\treturn self.selection_updown(\n\t\t\t\tcurrent_selection,\n\t\t\t\tDirection::Down,\n\t\t\t);\n\t\t}\n\n\t\tNone\n\t}\n\n\tfn is_visible_index(&self, index: usize) -> bool {\n\t\tself.items\n\t\t\t.tree_items\n\t\t\t.get(index)\n\t\t\t.is_some_and(|item| item.info().is_visible())\n\t}\n}\n\n#[cfg(test)]\nmod test {\n\tuse crate::{FileTree, MoveSelection};\n\tuse pretty_assertions::assert_eq;\n\tuse std::{collections::BTreeSet, path::Path};\n\n\t#[test]\n\tfn test_selection() {\n\t\tlet items = vec![\n\t\t\tPath::new(\"a/b\"), //\n\t\t];\n\n\t\tlet mut tree =\n\t\t\tFileTree::new(&items, &BTreeSet::new()).unwrap();\n\n\t\tassert!(tree.move_selection(MoveSelection::Down));\n\n\t\tassert_eq!(tree.selection, Some(1));\n\n\t\tassert!(!tree.move_selection(MoveSelection::Down));\n\n\t\tassert_eq!(tree.selection, Some(1));\n\t}\n\n\t#[test]\n\tfn test_selection_skips_collapsed() {\n\t\tlet items = vec![\n\t\t\tPath::new(\"a/b/c\"), //\n\t\t\tPath::new(\"a/d\"),   //\n\t\t];\n\n\t\t//0 a/\n\t\t//1   b/\n\t\t//2     c\n\t\t//3   d\n\n\t\tlet mut tree =\n\t\t\tFileTree::new(&items, &BTreeSet::new()).unwrap();\n\n\t\ttree.items.collapse(1, false);\n\t\ttree.selection = Some(1);\n\n\t\tassert!(tree.move_selection(MoveSelection::Down));\n\n\t\tassert_eq!(tree.selection, Some(3));\n\t}\n\n\t#[test]\n\tfn test_selection_left_collapse() {\n\t\tlet items = vec![\n\t\t\tPath::new(\"a/b/c\"), //\n\t\t\tPath::new(\"a/d\"),   //\n\t\t];\n\n\t\t//0 a/\n\t\t//1   b/\n\t\t//2     c\n\t\t//3   d\n\n\t\tlet mut tree =\n\t\t\tFileTree::new(&items, &BTreeSet::new()).unwrap();\n\n\t\ttree.selection = Some(1);\n\n\t\t//collapses 1\n\t\tassert!(tree.move_selection(MoveSelection::Left));\n\t\t// index will not change\n\t\tassert_eq!(tree.selection, Some(1));\n\n\t\tassert!(tree.items.tree_items[1].kind().is_path_collapsed());\n\t\tassert!(!tree.items.tree_items[2].info().is_visible());\n\t}\n\n\t#[test]\n\tfn test_selection_left_parent() {\n\t\tlet items = vec![\n\t\t\tPath::new(\"a/b/c\"), //\n\t\t\tPath::new(\"a/d\"),   //\n\t\t];\n\n\t\t//0 a/\n\t\t//1   b/\n\t\t//2     c\n\t\t//3   d\n\n\t\tlet mut tree =\n\t\t\tFileTree::new(&items, &BTreeSet::new()).unwrap();\n\n\t\ttree.selection = Some(2);\n\n\t\tassert!(tree.move_selection(MoveSelection::Left));\n\t\tassert_eq!(tree.selection, Some(1));\n\n\t\tassert!(tree.move_selection(MoveSelection::Left));\n\t\tassert_eq!(tree.selection, Some(1));\n\n\t\tassert!(tree.move_selection(MoveSelection::Left));\n\t\tassert_eq!(tree.selection, Some(0));\n\t}\n\n\t#[test]\n\tfn test_selection_right_expand() {\n\t\tlet items = vec![\n\t\t\tPath::new(\"a/b/c\"), //\n\t\t\tPath::new(\"a/d\"),   //\n\t\t];\n\n\t\t//0 a/\n\t\t//1   b/\n\t\t//2     c\n\t\t//3   d\n\n\t\tlet mut tree =\n\t\t\tFileTree::new(&items, &BTreeSet::new()).unwrap();\n\n\t\ttree.items.collapse(1, false);\n\t\ttree.items.collapse(0, false);\n\t\ttree.selection = Some(0);\n\n\t\tassert!(tree.move_selection(MoveSelection::Right));\n\t\tassert_eq!(tree.selection, Some(0));\n\t\tassert!(!tree.items.tree_items[0].kind().is_path_collapsed());\n\n\t\tassert!(tree.move_selection(MoveSelection::Right));\n\t\tassert_eq!(tree.selection, Some(1));\n\t\tassert!(tree.items.tree_items[1].kind().is_path_collapsed());\n\n\t\tassert!(tree.move_selection(MoveSelection::Right));\n\t\tassert_eq!(tree.selection, Some(1));\n\t\tassert!(!tree.items.tree_items[1].kind().is_path_collapsed());\n\t}\n\n\t#[test]\n\tfn test_selection_top() {\n\t\tlet items = vec![\n\t\t\tPath::new(\"a/b/c\"), //\n\t\t\tPath::new(\"a/d\"),   //\n\t\t];\n\n\t\t//0 a/\n\t\t//1   b/\n\t\t//2     c\n\t\t//3   d\n\n\t\tlet mut tree =\n\t\t\tFileTree::new(&items, &BTreeSet::new()).unwrap();\n\n\t\ttree.selection = Some(3);\n\n\t\tassert!(tree.move_selection(MoveSelection::Top));\n\t\tassert_eq!(tree.selection, Some(0));\n\t}\n\n\t#[test]\n\tfn test_visible_selection() {\n\t\tlet items = vec![\n\t\t\tPath::new(\"a/b/c\"),  //\n\t\t\tPath::new(\"a/b/c2\"), //\n\t\t\tPath::new(\"a/d\"),    //\n\t\t];\n\n\t\t//0 a/\n\t\t//1   b/\n\t\t//2     c\n\t\t//3     c2\n\t\t//4   d\n\n\t\tlet mut tree =\n\t\t\tFileTree::new(&items, &BTreeSet::new()).unwrap();\n\n\t\ttree.selection = Some(1);\n\t\tassert!(tree.move_selection(MoveSelection::Left));\n\t\tassert!(tree.move_selection(MoveSelection::Down));\n\t\tlet s = tree.visual_selection().unwrap();\n\n\t\tassert_eq!(s.count, 3);\n\t\tassert_eq!(s.index, 2);\n\t}\n\n\t#[test]\n\tfn test_selection_page_updown() {\n\t\tlet items = vec![\n\t\t\tPath::new(\"a/b/c\"),  //\n\t\t\tPath::new(\"a/b/c2\"), //\n\t\t\tPath::new(\"a/d\"),    //\n\t\t\tPath::new(\"a/e\"),    //\n\t\t];\n\n\t\t//0 a/\n\t\t//1   b/\n\t\t//2     c\n\t\t//3     c2\n\t\t//4   d\n\t\t//5   e\n\n\t\tlet mut tree =\n\t\t\tFileTree::new(&items, &BTreeSet::new()).unwrap();\n\n\t\ttree.window_height.set(Some(3));\n\n\t\ttree.selection = Some(0);\n\t\tassert!(tree.move_selection(MoveSelection::PageDown));\n\t\tassert_eq!(tree.selection, Some(2));\n\t\tassert!(tree.move_selection(MoveSelection::PageDown));\n\t\tassert_eq!(tree.selection, Some(4));\n\t\tassert!(tree.move_selection(MoveSelection::PageUp));\n\t\tassert_eq!(tree.selection, Some(2));\n\t\tassert!(tree.move_selection(MoveSelection::PageUp));\n\t\tassert_eq!(tree.selection, Some(0));\n\t}\n}\n"
  },
  {
    "path": "filetreelist/src/filetreeitems.rs",
    "content": "use crate::{\n\terror::Error,\n\titem::{FileTreeItemKind, PathCollapsed},\n\tFileTreeItem,\n};\nuse crate::{error::Result, treeitems_iter::TreeItemsIterator};\nuse std::{\n\tcollections::{BTreeSet, HashMap},\n\tpath::{Path, PathBuf},\n};\n\n///\n#[derive(Default)]\npub struct FileTreeItems {\n\tpub tree_items: Vec<FileTreeItem>,\n\tfiles: usize,\n}\n\nimpl FileTreeItems {\n\t///\n\tpub fn new(\n\t\tlist: &[&Path],\n\t\tcollapsed: &BTreeSet<&String>,\n\t) -> Result<Self> {\n\t\tlet (mut items, paths) = Self::create_items(list, collapsed)?;\n\n\t\tSelf::fold_paths(&mut items, &paths);\n\n\t\tOk(Self {\n\t\t\ttree_items: items,\n\t\t\tfiles: list.len(),\n\t\t})\n\t}\n\n\tfn create_items<'a>(\n\t\tlist: &'a [&Path],\n\t\tcollapsed: &BTreeSet<&String>,\n\t) -> Result<(Vec<FileTreeItem>, HashMap<&'a Path, usize>)> {\n\t\t// scopetime::scope_time!(\"create_items\");\n\n\t\tlet mut items = Vec::with_capacity(list.len());\n\t\tlet mut paths_added: HashMap<&Path, usize> =\n\t\t\tHashMap::with_capacity(list.len());\n\n\t\tfor e in list {\n\t\t\t{\n\t\t\t\tSelf::push_dirs(\n\t\t\t\t\te,\n\t\t\t\t\t&mut items,\n\t\t\t\t\t&mut paths_added,\n\t\t\t\t\tcollapsed,\n\t\t\t\t)?;\n\t\t\t}\n\n\t\t\titems.push(FileTreeItem::new_file(e)?);\n\t\t}\n\n\t\tOk((items, paths_added))\n\t}\n\n\t/// how many individual items (files/paths) are in the list\n\tpub const fn len(&self) -> usize {\n\t\tself.tree_items.len()\n\t}\n\n\t/// how many files were added to this list\n\tpub const fn file_count(&self) -> usize {\n\t\tself.files\n\t}\n\n\t/// iterates visible elements\n\tpub const fn iterate(\n\t\t&self,\n\t\tstart: usize,\n\t\tmax_amount: usize,\n\t) -> TreeItemsIterator<'_> {\n\t\tTreeItemsIterator::new(self, start, max_amount)\n\t}\n\n\tfn push_dirs<'a>(\n\t\titem_path: &'a Path,\n\t\tnodes: &mut Vec<FileTreeItem>,\n\t\t// helps to only add new nodes for paths that were not added before\n\t\t// we also count the number of children a node has for later folding\n\t\tpaths_added: &mut HashMap<&'a Path, usize>,\n\t\tcollapsed: &BTreeSet<&String>,\n\t) -> Result<()> {\n\t\tlet mut ancestors =\n\t\t\titem_path.ancestors().skip(1).collect::<Vec<_>>();\n\t\tancestors.reverse();\n\n\t\tfor c in &ancestors {\n\t\t\tif c.parent().is_some() && !paths_added.contains_key(c) {\n\t\t\t\t// add node and set count to have no children\n\t\t\t\tpaths_added.insert(c, 0);\n\n\t\t\t\t// increase the number of children in the parent node count\n\t\t\t\tif let Some(parent) = c.parent() {\n\t\t\t\t\tif !parent.as_os_str().is_empty() {\n\t\t\t\t\t\t*paths_added.entry(parent).or_insert(0) += 1;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t//TODO: make non alloc\n\t\t\t\tlet path_string = Self::path_to_string(c)?;\n\t\t\t\tlet is_collapsed = collapsed.contains(&path_string);\n\t\t\t\tnodes.push(FileTreeItem::new_path(c, is_collapsed)?);\n\t\t\t}\n\t\t}\n\n\t\t// increase child count in parent node (the above ancenstor ignores the leaf component)\n\t\tif let Some(parent) = item_path.parent() {\n\t\t\t*paths_added.entry(parent).or_insert(0) += 1;\n\t\t}\n\n\t\tOk(())\n\t}\n\n\t//TODO: return ref\n\tfn path_to_string(p: &Path) -> Result<String> {\n\t\tOk(p.to_str()\n\t\t\t.map_or_else(\n\t\t\t\t|| Err(Error::InvalidPath(p.to_path_buf())),\n\t\t\t\tOk,\n\t\t\t)?\n\t\t\t.to_string())\n\t}\n\n\tpub fn collapse(&mut self, index: usize, recursive: bool) {\n\t\tif self.tree_items[index].kind().is_path() {\n\t\t\tself.tree_items[index].collapse_path();\n\n\t\t\tlet path = PathBuf::from(\n\t\t\t\tself.tree_items[index].info().full_path_str(),\n\t\t\t);\n\n\t\t\tfor i in index + 1..self.tree_items.len() {\n\t\t\t\tlet item = &mut self.tree_items[i];\n\n\t\t\t\tif recursive && item.kind().is_path() {\n\t\t\t\t\titem.collapse_path();\n\t\t\t\t}\n\n\t\t\t\tlet item_path =\n\t\t\t\t\tPath::new(item.info().full_path_str());\n\n\t\t\t\tif item_path.starts_with(&path) {\n\t\t\t\t\titem.hide();\n\t\t\t\t} else {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tpub fn expand(&mut self, index: usize, recursive: bool) {\n\t\tif self.tree_items[index].kind().is_path() {\n\t\t\tself.tree_items[index].expand_path();\n\n\t\t\tlet full_path = PathBuf::from(\n\t\t\t\tself.tree_items[index].info().full_path_str(),\n\t\t\t);\n\n\t\t\tif recursive {\n\t\t\t\tfor i in index + 1..self.tree_items.len() {\n\t\t\t\t\tlet item = &mut self.tree_items[i];\n\n\t\t\t\t\tif !Path::new(item.info().full_path_str())\n\t\t\t\t\t\t.starts_with(&full_path)\n\t\t\t\t\t{\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tif item.kind().is_path()\n\t\t\t\t\t\t&& item.kind().is_path_collapsed()\n\t\t\t\t\t{\n\t\t\t\t\t\titem.expand_path();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tself.update_visibility(\n\t\t\t\tSome(full_path).as_ref(),\n\t\t\t\tindex + 1,\n\t\t\t\tfalse,\n\t\t\t);\n\t\t}\n\t}\n\n\t/// makes sure `index` is visible.\n\t/// this expands all parents and shows all siblings\n\tpub fn show_element(&mut self, index: usize) -> Option<usize> {\n\t\tSome(\n\t\t\tself.show_element_upward(index)?\n\t\t\t\t+ self.show_element_downward(index)?,\n\t\t)\n\t}\n\n\tfn show_element_upward(&mut self, index: usize) -> Option<usize> {\n\t\tlet mut shown = 0_usize;\n\n\t\tlet item = self.tree_items.get(index)?;\n\t\tlet mut current_folder: (PathBuf, u8) = (\n\t\t\titem.info().full_path().parent()?.to_path_buf(),\n\t\t\titem.info().indent(),\n\t\t);\n\n\t\tlet item_count = self.tree_items.len();\n\t\tfor item in self\n\t\t\t.tree_items\n\t\t\t.iter_mut()\n\t\t\t.rev()\n\t\t\t.skip(item_count - index - 1)\n\t\t{\n\t\t\tif item.info().indent() == current_folder.1 {\n\t\t\t\titem.show();\n\t\t\t\tshown += 1;\n\t\t\t} else if item.info().indent() == current_folder.1 - 1 {\n\t\t\t\t// this must be our parent\n\n\t\t\t\titem.expand_path();\n\n\t\t\t\tif item.info().is_visible() {\n\t\t\t\t\t// early out if parent already visible\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\titem.show();\n\t\t\t\tshown += 1;\n\n\t\t\t\tcurrent_folder = (\n\t\t\t\t\titem.info().full_path().parent()?.to_path_buf(),\n\t\t\t\t\titem.info().indent(),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tSome(shown)\n\t}\n\n\tfn show_element_downward(\n\t\t&mut self,\n\t\tindex: usize,\n\t) -> Option<usize> {\n\t\tlet mut shown = 0_usize;\n\n\t\tlet item = self.tree_items.get(index)?;\n\t\tlet mut current_folder: (PathBuf, u8) = (\n\t\t\titem.info().full_path().parent()?.to_path_buf(),\n\t\t\titem.info().indent(),\n\t\t);\n\n\t\tfor item in self.tree_items.iter_mut().skip(index + 1) {\n\t\t\tif item.info().indent() == current_folder.1 {\n\t\t\t\titem.show();\n\t\t\t\tshown += 1;\n\t\t\t}\n\t\t\tif item.info().indent() == current_folder.1 - 1 {\n\t\t\t\t// this must be our parent\n\n\t\t\t\titem.show();\n\t\t\t\tshown += 1;\n\n\t\t\t\tcurrent_folder = (\n\t\t\t\t\titem.info().full_path().parent()?.to_path_buf(),\n\t\t\t\t\titem.info().indent(),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tSome(shown)\n\t}\n\n\tfn update_visibility(\n\t\t&mut self,\n\t\tprefix: Option<&PathBuf>,\n\t\tstart_idx: usize,\n\t\tset_defaults: bool,\n\t) {\n\t\t// if we are in any subpath that is collapsed we keep skipping over it\n\t\tlet mut inner_collapsed: Option<PathBuf> = None;\n\n\t\tfor i in start_idx..self.tree_items.len() {\n\t\t\tif let Some(ref collapsed_path) = inner_collapsed {\n\t\t\t\tlet p = Path::new(\n\t\t\t\t\tself.tree_items[i].info().full_path_str(),\n\t\t\t\t);\n\t\t\t\tif p.starts_with(collapsed_path) {\n\t\t\t\t\tif set_defaults {\n\t\t\t\t\t\tself.tree_items[i]\n\t\t\t\t\t\t\t.info_mut()\n\t\t\t\t\t\t\t.set_visible(false);\n\t\t\t\t\t}\n\t\t\t\t\t// we are still in a collapsed inner path\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tinner_collapsed = None;\n\t\t\t}\n\n\t\t\tlet item_kind = self.tree_items[i].kind().clone();\n\t\t\tlet item_path =\n\t\t\t\tPath::new(self.tree_items[i].info().full_path_str());\n\n\t\t\tif matches!(item_kind, FileTreeItemKind::Path(PathCollapsed(collapsed)) if collapsed)\n\t\t\t{\n\t\t\t\t// we encountered an inner path that is still collapsed\n\t\t\t\tinner_collapsed = Some(item_path.into());\n\t\t\t}\n\n\t\t\tif prefix\n\t\t\t\t.as_ref()\n\t\t\t\t.is_none_or(|prefix| item_path.starts_with(prefix))\n\t\t\t{\n\t\t\t\tself.tree_items[i].info_mut().set_visible(true);\n\t\t\t} else {\n\t\t\t\t// if we do not set defaults we can early out\n\t\t\t\tif set_defaults {\n\t\t\t\t\tself.tree_items[i].info_mut().set_visible(false);\n\t\t\t\t} else {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfn fold_paths(\n\t\titems: &mut Vec<FileTreeItem>,\n\t\tpaths: &HashMap<&Path, usize>,\n\t) {\n\t\tlet mut i = 0;\n\n\t\twhile i < items.len() {\n\t\t\tlet item = &items[i];\n\t\t\tif item.kind().is_path() {\n\t\t\t\tlet children = paths\n\t\t\t\t\t.get(&Path::new(item.info().full_path_str()));\n\n\t\t\t\tif let Some(children) = children {\n\t\t\t\t\tif *children == 1 {\n\t\t\t\t\t\tif i + 1 >= items.len() {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif items\n\t\t\t\t\t\t\t.get(i + 1)\n\t\t\t\t\t\t\t.is_some_and(|item| item.kind().is_path())\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tlet next_item = items.remove(i + 1);\n\t\t\t\t\t\t\tlet item_mut = &mut items[i];\n\t\t\t\t\t\t\titem_mut.fold(next_item);\n\n\t\t\t\t\t\t\tlet prefix = item_mut\n\t\t\t\t\t\t\t\t.info()\n\t\t\t\t\t\t\t\t.full_path_str()\n\t\t\t\t\t\t\t\t.to_owned();\n\n\t\t\t\t\t\t\tSelf::unindent(items, &prefix, i + 1);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ti += 1;\n\t\t}\n\t}\n\n\tfn unindent(\n\t\titems: &mut [FileTreeItem],\n\t\tprefix: &str,\n\t\tstart: usize,\n\t) {\n\t\tfor elem in items.iter_mut().skip(start) {\n\t\t\tif elem.info().full_path_str().starts_with(prefix) {\n\t\t\t\telem.info_mut().unindent();\n\t\t\t} else {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse pretty_assertions::assert_eq;\n\n\t#[test]\n\tfn test_simple() {\n\t\tlet items = vec![\n\t\t\tPath::new(\"file.txt\"), //\n\t\t];\n\n\t\tlet res =\n\t\t\tFileTreeItems::new(&items, &BTreeSet::new()).unwrap();\n\n\t\tassert!(res.tree_items[0].info().is_visible());\n\t\tassert_eq!(res.tree_items[0].info().indent(), 0);\n\t\tassert_eq!(res.tree_items[0].info().path(), items[0]);\n\t\tassert_eq!(res.tree_items[0].info().full_path(), items[0]);\n\n\t\tlet items = vec![\n\t\t\tPath::new(\"file.txt\"),  //\n\t\t\tPath::new(\"file2.txt\"), //\n\t\t];\n\n\t\tlet res =\n\t\t\tFileTreeItems::new(&items, &BTreeSet::new()).unwrap();\n\n\t\tassert_eq!(res.tree_items.len(), 2);\n\t\tassert_eq!(res.tree_items.len(), res.len());\n\t\tassert_eq!(res.tree_items[1].info().path(), items[1]);\n\t}\n\n\t#[test]\n\tfn test_push_path() {\n\t\tlet mut items = Vec::new();\n\t\tlet mut paths: HashMap<&Path, usize> = HashMap::new();\n\n\t\tFileTreeItems::push_dirs(\n\t\t\tPath::new(\"a/b/c\"),\n\t\t\t&mut items,\n\t\t\t&mut paths,\n\t\t\t&BTreeSet::new(),\n\t\t)\n\t\t.unwrap();\n\n\t\tassert_eq!(*paths.get(&Path::new(\"a\")).unwrap(), 1);\n\n\t\tFileTreeItems::push_dirs(\n\t\t\tPath::new(\"a/b2/c\"),\n\t\t\t&mut items,\n\t\t\t&mut paths,\n\t\t\t&BTreeSet::new(),\n\t\t)\n\t\t.unwrap();\n\n\t\tassert_eq!(*paths.get(&Path::new(\"a\")).unwrap(), 2);\n\t}\n\n\t#[test]\n\tfn test_push_path2() {\n\t\tlet mut items = Vec::new();\n\t\tlet mut paths: HashMap<&Path, usize> = HashMap::new();\n\n\t\tFileTreeItems::push_dirs(\n\t\t\tPath::new(\"a/b/c\"),\n\t\t\t&mut items,\n\t\t\t&mut paths,\n\t\t\t&BTreeSet::new(),\n\t\t)\n\t\t.unwrap();\n\n\t\tassert_eq!(*paths.get(&Path::new(\"a\")).unwrap(), 1);\n\t\tassert_eq!(*paths.get(&Path::new(\"a/b\")).unwrap(), 1);\n\n\t\tFileTreeItems::push_dirs(\n\t\t\tPath::new(\"a/b/d\"),\n\t\t\t&mut items,\n\t\t\t&mut paths,\n\t\t\t&BTreeSet::new(),\n\t\t)\n\t\t.unwrap();\n\n\t\tassert_eq!(*paths.get(&Path::new(\"a\")).unwrap(), 1);\n\t\tassert_eq!(*paths.get(&Path::new(\"a/b\")).unwrap(), 2);\n\t}\n\n\t#[test]\n\tfn test_folder() {\n\t\tlet items = vec![\n\t\t\tPath::new(\"a/file.txt\"), //\n\t\t];\n\n\t\tlet res = FileTreeItems::new(&items, &BTreeSet::new())\n\t\t\t.unwrap()\n\t\t\t.tree_items\n\t\t\t.iter()\n\t\t\t.map(|i| i.info().full_path_str().to_string())\n\t\t\t.collect::<Vec<_>>();\n\n\t\tassert_eq!(\n\t\t\tres,\n\t\t\tvec![String::from(\"a\"), String::from(\"a/file.txt\"),]\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_indent() {\n\t\tlet items = vec![\n\t\t\tPath::new(\"a/b/file.txt\"), //\n\t\t];\n\n\t\tlet list =\n\t\t\tFileTreeItems::new(&items, &BTreeSet::new()).unwrap();\n\t\tlet mut res = list\n\t\t\t.tree_items\n\t\t\t.iter()\n\t\t\t.map(|i| (i.info().indent(), i.info().path()));\n\n\t\tassert_eq!(res.next(), Some((0, Path::new(\"a/b\"))));\n\t\tassert_eq!(res.next(), Some((1, Path::new(\"file.txt\"))));\n\t}\n\n\t#[test]\n\tfn test_indent_folder_file_name() {\n\t\tlet items = vec![\n\t\t\tPath::new(\"a/b\"),   //\n\t\t\tPath::new(\"a.txt\"), //\n\t\t];\n\n\t\tlet list =\n\t\t\tFileTreeItems::new(&items, &BTreeSet::new()).unwrap();\n\t\tlet mut res = list\n\t\t\t.tree_items\n\t\t\t.iter()\n\t\t\t.map(|i| (i.info().indent(), i.info().path_str()));\n\n\t\tassert_eq!(res.next(), Some((0, \"a\")));\n\t\tassert_eq!(res.next(), Some((1, \"b\")));\n\t\tassert_eq!(res.next(), Some((0, \"a.txt\")));\n\t}\n\n\t#[test]\n\tfn test_folder_dup() {\n\t\tlet items = vec![\n\t\t\tPath::new(\"a/file.txt\"),  //\n\t\t\tPath::new(\"a/file2.txt\"), //\n\t\t];\n\n\t\tlet tree =\n\t\t\tFileTreeItems::new(&items, &BTreeSet::new()).unwrap();\n\n\t\tassert_eq!(tree.file_count(), 2);\n\t\tassert_eq!(tree.len(), 3);\n\n\t\tlet res = tree\n\t\t\t.tree_items\n\t\t\t.iter()\n\t\t\t.map(|i| i.info().full_path_str().to_string())\n\t\t\t.collect::<Vec<_>>();\n\n\t\tassert_eq!(\n\t\t\tres,\n\t\t\tvec![\n\t\t\t\tString::from(\"a\"),\n\t\t\t\tString::from(\"a/file.txt\"),\n\t\t\t\tString::from(\"a/file2.txt\"),\n\t\t\t]\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_collapse() {\n\t\tlet items = vec![\n\t\t\tPath::new(\"a/file1.txt\"), //\n\t\t\tPath::new(\"b/file2.txt\"), //\n\t\t];\n\n\t\tlet mut tree =\n\t\t\tFileTreeItems::new(&items, &BTreeSet::new()).unwrap();\n\n\t\tassert!(tree.tree_items[1].info().is_visible());\n\n\t\ttree.collapse(0, false);\n\n\t\tassert!(!tree.tree_items[1].info().is_visible());\n\t}\n\n\t#[test]\n\tfn test_iterate_collapsed() {\n\t\tlet items = vec![\n\t\t\tPath::new(\"a/file1.txt\"), //\n\t\t\tPath::new(\"b/file2.txt\"), //\n\t\t];\n\n\t\tlet mut tree =\n\t\t\tFileTreeItems::new(&items, &BTreeSet::new()).unwrap();\n\n\t\ttree.collapse(0, false);\n\n\t\tlet mut it = tree.iterate(0, 10);\n\n\t\tassert_eq!(it.next().unwrap().0, 0);\n\t\tassert_eq!(it.next().unwrap().0, 2);\n\t\tassert_eq!(it.next().unwrap().0, 3);\n\t\tassert_eq!(it.next(), None);\n\t}\n\n\tpub fn get_visible(tree: &FileTreeItems) -> Vec<bool> {\n\t\ttree.tree_items\n\t\t\t.iter()\n\t\t\t.map(|e| e.info().is_visible())\n\t\t\t.collect::<Vec<_>>()\n\t}\n\n\t#[test]\n\tfn test_expand() {\n\t\tlet items = vec![\n\t\t\tPath::new(\"a/b/c\"), //\n\t\t\tPath::new(\"a/d\"),   //\n\t\t];\n\n\t\t//0 a/\n\t\t//1   b/\n\t\t//2     c\n\t\t//3   d\n\n\t\tlet mut tree =\n\t\t\tFileTreeItems::new(&items, &BTreeSet::new()).unwrap();\n\n\t\ttree.collapse(1, false);\n\n\t\tlet visibles = get_visible(&tree);\n\n\t\tassert_eq!(\n\t\t\tvisibles,\n\t\t\tvec![\n\t\t\t\ttrue,  //\n\t\t\t\ttrue,  //\n\t\t\t\tfalse, //\n\t\t\t\ttrue,\n\t\t\t]\n\t\t);\n\n\t\ttree.expand(1, false);\n\n\t\tlet visibles = get_visible(&tree);\n\n\t\tassert_eq!(\n\t\t\tvisibles,\n\t\t\tvec![\n\t\t\t\ttrue, //\n\t\t\t\ttrue, //\n\t\t\t\ttrue, //\n\t\t\t\ttrue,\n\t\t\t]\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_expand_bug() {\n\t\tlet items = vec![\n\t\t\tPath::new(\"a/b/c\"),  //\n\t\t\tPath::new(\"a/b2/d\"), //\n\t\t];\n\n\t\t//0 a/\n\t\t//1   b/\n\t\t//2     c\n\t\t//3   b2/\n\t\t//4     d\n\n\t\tlet mut tree =\n\t\t\tFileTreeItems::new(&items, &BTreeSet::new()).unwrap();\n\n\t\ttree.collapse(1, false);\n\t\ttree.collapse(0, false);\n\n\t\tassert_eq!(\n\t\t\tget_visible(&tree),\n\t\t\tvec![\n\t\t\t\ttrue,  //\n\t\t\t\tfalse, //\n\t\t\t\tfalse, //\n\t\t\t\tfalse, //\n\t\t\t\tfalse,\n\t\t\t]\n\t\t);\n\n\t\ttree.expand(0, false);\n\n\t\tassert_eq!(\n\t\t\tget_visible(&tree),\n\t\t\tvec![\n\t\t\t\ttrue,  //\n\t\t\t\ttrue,  //\n\t\t\t\tfalse, //\n\t\t\t\ttrue,  //\n\t\t\t\ttrue,\n\t\t\t]\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_collapse_too_much() {\n\t\tlet items = vec![\n\t\t\tPath::new(\"a/b\"),  //\n\t\t\tPath::new(\"a2/c\"), //\n\t\t];\n\n\t\t//0 a/\n\t\t//1   b\n\t\t//2 a2/\n\t\t//3   c\n\n\t\tlet mut tree =\n\t\t\tFileTreeItems::new(&items, &BTreeSet::new()).unwrap();\n\n\t\ttree.collapse(0, false);\n\n\t\tlet visibles = get_visible(&tree);\n\n\t\tassert_eq!(\n\t\t\tvisibles,\n\t\t\tvec![\n\t\t\t\ttrue,  //\n\t\t\t\tfalse, //\n\t\t\t\ttrue,  //\n\t\t\t\ttrue,\n\t\t\t]\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_expand_with_collapsed_sub_parts() {\n\t\tlet items = vec![\n\t\t\tPath::new(\"a/b/c\"), //\n\t\t\tPath::new(\"a/d\"),   //\n\t\t];\n\n\t\t//0 a/\n\t\t//1   b/\n\t\t//2     c\n\t\t//3   d\n\n\t\tlet mut tree =\n\t\t\tFileTreeItems::new(&items, &BTreeSet::new()).unwrap();\n\n\t\ttree.collapse(1, false);\n\n\t\tlet visibles = get_visible(&tree);\n\n\t\tassert_eq!(\n\t\t\tvisibles,\n\t\t\tvec![\n\t\t\t\ttrue,  //\n\t\t\t\ttrue,  //\n\t\t\t\tfalse, //\n\t\t\t\ttrue,\n\t\t\t]\n\t\t);\n\n\t\ttree.collapse(0, false);\n\n\t\tlet visibles = get_visible(&tree);\n\n\t\tassert_eq!(\n\t\t\tvisibles,\n\t\t\tvec![\n\t\t\t\ttrue,  //\n\t\t\t\tfalse, //\n\t\t\t\tfalse, //\n\t\t\t\tfalse,\n\t\t\t]\n\t\t);\n\n\t\ttree.expand(0, false);\n\n\t\tlet visible = get_visible(&tree);\n\n\t\tassert_eq!(\n\t\t\tvisible,\n\t\t\tvec![\n\t\t\t\ttrue,  //\n\t\t\t\ttrue,  //\n\t\t\t\tfalse, //\n\t\t\t\ttrue,\n\t\t\t]\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_show_element() {\n\t\tlet items = vec![\n\t\t\tPath::new(\"a/b/c\"),  //\n\t\t\tPath::new(\"a/b2/d\"), //\n\t\t\tPath::new(\"a/b2/e\"), //\n\t\t];\n\n\t\t//0 a/\n\t\t//1   b/\n\t\t//2     c\n\t\t//3   b2/\n\t\t//4   \td\n\t\t//5     e\n\n\t\tlet mut tree =\n\t\t\tFileTreeItems::new(&items, &BTreeSet::new()).unwrap();\n\n\t\ttree.collapse(0, true);\n\n\t\tlet res = tree.show_element(5).unwrap();\n\t\tassert_eq!(res, 4);\n\t\tassert!(tree.tree_items[3].kind().is_path());\n\t\tassert!(!tree.tree_items[3].kind().is_path_collapsed());\n\n\t\tassert_eq!(\n\t\t\tget_visible(&tree),\n\t\t\tvec![\n\t\t\t\ttrue,  //\n\t\t\t\ttrue,  //\n\t\t\t\tfalse, //\n\t\t\t\ttrue,  //\n\t\t\t\ttrue,  //\n\t\t\t\ttrue,\n\t\t\t]\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_show_element_later_elements() {\n\t\tlet items = vec![\n\t\t\tPath::new(\"a/b\"), //\n\t\t\tPath::new(\"a/c\"), //\n\t\t];\n\n\t\t//0 a/\n\t\t//1   b\n\t\t//2   c\n\n\t\tlet mut tree =\n\t\t\tFileTreeItems::new(&items, &BTreeSet::new()).unwrap();\n\n\t\ttree.collapse(0, true);\n\n\t\tassert_eq!(\n\t\t\tget_visible(&tree),\n\t\t\tvec![\n\t\t\t\ttrue,  //\n\t\t\t\tfalse, //\n\t\t\t\tfalse, //\n\t\t\t]\n\t\t);\n\n\t\tlet res = tree.show_element(1).unwrap();\n\t\tassert_eq!(res, 2);\n\n\t\tassert_eq!(\n\t\t\tget_visible(&tree),\n\t\t\tvec![\n\t\t\t\ttrue, //\n\t\t\t\ttrue, //\n\t\t\t\ttrue, //\n\t\t\t]\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_show_element_downward_parent() {\n\t\tlet items = vec![\n\t\t\tPath::new(\"a/b/c\"), //\n\t\t\tPath::new(\"a/d\"),   //\n\t\t\tPath::new(\"a/e\"),   //\n\t\t];\n\n\t\t//0 a/\n\t\t//1   b/\n\t\t//2     c\n\t\t//3   d\n\t\t//4   e\n\n\t\tlet mut tree =\n\t\t\tFileTreeItems::new(&items, &BTreeSet::new()).unwrap();\n\n\t\ttree.collapse(0, true);\n\n\t\tlet res = tree.show_element(2).unwrap();\n\t\tassert_eq!(res, 4);\n\n\t\tassert_eq!(\n\t\t\tget_visible(&tree),\n\t\t\tvec![\n\t\t\t\ttrue, //\n\t\t\t\ttrue, //\n\t\t\t\ttrue, //\n\t\t\t\ttrue, //\n\t\t\t\ttrue, //\n\t\t\t]\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_show_element_expand_visible_parent() {\n\t\tlet items = vec![\n\t\t\tPath::new(\"a/b\"), //\n\t\t];\n\n\t\t//0 a/\n\t\t//1   b\n\n\t\tlet mut tree =\n\t\t\tFileTreeItems::new(&items, &BTreeSet::new()).unwrap();\n\n\t\ttree.collapse(0, true);\n\n\t\tassert_eq!(\n\t\t\tget_visible(&tree),\n\t\t\tvec![\n\t\t\t\ttrue,  //\n\t\t\t\tfalse, //\n\t\t\t]\n\t\t);\n\n\t\tlet res = tree.show_element(1).unwrap();\n\t\tassert_eq!(res, 1);\n\t\tassert!(tree.tree_items[0].kind().is_path());\n\t\tassert!(!tree.tree_items[0].kind().is_path_collapsed());\n\n\t\tassert_eq!(\n\t\t\tget_visible(&tree),\n\t\t\tvec![\n\t\t\t\ttrue, //\n\t\t\t\ttrue, //\n\t\t\t]\n\t\t);\n\t}\n}\n\n#[cfg(test)]\nmod test_merging {\n\tuse super::*;\n\tuse pretty_assertions::assert_eq;\n\n\t#[test]\n\tfn test_merge_simple() {\n\t\tlet list = vec![Path::new(\"a/b/c\")];\n\t\tlet (mut items, paths) =\n\t\t\tFileTreeItems::create_items(&list, &BTreeSet::new())\n\t\t\t\t.unwrap();\n\n\t\tassert_eq!(items.len(), 3);\n\n\t\tFileTreeItems::fold_paths(&mut items, &paths);\n\n\t\tassert_eq!(items.len(), 2);\n\t}\n\n\t#[test]\n\tfn test_merge_simple2() {\n\t\tlet list = vec![\n\t\t\tPath::new(\"a/b/c\"), //\n\t\t\tPath::new(\"a/b/d\"), //\n\t\t];\n\t\tlet (mut items, paths) =\n\t\t\tFileTreeItems::create_items(&list, &BTreeSet::new())\n\t\t\t\t.unwrap();\n\n\t\tassert_eq!(paths.len(), 2);\n\t\tassert_eq!(*paths.get(&Path::new(\"a\")).unwrap(), 1);\n\t\tassert_eq!(*paths.get(&Path::new(\"a/b\")).unwrap(), 2);\n\t\tassert_eq!(items.len(), 4);\n\n\t\tFileTreeItems::fold_paths(&mut items, &paths);\n\n\t\tassert_eq!(items.len(), 3);\n\t}\n\n\t#[test]\n\tfn test_merge_indent() {\n\t\tlet list = vec![\n\t\t\tPath::new(\"a/b/c/d\"), //\n\t\t\tPath::new(\"a/e/f\"),   //\n\t\t];\n\n\t\t//0:0 a/\n\t\t//1:1   b/c\n\t\t//2:2     d\n\t\t//3:1   e/\n\t\t//4:2     f\n\n\t\tlet (mut items, paths) =\n\t\t\tFileTreeItems::create_items(&list, &BTreeSet::new())\n\t\t\t\t.unwrap();\n\n\t\tassert_eq!(items.len(), 6);\n\n\t\tassert_eq!(paths.len(), 4);\n\t\tassert_eq!(*paths.get(&Path::new(\"a\")).unwrap(), 2);\n\t\tassert_eq!(*paths.get(&Path::new(\"a/b\")).unwrap(), 1);\n\t\tassert_eq!(*paths.get(&Path::new(\"a/b/c\")).unwrap(), 1);\n\t\tassert_eq!(*paths.get(&Path::new(\"a/e\")).unwrap(), 1);\n\n\t\tFileTreeItems::fold_paths(&mut items, &paths);\n\n\t\tlet indents: Vec<u8> =\n\t\t\titems.iter().map(|i| i.info().indent()).collect();\n\t\tassert_eq!(indents, vec![0, 1, 2, 1, 2]);\n\t}\n\n\t#[test]\n\tfn test_merge_single_paths() {\n\t\tlet items = vec![\n\t\t\tPath::new(\"a/b/c\"), //\n\t\t\tPath::new(\"a/b/d\"), //\n\t\t];\n\n\t\t//0 a/b/\n\t\t//1   c\n\t\t//2   d\n\n\t\tlet tree =\n\t\t\tFileTreeItems::new(&items, &BTreeSet::new()).unwrap();\n\n\t\tlet mut it = tree\n\t\t\t.iterate(0, 10)\n\t\t\t.map(|(_, item)| item.info().full_path_str());\n\n\t\tassert_eq!(it.next().unwrap(), \"a/b\");\n\t\tassert_eq!(it.next().unwrap(), \"a/b/c\");\n\t\tassert_eq!(it.next().unwrap(), \"a/b/d\");\n\t\tassert_eq!(it.next(), None);\n\t}\n\n\t#[test]\n\tfn test_merge_nothing() {\n\t\tlet items = vec![\n\t\t\tPath::new(\"a/b/c\"),  //\n\t\t\tPath::new(\"a/b2/d\"), //\n\t\t];\n\n\t\t//0 a/\n\t\t//1   b/\n\t\t//2     c\n\t\t//3   b2/\n\t\t//4     d\n\n\t\tlet tree =\n\t\t\tFileTreeItems::new(&items, &BTreeSet::new()).unwrap();\n\n\t\tlet mut it = tree\n\t\t\t.iterate(0, 10)\n\t\t\t.map(|(_, item)| item.info().full_path_str());\n\n\t\tassert_eq!(it.next().unwrap(), \"a\");\n\t\tassert_eq!(it.next().unwrap(), \"a/b\");\n\t\tassert_eq!(it.next().unwrap(), \"a/b/c\");\n\t\tassert_eq!(it.next().unwrap(), \"a/b2\");\n\t\tassert_eq!(it.next().unwrap(), \"a/b2/d\");\n\t\tassert_eq!(it.next(), None);\n\t}\n}\n"
  },
  {
    "path": "filetreelist/src/item.rs",
    "content": "use crate::error::Result;\nuse std::path::{Path, PathBuf};\n\n/// holds the information shared among all `FileTreeItem` in a `FileTree`\n#[derive(Debug, Clone)]\npub struct TreeItemInfo {\n\t/// indent level\n\tindent: u8,\n\t/// currently visible depending on the folder collapse states\n\tvisible: bool,\n\t/// contains this paths last component and folded up paths added to it\n\t/// if this is `None` nothing was folding into here\n\tfolded: Option<PathBuf>,\n\t/// the full path\n\tfull_path: PathBuf,\n}\n\nimpl TreeItemInfo {\n\t///\n\tpub const fn new(indent: u8, full_path: PathBuf) -> Self {\n\t\tSelf {\n\t\t\tindent,\n\t\t\tvisible: true,\n\t\t\tfolded: None,\n\t\t\tfull_path,\n\t\t}\n\t}\n\n\t///\n\tpub const fn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\t///\n\t//TODO: remove\n\tpub fn full_path_str(&self) -> &str {\n\t\tself.full_path.to_str().unwrap_or_default()\n\t}\n\n\t///\n\tpub fn full_path(&self) -> &Path {\n\t\tself.full_path.as_path()\n\t}\n\n\t/// like `path` but as `&str`\n\tpub fn path_str(&self) -> &str {\n\t\tself.path().as_os_str().to_str().unwrap_or_default()\n\t}\n\n\t/// returns the last component of `full_path`\n\t/// or the last components plus folded up children paths\n\tpub fn path(&self) -> &Path {\n\t\tself.folded.as_ref().map_or_else(\n\t\t\t|| {\n\t\t\t\tPath::new(\n\t\t\t\t\tself.full_path\n\t\t\t\t\t\t.components()\n\t\t\t\t\t\t.next_back()\n\t\t\t\t\t\t.and_then(|c| c.as_os_str().to_str())\n\t\t\t\t\t\t.unwrap_or_default(),\n\t\t\t\t)\n\t\t\t},\n\t\t\tPathBuf::as_path,\n\t\t)\n\t}\n\n\t///\n\tpub const fn indent(&self) -> u8 {\n\t\tself.indent\n\t}\n\n\t///\n\tpub const fn unindent(&mut self) {\n\t\tself.indent = self.indent.saturating_sub(1);\n\t}\n\n\tpub const fn set_visible(&mut self, visible: bool) {\n\t\tself.visible = visible;\n\t}\n}\n\n/// attribute used to indicate the collapse/expand state of a path item\n#[derive(PartialEq, Eq, Debug, Copy, Clone)]\npub struct PathCollapsed(pub bool);\n\n/// `FileTreeItem` can be of two kinds\n#[derive(PartialEq, Eq, Debug, Clone)]\npub enum FileTreeItemKind {\n\tPath(PathCollapsed),\n\tFile,\n}\n\nimpl FileTreeItemKind {\n\tpub const fn is_path(&self) -> bool {\n\t\tmatches!(self, Self::Path(_))\n\t}\n\n\tpub const fn is_path_collapsed(&self) -> bool {\n\t\tmatch self {\n\t\t\tSelf::Path(collapsed) => collapsed.0,\n\t\t\tSelf::File => false,\n\t\t}\n\t}\n}\n\n/// `FileTreeItem` can be of two kinds: see `FileTreeItem` but shares an info\n#[derive(Debug, Clone)]\npub struct FileTreeItem {\n\tinfo: TreeItemInfo,\n\tkind: FileTreeItemKind,\n}\n\nimpl FileTreeItem {\n\tpub fn new_file(path: &Path) -> Result<Self> {\n\t\tlet item_path = PathBuf::from(path);\n\n\t\tlet indent = u8::try_from(\n\t\t\titem_path.ancestors().count().saturating_sub(2),\n\t\t)?;\n\n\t\tOk(Self {\n\t\t\tinfo: TreeItemInfo::new(indent, item_path),\n\t\t\tkind: FileTreeItemKind::File,\n\t\t})\n\t}\n\n\tpub fn new_path(path: &Path, collapsed: bool) -> Result<Self> {\n\t\tlet indent =\n\t\t\tu8::try_from(path.ancestors().count().saturating_sub(2))?;\n\n\t\tOk(Self {\n\t\t\tinfo: TreeItemInfo::new(indent, path.to_owned()),\n\t\t\tkind: FileTreeItemKind::Path(PathCollapsed(collapsed)),\n\t\t})\n\t}\n\n\t///\n\tpub fn fold(&mut self, next: Self) {\n\t\tif let Some(folded) = self.info.folded.as_mut() {\n\t\t\t*folded = folded.join(next.info.path());\n\t\t} else {\n\t\t\tself.info.folded =\n\t\t\t\tSome(self.info.path().join(next.info.path()));\n\t\t}\n\n\t\tself.info.full_path = next.info.full_path;\n\t}\n\n\t///\n\tpub const fn info(&self) -> &TreeItemInfo {\n\t\t&self.info\n\t}\n\n\t///\n\tpub const fn info_mut(&mut self) -> &mut TreeItemInfo {\n\t\t&mut self.info\n\t}\n\n\t///\n\tpub const fn kind(&self) -> &FileTreeItemKind {\n\t\t&self.kind\n\t}\n\n\t/// # Panics\n\t/// panics if self is not a path\n\tpub fn collapse_path(&mut self) {\n\t\tassert!(self.kind.is_path());\n\t\tself.kind = FileTreeItemKind::Path(PathCollapsed(true));\n\t}\n\n\t/// # Panics\n\t/// panics if self is not a path\n\tpub fn expand_path(&mut self) {\n\t\tassert!(self.kind.is_path());\n\t\tself.kind = FileTreeItemKind::Path(PathCollapsed(false));\n\t}\n\n\t///\n\tpub const fn hide(&mut self) {\n\t\tself.info.visible = false;\n\t}\n\n\t///\n\tpub const fn show(&mut self) {\n\t\tself.info.visible = true;\n\t}\n}\n\nimpl Eq for FileTreeItem {}\n\nimpl PartialEq for FileTreeItem {\n\tfn eq(&self, other: &Self) -> bool {\n\t\tself.info.full_path.eq(&other.info.full_path)\n\t}\n}\n\nimpl PartialOrd for FileTreeItem {\n\tfn partial_cmp(\n\t\t&self,\n\t\tother: &Self,\n\t) -> Option<std::cmp::Ordering> {\n\t\tSome(self.cmp(other))\n\t}\n}\n\nimpl Ord for FileTreeItem {\n\tfn cmp(&self, other: &Self) -> std::cmp::Ordering {\n\t\tself.info.path().cmp(other.info.path())\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse pretty_assertions::assert_eq;\n\n\t#[test]\n\tfn test_smoke() {\n\t\tlet mut a =\n\t\t\tFileTreeItem::new_path(Path::new(\"a\"), false).unwrap();\n\n\t\tassert_eq!(a.info.full_path_str(), \"a\");\n\t\tassert_eq!(a.info.path_str(), \"a\");\n\n\t\tlet b =\n\t\t\tFileTreeItem::new_path(Path::new(\"a/b\"), false).unwrap();\n\t\ta.fold(b);\n\n\t\tassert_eq!(a.info.full_path_str(), \"a/b\");\n\t\tassert_eq!(\n\t\t\t&a.info.folded.as_ref().unwrap(),\n\t\t\t&Path::new(\"a/b\")\n\t\t);\n\t\tassert_eq!(a.info.path(), Path::new(\"a/b\"));\n\t}\n}\n"
  },
  {
    "path": "filetreelist/src/lib.rs",
    "content": "// #![forbid(missing_docs)]\n#![forbid(unsafe_code)]\n#![deny(\n\tmismatched_lifetime_syntaxes,\n\tunused_imports,\n\tunused_must_use,\n\tdead_code,\n\tunstable_name_collisions,\n\tunused_assignments\n)]\n#![deny(clippy::all, clippy::perf, clippy::nursery, clippy::pedantic)]\n#![deny(clippy::expect_used)]\n#![deny(clippy::filetype_is_file)]\n#![deny(clippy::cargo)]\n#![deny(clippy::unwrap_used)]\n#![deny(clippy::panic)]\n#![deny(clippy::match_like_matches_macro)]\n#![deny(clippy::needless_update)]\n#![allow(\n\tclippy::module_name_repetitions,\n\tclippy::must_use_candidate,\n\tclippy::missing_errors_doc,\n\tclippy::empty_docs\n)]\n\nmod error;\nmod filetree;\nmod filetreeitems;\nmod item;\nmod tree_iter;\nmod treeitems_iter;\n\npub use crate::{\n\tfiletree::FileTree,\n\tfiletree::MoveSelection,\n\titem::{FileTreeItem, TreeItemInfo},\n};\n"
  },
  {
    "path": "filetreelist/src/tree_iter.rs",
    "content": "use crate::{item::FileTreeItem, treeitems_iter::TreeItemsIterator};\n\npub struct TreeIterator<'a> {\n\titem_iter: TreeItemsIterator<'a>,\n\tselection: Option<usize>,\n}\n\nimpl<'a> TreeIterator<'a> {\n\tpub const fn new(\n\t\titem_iter: TreeItemsIterator<'a>,\n\t\tselection: Option<usize>,\n\t) -> Self {\n\t\tSelf {\n\t\t\titem_iter,\n\t\t\tselection,\n\t\t}\n\t}\n}\n\nimpl<'a> Iterator for TreeIterator<'a> {\n\ttype Item = (&'a FileTreeItem, bool);\n\n\tfn next(&mut self) -> Option<Self::Item> {\n\t\tself.item_iter.next().map(|(index, item)| {\n\t\t\t(item, self.selection.is_some_and(|i| i == index))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "filetreelist/src/treeitems_iter.rs",
    "content": "use crate::{filetreeitems::FileTreeItems, item::FileTreeItem};\n\npub struct TreeItemsIterator<'a> {\n\ttree: &'a FileTreeItems,\n\tindex: usize,\n\tincrements: Option<usize>,\n\tmax_amount: usize,\n}\n\nimpl<'a> TreeItemsIterator<'a> {\n\tpub const fn new(\n\t\ttree: &'a FileTreeItems,\n\t\tstart: usize,\n\t\tmax_amount: usize,\n\t) -> Self {\n\t\tTreeItemsIterator {\n\t\t\tmax_amount,\n\t\t\tincrements: None,\n\t\t\tindex: start,\n\t\t\ttree,\n\t\t}\n\t}\n}\n\nimpl<'a> Iterator for TreeItemsIterator<'a> {\n\ttype Item = (usize, &'a FileTreeItem);\n\n\tfn next(&mut self) -> Option<Self::Item> {\n\t\tif self.increments.unwrap_or_default() < self.max_amount {\n\t\t\tlet items = &self.tree.tree_items;\n\n\t\t\tlet mut init = self.increments.is_none();\n\n\t\t\tif let Some(i) = self.increments.as_mut() {\n\t\t\t\t*i += 1;\n\t\t\t} else {\n\t\t\t\tself.increments = Some(0);\n\t\t\t}\n\n\t\t\tloop {\n\t\t\t\tif !init {\n\t\t\t\t\tself.index += 1;\n\t\t\t\t}\n\t\t\t\tinit = false;\n\n\t\t\t\tif self.index >= self.tree.len() {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tlet elem = &items[self.index];\n\n\t\t\t\tif elem.info().is_visible() {\n\t\t\t\t\treturn Some((self.index, &items[self.index]));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tNone\n\t}\n}\n"
  },
  {
    "path": "git2-hooks/Cargo.toml",
    "content": "[package]\nname = \"git2-hooks\"\nversion = \"0.6.0\"\nauthors = [\"extrawurst <mail@rusticorn.com>\"]\nedition = \"2021\"\ndescription = \"adds git hooks support based on git2-rs\"\nhomepage = \"https://github.com/gitui-org/gitui\"\nrepository = \"https://github.com/gitui-org/gitui\"\ndocumentation = \"https://docs.rs/git2-hooks/\"\nreadme = \"README.md\"\nlicense = \"MIT\"\ncategories = [\"development-tools\"]\nkeywords = [\"git\"]\n\n[dependencies]\ngit2 = \">=0.17\"\ngix-path = \"0.11\"\nlog = \"0.4\"\nshellexpand = \"3.1\"\nthiserror = \"2.0\"\n\n[dev-dependencies]\ngit2-testing = { path = \"../git2-testing\" }\npretty_assertions = \"1.4\"\ntempfile = \"3\"\n"
  },
  {
    "path": "git2-hooks/README.md",
    "content": "# git2-hooks\n\nadds git hook functionality on top of git2-rs\n\n## todo\n\n- [ ] unittest coverage symlinks from `.git/hooks/<hook>` -> `X`\n- [ ] unittest coverage `~` expansion inside `core.hooksPath`"
  },
  {
    "path": "git2-hooks/src/error.rs",
    "content": "use thiserror::Error;\n\n/// crate specific error type\n#[derive(Error, Debug)]\npub enum HooksError {\n\t#[error(\"git error:{0}\")]\n\tGit(#[from] git2::Error),\n\n\t#[error(\"io error:{0}\")]\n\tIo(#[from] std::io::Error),\n\n\t#[error(\"path string conversion error\")]\n\tPathToString,\n\n\t#[error(\"shellexpand error:{0}\")]\n\tShellExpand(#[from] shellexpand::LookupError<std::env::VarError>),\n\n\t#[error(\"hook process terminated by signal without exit code\")]\n\tNoExitCode,\n}\n\n/// crate specific `Result` type\npub type Result<T> = std::result::Result<T, HooksError>;\n"
  },
  {
    "path": "git2-hooks/src/hookspath.rs",
    "content": "use git2::Repository;\n\nuse crate::{error::Result, HookResult, HooksError};\n\nuse std::{\n\tffi::{OsStr, OsString},\n\tpath::{Path, PathBuf},\n\tprocess::Command,\n\tstr::FromStr,\n};\n\npub struct HookPaths {\n\tpub git: PathBuf,\n\tpub hook: PathBuf,\n\tpub pwd: PathBuf,\n}\n\nconst CONFIG_HOOKS_PATH: &str = \"core.hooksPath\";\nconst DEFAULT_HOOKS_PATH: &str = \"hooks\";\nconst ENOEXEC: i32 = 8;\n\nimpl HookPaths {\n\t/// `core.hooksPath` always takes precedence.\n\t/// If its defined and there is no hook `hook` this is not considered\n\t/// an error or a reason to search in other paths.\n\t/// If the config is not set we go into search mode and\n\t/// first check standard `.git/hooks` folder and any sub path provided in `other_paths`.\n\t///\n\t/// Note: we try to model as closely as possible what git shell is doing.\n\tpub fn new(\n\t\trepo: &Repository,\n\t\tother_paths: Option<&[&str]>,\n\t\thook: &str,\n\t) -> Result<Self> {\n\t\tlet pwd = repo\n\t\t\t.workdir()\n\t\t\t.unwrap_or_else(|| repo.path())\n\t\t\t.to_path_buf();\n\n\t\tlet git_dir = repo.path().to_path_buf();\n\n\t\tif let Some(config_path) = Self::config_hook_path(repo)? {\n\t\t\tlet hooks_path = PathBuf::from(config_path);\n\n\t\t\tlet hook =\n\t\t\t\tSelf::expand_path(&hooks_path.join(hook), &pwd)?;\n\n\t\t\treturn Ok(Self {\n\t\t\t\tgit: git_dir,\n\t\t\t\thook,\n\t\t\t\tpwd,\n\t\t\t});\n\t\t}\n\n\t\tOk(Self {\n\t\t\tgit: git_dir,\n\t\t\thook: Self::find_hook(repo, other_paths, hook),\n\t\t\tpwd,\n\t\t})\n\t}\n\n\t/// Expand path according to the rule of githooks and config\n\t/// core.hooksPath\n\tfn expand_path(path: &Path, pwd: &Path) -> Result<PathBuf> {\n\t\tlet hook_expanded = shellexpand::full(\n\t\t\tpath.as_os_str()\n\t\t\t\t.to_str()\n\t\t\t\t.ok_or(HooksError::PathToString)?,\n\t\t)?;\n\t\tlet hook_expanded = PathBuf::from_str(hook_expanded.as_ref())\n\t\t\t.map_err(|_| HooksError::PathToString)?;\n\n\t\t// `man git-config`:\n\t\t//\n\t\t// > A relative path is taken as relative to the\n\t\t// > directory where the hooks are run (see the\n\t\t// > \"DESCRIPTION\" section of githooks[5]).\n\t\t//\n\t\t// `man githooks`:\n\t\t//\n\t\t// > Before Git invokes a hook, it changes its\n\t\t// > working directory to either $GIT_DIR in a bare\n\t\t// > repository or the root of the working tree in a\n\t\t// > non-bare repository.\n\t\t//\n\t\t// I.e. relative paths in core.hooksPath in non-bare\n\t\t// repositories are always relative to GIT_WORK_TREE.\n\t\tOk({\n\t\t\tif hook_expanded.is_absolute() {\n\t\t\t\thook_expanded\n\t\t\t} else {\n\t\t\t\tpwd.join(hook_expanded)\n\t\t\t}\n\t\t})\n\t}\n\n\tfn config_hook_path(repo: &Repository) -> Result<Option<String>> {\n\t\tOk(repo.config()?.get_string(CONFIG_HOOKS_PATH).ok())\n\t}\n\n\t/// check default hook path first and then followed by `other_paths`.\n\t/// if no hook is found we return the default hook path\n\tfn find_hook(\n\t\trepo: &Repository,\n\t\tother_paths: Option<&[&str]>,\n\t\thook: &str,\n\t) -> PathBuf {\n\t\tlet mut paths = vec![DEFAULT_HOOKS_PATH.to_string()];\n\t\tif let Some(others) = other_paths {\n\t\t\tpaths.extend(\n\t\t\t\tothers\n\t\t\t\t\t.iter()\n\t\t\t\t\t.map(|p| p.trim_end_matches('/').to_string()),\n\t\t\t);\n\t\t}\n\n\t\tfor p in paths {\n\t\t\tlet p = repo.path().to_path_buf().join(p).join(hook);\n\t\t\tif p.exists() {\n\t\t\t\treturn p;\n\t\t\t}\n\t\t}\n\n\t\trepo.path()\n\t\t\t.to_path_buf()\n\t\t\t.join(DEFAULT_HOOKS_PATH)\n\t\t\t.join(hook)\n\t}\n\n\t/// was a hook file found and is it executable\n\tpub fn found(&self) -> bool {\n\t\tself.hook.exists() && is_executable(&self.hook)\n\t}\n\n\t/// this function calls hook scripts based on conventions documented here\n\t/// see <https://git-scm.com/docs/githooks>\n\tpub fn run_hook(&self, args: &[&str]) -> Result<HookResult> {\n\t\tself.run_hook_os_str(args)\n\t}\n\n\t/// this function calls hook scripts based on conventions documented here\n\t/// see <https://git-scm.com/docs/githooks>\n\tpub fn run_hook_os_str<I, S>(&self, args: I) -> Result<HookResult>\n\twhere\n\t\tI: IntoIterator<Item = S> + Copy,\n\t\tS: AsRef<OsStr>,\n\t{\n\t\tself.run_hook_os_str_with_stdin(args, None)\n\t}\n\n\t/// this function calls hook scripts with stdin input based on conventions documented here\n\t/// see <https://git-scm.com/docs/githooks>\n\tpub fn run_hook_os_str_with_stdin<I, S>(\n\t\t&self,\n\t\targs: I,\n\t\tstdin: Option<&[u8]>,\n\t) -> Result<HookResult>\n\twhere\n\t\tI: IntoIterator<Item = S> + Copy,\n\t\tS: AsRef<OsStr>,\n\t{\n\t\tlet hook = self.hook.clone();\n\t\tlog::trace!(\n\t\t\t\"run hook '{}' in '{}'\",\n\t\t\thook.display(),\n\t\t\tself.pwd.display()\n\t\t);\n\n\t\tlet run_command = |command: &mut Command| {\n\t\t\tlet mut child = command\n\t\t\t\t.args(args)\n\t\t\t\t.current_dir(&self.pwd)\n\t\t\t\t.with_no_window()\n\t\t\t\t.stdin(if stdin.is_some() {\n\t\t\t\t\tstd::process::Stdio::piped()\n\t\t\t\t} else {\n\t\t\t\t\tstd::process::Stdio::null()\n\t\t\t\t})\n\t\t\t\t.stdout(std::process::Stdio::piped())\n\t\t\t\t.stderr(std::process::Stdio::piped())\n\t\t\t\t.spawn()?;\n\n\t\t\tif let (Some(mut stdin_handle), Some(input)) =\n\t\t\t\t(child.stdin.take(), stdin)\n\t\t\t{\n\t\t\t\tuse std::io::{ErrorKind, Write};\n\n\t\t\t\t// Write stdin to hook process\n\t\t\t\t// Ignore broken pipe - hook may exit early without reading all input\n\t\t\t\tlet _ =\n\t\t\t\t\tstdin_handle.write_all(input).inspect_err(|e| {\n\t\t\t\t\t\tmatch e.kind() {\n\t\t\t\t\t\t\tErrorKind::BrokenPipe => {\n\t\t\t\t\t\t\t\tlog::debug!(\n\t\t\t\t\t\t\t\t\t\"Hook closed stdin early\"\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t_ => log::warn!(\n\t\t\t\t\t\t\t\t\"Failed to write stdin to hook: {e}\"\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t}\n\n\t\t\tchild.wait_with_output()\n\t\t};\n\n\t\tlet output = if cfg!(windows) {\n\t\t\t// execute hook in shell\n\t\t\tlet command = {\n\t\t\t\t// SEE: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_02_02\n\t\t\t\t// Enclosing characters in single-quotes ( '' ) shall preserve the literal value of each character within the single-quotes.\n\t\t\t\t// A single-quote cannot occur within single-quotes.\n\t\t\t\tconst REPLACEMENT: &str = concat!(\n\t\t\t\t\t\"'\",   // closing single-quote\n\t\t\t\t\t\"\\\\'\", // one escaped single-quote (outside of single-quotes)\n\t\t\t\t\t\"'\",   // new single-quote\n\t\t\t\t);\n\n\t\t\t\tlet mut os_str = OsString::new();\n\t\t\t\tos_str.push(\"'\");\n\t\t\t\tif let Some(hook) = hook.to_str() {\n\t\t\t\t\tos_str.push(hook.replace('\\'', REPLACEMENT));\n\t\t\t\t} else {\n\t\t\t\t\t#[cfg(windows)]\n\t\t\t\t\t{\n\t\t\t\t\t\tuse std::os::windows::ffi::OsStrExt;\n\t\t\t\t\t\tif hook\n\t\t\t\t\t\t\t.as_os_str()\n\t\t\t\t\t\t\t.encode_wide()\n\t\t\t\t\t\t\t.any(|x| x == u16::from(b'\\''))\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t// TODO: escape single quotes instead of failing\n\t\t\t\t\t\t\treturn Err(HooksError::PathToString);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tos_str.push(hook.as_os_str());\n\t\t\t\t}\n\t\t\t\tos_str.push(\"'\");\n\t\t\t\tos_str.push(\" \\\"$@\\\"\");\n\n\t\t\t\tos_str\n\t\t\t};\n\t\t\trun_command(\n\t\t\t\tsh_command().arg(\"-c\").arg(command).arg(&hook),\n\t\t\t)\n\t\t} else {\n\t\t\t// execute hook directly\n\t\t\tmatch run_command(&mut Command::new(&hook)) {\n\t\t\t\tErr(err) if err.raw_os_error() == Some(ENOEXEC) => {\n\t\t\t\t\trun_command(sh_command().arg(&hook))\n\t\t\t\t}\n\t\t\t\tresult => result,\n\t\t\t}\n\t\t}?;\n\n\t\tlet stderr =\n\t\t\tString::from_utf8_lossy(&output.stderr).to_string();\n\t\tlet stdout =\n\t\t\tString::from_utf8_lossy(&output.stdout).to_string();\n\n\t\t// Get exit code, or fail if process was killed by signal\n\t\tlet code =\n\t\t\toutput.status.code().ok_or(HooksError::NoExitCode)?;\n\n\t\tOk(HookResult::Run(crate::HookRunResponse {\n\t\t\thook,\n\t\t\tstdout,\n\t\t\tstderr,\n\t\t\tcode,\n\t\t}))\n\t}\n}\n\nfn sh_command() -> Command {\n\tlet mut command = Command::new(gix_path::env::shell());\n\n\tif cfg!(windows) {\n\t\t// This call forces Command to handle the Path environment correctly on windows,\n\t\t// the specific env set here does not matter\n\t\t// see https://github.com/rust-lang/rust/issues/37519\n\t\tcommand.env(\n\t\t\t\"DUMMY_ENV_TO_FIX_WINDOWS_CMD_RUNS\",\n\t\t\t\"FixPathHandlingOnWindows\",\n\t\t);\n\n\t\t// Use -l to avoid \"command not found\"\n\t\tcommand.arg(\"-l\");\n\t}\n\n\tcommand\n}\n\n#[cfg(unix)]\nfn is_executable(path: &Path) -> bool {\n\tuse std::os::unix::fs::PermissionsExt;\n\n\tlet metadata = match path.metadata() {\n\t\tOk(metadata) => metadata,\n\t\tErr(e) => {\n\t\t\tlog::error!(\"metadata error: {e}\");\n\t\t\treturn false;\n\t\t}\n\t};\n\n\tlet permissions = metadata.permissions();\n\n\tpermissions.mode() & 0o111 != 0\n}\n\n#[cfg(windows)]\n/// windows does not consider shell scripts to be executable so we consider everything\n/// to be executable (which is not far from the truth for windows platform.)\nconst fn is_executable(_: &Path) -> bool {\n\ttrue\n}\n\ntrait CommandExt {\n\t/// The process is a console application that is being run without a\n\t/// console window. Therefore, the console handle for the application is\n\t/// not set.\n\t///\n\t/// This flag is ignored if the application is not a console application,\n\t/// or if it used with either `CREATE_NEW_CONSOLE` or `DETACHED_PROCESS`.\n\t///\n\t/// See: <https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags>\n\t#[cfg(windows)]\n\tconst CREATE_NO_WINDOW: u32 = 0x0800_0000;\n\n\tfn with_no_window(&mut self) -> &mut Self;\n}\n\nimpl CommandExt for Command {\n\t/// On Windows, CLI applications that aren't the window's subsystem will\n\t/// create and show a console window that pops up next to the main\n\t/// application window when run. We disable this behavior by setting the\n\t/// `CREATE_NO_WINDOW` flag.\n\t#[inline]\n\tfn with_no_window(&mut self) -> &mut Self {\n\t\t#[cfg(windows)]\n\t\t{\n\t\t\tuse std::os::windows::process::CommandExt;\n\t\t\tself.creation_flags(Self::CREATE_NO_WINDOW);\n\t\t}\n\n\t\tself\n\t}\n}\n\n#[cfg(test)]\nmod test {\n\tuse super::HookPaths;\n\tuse std::path::Path;\n\n\t#[test]\n\tfn test_hookspath_relative() {\n\t\tassert_eq!(\n\t\t\tHookPaths::expand_path(\n\t\t\t\tPath::new(\"pre-commit\"),\n\t\t\t\tPath::new(\"example_git_root\"),\n\t\t\t)\n\t\t\t.unwrap(),\n\t\t\tPath::new(\"example_git_root\").join(\"pre-commit\")\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_hookspath_absolute() {\n\t\tlet absolute_hook =\n\t\t\tstd::env::current_dir().unwrap().join(\"pre-commit\");\n\t\tassert_eq!(\n\t\t\tHookPaths::expand_path(\n\t\t\t\t&absolute_hook,\n\t\t\t\tPath::new(\"example_git_root\"),\n\t\t\t)\n\t\t\t.unwrap(),\n\t\t\tabsolute_hook\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "git2-hooks/src/lib.rs",
    "content": "//! git2-rs addon supporting git hooks\n//!\n//! we look for hooks in the following locations:\n//!  * whatever `config.hooksPath` points to\n//!  * `.git/hooks/`\n//!  * whatever list of paths provided as `other_paths` (in order)\n//!\n//! most basic hook is: [`hooks_pre_commit`]. see also other `hooks_*` functions.\n//!\n//! [`create_hook`] is useful to create git hooks from code (unittest make heavy usage of it)\n\n#![forbid(unsafe_code)]\n#![deny(\n\tmismatched_lifetime_syntaxes,\n\tunused_imports,\n\tunused_must_use,\n\tdead_code,\n\tunstable_name_collisions,\n\tunused_assignments\n)]\n#![deny(clippy::all, clippy::perf, clippy::pedantic, clippy::nursery)]\n#![allow(\n\tclippy::missing_errors_doc,\n\tclippy::must_use_candidate,\n\tclippy::module_name_repetitions\n)]\n\nmod error;\nmod hookspath;\n\nuse std::{\n\tfs::File,\n\tio::{Read, Write},\n\tpath::{Path, PathBuf},\n};\n\npub use error::HooksError;\nuse error::Result;\nuse hookspath::HookPaths;\n\nuse git2::{Oid, Repository};\n\npub const HOOK_POST_COMMIT: &str = \"post-commit\";\npub const HOOK_PRE_COMMIT: &str = \"pre-commit\";\npub const HOOK_COMMIT_MSG: &str = \"commit-msg\";\npub const HOOK_PREPARE_COMMIT_MSG: &str = \"prepare-commit-msg\";\npub const HOOK_PRE_PUSH: &str = \"pre-push\";\n\nconst HOOK_COMMIT_MSG_TEMP_FILE: &str = \"COMMIT_EDITMSG\";\n\n/// Check if a given hook is present considering config/paths and optional extra paths.\npub fn hook_available(\n\trepo: &Repository,\n\tother_paths: Option<&[&str]>,\n\thook: &str,\n) -> Result<bool> {\n\tlet hook = HookPaths::new(repo, other_paths, hook)?;\n\tOk(hook.found())\n}\n\n#[derive(Clone, Debug, PartialEq, Eq)]\npub struct PrePushRef {\n\tpub local_ref: String,\n\tpub local_oid: Option<Oid>,\n\tpub remote_ref: String,\n\tpub remote_oid: Option<Oid>,\n}\n\nimpl PrePushRef {\n\tpub fn new(\n\t\tlocal_ref: impl Into<String>,\n\t\tlocal_oid: Option<Oid>,\n\t\tremote_ref: impl Into<String>,\n\t\tremote_oid: Option<Oid>,\n\t) -> Self {\n\t\tSelf {\n\t\t\tlocal_ref: local_ref.into(),\n\t\t\tlocal_oid,\n\t\t\tremote_ref: remote_ref.into(),\n\t\t\tremote_oid,\n\t\t}\n\t}\n\n\tfn format_oid(oid: Option<Oid>) -> String {\n\t\t// \"If the foreign ref does not yet exist the <remote-object-name> will be the all-zeroes object name\"\n\t\t// see https://git-scm.com/docs/githooks#_pre_push\n\t\toid.map_or_else(|| \"0\".repeat(40), |id| id.to_string())\n\t}\n\n\tpub fn to_line(&self) -> String {\n\t\tformat!(\n\t\t\t\"{} {} {} {}\",\n\t\t\tself.local_ref,\n\t\t\tSelf::format_oid(self.local_oid),\n\t\t\tself.remote_ref,\n\t\t\tSelf::format_oid(self.remote_oid)\n\t\t)\n\t}\n\n\t/// Build stdin content from a slice of updates (for pre-push hook)\n\tpub fn to_stdin(updates: &[Self]) -> String {\n\t\tlet mut stdin = String::new();\n\t\tfor update in updates {\n\t\t\tstdin.push_str(&update.to_line());\n\t\t\tstdin.push('\\n');\n\t\t}\n\t\tstdin\n\t}\n}\n\n/// Response from running a hook\n#[derive(Debug, PartialEq, Eq)]\npub struct HookRunResponse {\n\t/// path of the hook that was run\n\tpub hook: PathBuf,\n\t/// stdout output emitted by hook\n\tpub stdout: String,\n\t/// stderr output emitted by hook\n\tpub stderr: String,\n\t/// exit code as reported back from process calling the hook (0 = success)\n\tpub code: i32,\n}\n\n#[derive(Debug, PartialEq, Eq)]\npub enum HookResult {\n\t/// No hook found\n\tNoHookFound,\n\t/// Hook executed (check `HookRunResponse.code` for success/failure)\n\tRun(HookRunResponse),\n}\n\nimpl HookResult {\n\t/// helper to check if hook ran successfully (found and exit code 0)\n\tpub const fn is_successful(&self) -> bool {\n\t\tmatches!(self, Self::Run(response) if response.is_successful())\n\t}\n}\n\nimpl HookRunResponse {\n\t/// Check if the hook succeeded (exit code 0)\n\tpub const fn is_successful(&self) -> bool {\n\t\tself.code == 0\n\t}\n}\n\n/// helper method to create git hooks programmatically (heavy used in unittests)\n///\n/// # Panics\n/// Panics if hook could not be created\npub fn create_hook(\n\tr: &Repository,\n\thook: &str,\n\thook_script: &[u8],\n) -> PathBuf {\n\tlet hook = HookPaths::new(r, None, hook).unwrap();\n\n\tlet path = hook.hook.clone();\n\n\tcreate_hook_in_path(&hook.hook, hook_script);\n\n\tpath\n}\n\nfn create_hook_in_path(path: &Path, hook_script: &[u8]) {\n\tFile::create(path).unwrap().write_all(hook_script).unwrap();\n\n\t#[cfg(unix)]\n\t{\n\t\tstd::process::Command::new(\"chmod\")\n\t\t\t.arg(\"+x\")\n\t\t\t.arg(path)\n\t\t\t// .current_dir(path)\n\t\t\t.output()\n\t\t\t.unwrap();\n\t}\n}\n\n/// Git hook: `commit_msg`\n///\n/// This hook is documented here <https://git-scm.com/docs/githooks#_commit_msg>.\n/// We use the same convention as other git clients to create a temp file containing\n/// the commit message at `<.git|hooksPath>/COMMIT_EDITMSG` and pass it's relative path as the only\n/// parameter to the hook script.\npub fn hooks_commit_msg(\n\trepo: &Repository,\n\tother_paths: Option<&[&str]>,\n\tmsg: &mut String,\n) -> Result<HookResult> {\n\tlet hook = HookPaths::new(repo, other_paths, HOOK_COMMIT_MSG)?;\n\n\tif !hook.found() {\n\t\treturn Ok(HookResult::NoHookFound);\n\t}\n\n\tlet temp_file = hook.git.join(HOOK_COMMIT_MSG_TEMP_FILE);\n\tFile::create(&temp_file)?.write_all(msg.as_bytes())?;\n\n\tlet res = hook.run_hook_os_str([&temp_file])?;\n\n\t// load possibly altered msg\n\tmsg.clear();\n\tFile::open(temp_file)?.read_to_string(msg)?;\n\n\tOk(res)\n}\n\n/// this hook is documented here <https://git-scm.com/docs/githooks#_pre_commit>\npub fn hooks_pre_commit(\n\trepo: &Repository,\n\tother_paths: Option<&[&str]>,\n) -> Result<HookResult> {\n\tlet hook = HookPaths::new(repo, other_paths, HOOK_PRE_COMMIT)?;\n\n\tif !hook.found() {\n\t\treturn Ok(HookResult::NoHookFound);\n\t}\n\n\thook.run_hook(&[])\n}\n\n/// this hook is documented here <https://git-scm.com/docs/githooks#_post_commit>\npub fn hooks_post_commit(\n\trepo: &Repository,\n\tother_paths: Option<&[&str]>,\n) -> Result<HookResult> {\n\tlet hook = HookPaths::new(repo, other_paths, HOOK_POST_COMMIT)?;\n\n\tif !hook.found() {\n\t\treturn Ok(HookResult::NoHookFound);\n\t}\n\n\thook.run_hook(&[])\n}\n\n/// this hook is documented here <https://git-scm.com/docs/githooks#_pre_push>\n///\n/// According to git documentation, pre-push hook receives:\n/// - remote name as first argument (or URL if remote is not named)\n/// - remote URL as second argument\n/// - information about refs being pushed via stdin in format:\n///   `<local-ref> SP <local-object-name> SP <remote-ref> SP <remote-object-name> LF`\n///\n/// If `remote` is `None` or empty, the `url` is used for both arguments as per Git spec.\n///\n/// Note: The hook is called even when `updates` is empty (matching Git's behavior).\n/// This can occur when pushing tags that already exist on the remote.\npub fn hooks_pre_push(\n\trepo: &Repository,\n\tother_paths: Option<&[&str]>,\n\tremote: Option<&str>,\n\turl: &str,\n\tupdates: &[PrePushRef],\n) -> Result<HookResult> {\n\tlet hook = HookPaths::new(repo, other_paths, HOOK_PRE_PUSH)?;\n\n\tif !hook.found() {\n\t\treturn Ok(HookResult::NoHookFound);\n\t}\n\n\t// If a remote is not named (None or empty), the URL is passed for both arguments\n\tlet remote_name = match remote {\n\t\tSome(r) if !r.is_empty() => r,\n\t\t_ => url,\n\t};\n\n\tlet stdin_data = PrePushRef::to_stdin(updates);\n\n\thook.run_hook_os_str_with_stdin(\n\t\t[remote_name, url],\n\t\tSome(stdin_data.as_bytes()),\n\t)\n}\n\npub enum PrepareCommitMsgSource {\n\tMessage,\n\tTemplate,\n\tMerge,\n\tSquash,\n\tCommit(git2::Oid),\n}\n\n/// this hook is documented here <https://git-scm.com/docs/githooks#_prepare_commit_msg>\n#[allow(clippy::needless_pass_by_value)]\npub fn hooks_prepare_commit_msg(\n\trepo: &Repository,\n\tother_paths: Option<&[&str]>,\n\tsource: PrepareCommitMsgSource,\n\tmsg: &mut String,\n) -> Result<HookResult> {\n\tlet hook =\n\t\tHookPaths::new(repo, other_paths, HOOK_PREPARE_COMMIT_MSG)?;\n\n\tif !hook.found() {\n\t\treturn Ok(HookResult::NoHookFound);\n\t}\n\n\tlet temp_file = hook.git.join(HOOK_COMMIT_MSG_TEMP_FILE);\n\tFile::create(&temp_file)?.write_all(msg.as_bytes())?;\n\n\tlet temp_file_path = temp_file.as_os_str().to_string_lossy();\n\n\tlet vec = vec![\n\t\ttemp_file_path.as_ref(),\n\t\tmatch source {\n\t\t\tPrepareCommitMsgSource::Message => \"message\",\n\t\t\tPrepareCommitMsgSource::Template => \"template\",\n\t\t\tPrepareCommitMsgSource::Merge => \"merge\",\n\t\t\tPrepareCommitMsgSource::Squash => \"squash\",\n\t\t\tPrepareCommitMsgSource::Commit(_) => \"commit\",\n\t\t},\n\t];\n\tlet mut args = vec;\n\n\tlet id = if let PrepareCommitMsgSource::Commit(id) = &source {\n\t\tSome(id.to_string())\n\t} else {\n\t\tNone\n\t};\n\n\tif let Some(id) = &id {\n\t\targs.push(id);\n\t}\n\n\tlet res = hook.run_hook(args.as_slice())?;\n\n\t// load possibly altered msg\n\tmsg.clear();\n\tFile::open(temp_file)?.read_to_string(msg)?;\n\n\tOk(res)\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse git2_testing::{repo_init, repo_init_bare};\n\tuse pretty_assertions::assert_eq;\n\tuse tempfile::TempDir;\n\n\tfn branch_update(\n\t\trepo: &Repository,\n\t\tremote: Option<&str>,\n\t\tbranch: &str,\n\t\tremote_branch: Option<&str>,\n\t\tdelete: bool,\n\t) -> PrePushRef {\n\t\tlet local_ref = format!(\"refs/heads/{branch}\");\n\t\tlet local_oid = (!delete).then(|| {\n\t\t\trepo.find_branch(branch, git2::BranchType::Local)\n\t\t\t\t.unwrap()\n\t\t\t\t.get()\n\t\t\t\t.peel_to_commit()\n\t\t\t\t.unwrap()\n\t\t\t\t.id()\n\t\t});\n\n\t\tlet remote_branch = remote_branch.unwrap_or(branch);\n\t\tlet remote_ref = format!(\"refs/heads/{remote_branch}\");\n\t\tlet remote_oid = remote.and_then(|remote_name| {\n\t\t\trepo.find_reference(&format!(\n\t\t\t\t\"refs/remotes/{remote_name}/{remote_branch}\"\n\t\t\t))\n\t\t\t.ok()\n\t\t\t.and_then(|r| r.peel_to_commit().ok())\n\t\t\t.map(|c| c.id())\n\t\t});\n\n\t\tPrePushRef::new(local_ref, local_oid, remote_ref, remote_oid)\n\t}\n\n\tfn head_branch(repo: &Repository) -> String {\n\t\trepo.head().unwrap().shorthand().unwrap().to_string()\n\t}\n\n\t#[test]\n\tfn test_pre_push_ref_format() {\n\t\tlet zero_oid = \"0\".repeat(40);\n\t\tlet oid_a = \"a\".repeat(40);\n\t\tlet oid_b = \"b\".repeat(40);\n\n\t\t// Both oids present\n\t\tlet update = PrePushRef::new(\n\t\t\t\"refs/heads/main\",\n\t\t\tSome(git2::Oid::from_str(&oid_a).unwrap()),\n\t\t\t\"refs/heads/main\",\n\t\t\tSome(git2::Oid::from_str(&oid_b).unwrap()),\n\t\t);\n\t\tassert_eq!(\n\t\t\tupdate.to_line(),\n\t\t\tformat!(\n\t\t\t\t\"refs/heads/main {oid_a} refs/heads/main {oid_b}\"\n\t\t\t)\n\t\t);\n\n\t\t// No remote oid (new branch)\n\t\tlet update = PrePushRef::new(\n\t\t\t\"refs/heads/feature\",\n\t\t\tSome(git2::Oid::from_str(&oid_a).unwrap()),\n\t\t\t\"refs/heads/feature\",\n\t\t\tNone,\n\t\t);\n\t\tassert_eq!(\n\t\t\tupdate.to_line(),\n\t\t\tformat!(\"refs/heads/feature {oid_a} refs/heads/feature {zero_oid}\")\n\t\t);\n\n\t\t// No local oid (delete)\n\t\tlet update = PrePushRef::new(\n\t\t\t\"refs/heads/old\",\n\t\t\tNone,\n\t\t\t\"refs/heads/old\",\n\t\t\tSome(git2::Oid::from_str(&oid_b).unwrap()),\n\t\t);\n\t\tassert_eq!(\n\t\t\tupdate.to_line(),\n\t\t\tformat!(\n\t\t\t\t\"refs/heads/old {zero_oid} refs/heads/old {oid_b}\"\n\t\t\t)\n\t\t);\n\n\t\t// to_stdin adds newlines\n\t\tlet updates = [\n\t\t\tPrePushRef::new(\n\t\t\t\t\"refs/heads/a\",\n\t\t\t\tSome(git2::Oid::from_str(&oid_a).unwrap()),\n\t\t\t\t\"refs/heads/a\",\n\t\t\t\tNone,\n\t\t\t),\n\t\t\tPrePushRef::new(\n\t\t\t\t\"refs/heads/b\",\n\t\t\t\tSome(git2::Oid::from_str(&oid_b).unwrap()),\n\t\t\t\t\"refs/heads/b\",\n\t\t\t\tNone,\n\t\t\t),\n\t\t];\n\t\tassert_eq!(\n\t\t\tPrePushRef::to_stdin(&updates),\n\t\t\tformat!(\n\t\t\t\t\"refs/heads/a {oid_a} refs/heads/a {zero_oid}\\nrefs/heads/b {oid_b} refs/heads/b {zero_oid}\\n\"\n\t\t\t)\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_smoke() {\n\t\tlet (_td, repo) = repo_init();\n\n\t\tlet mut msg = String::from(\"test\");\n\t\tlet res = hooks_commit_msg(&repo, None, &mut msg).unwrap();\n\n\t\tassert_eq!(res, HookResult::NoHookFound);\n\n\t\tlet hook = b\"#!/bin/sh\nexit 0\n        \";\n\n\t\tcreate_hook(&repo, HOOK_POST_COMMIT, hook);\n\n\t\tlet res = hooks_post_commit(&repo, None).unwrap();\n\n\t\tassert!(res.is_successful());\n\t}\n\n\t#[test]\n\tfn test_hooks_commit_msg_ok() {\n\t\tlet (_td, repo) = repo_init();\n\n\t\tlet hook = b\"#!/bin/sh\nexit 0\n        \";\n\n\t\tcreate_hook(&repo, HOOK_COMMIT_MSG, hook);\n\n\t\tlet mut msg = String::from(\"test\");\n\t\tlet res = hooks_commit_msg(&repo, None, &mut msg).unwrap();\n\n\t\tassert!(res.is_successful());\n\n\t\tassert_eq!(msg, String::from(\"test\"));\n\t}\n\n\t#[test]\n\tfn test_hooks_commit_msg_with_shell_command_ok() {\n\t\tlet (_td, repo) = repo_init();\n\n\t\tlet hook = br#\"#!/bin/sh\nCOMMIT_MSG=\"$(cat \"$1\")\"\nprintf \"$COMMIT_MSG\" | sed 's/sth/shell_command/g' > \"$1\"\nexit 0\n        \"#;\n\n\t\tcreate_hook(&repo, HOOK_COMMIT_MSG, hook);\n\n\t\tlet mut msg = String::from(\"test_sth\");\n\t\tlet res = hooks_commit_msg(&repo, None, &mut msg).unwrap();\n\n\t\tassert!(res.is_successful());\n\n\t\tassert_eq!(msg, String::from(\"test_shell_command\"));\n\t}\n\n\t#[test]\n\tfn test_pre_commit_sh() {\n\t\tlet (_td, repo) = repo_init();\n\n\t\tlet hook = b\"#!/bin/sh\nexit 0\n        \";\n\n\t\tcreate_hook(&repo, HOOK_PRE_COMMIT, hook);\n\t\tlet res = hooks_pre_commit(&repo, None).unwrap();\n\t\tassert!(res.is_successful());\n\t}\n\n\t#[test]\n\tfn test_hook_with_missing_shebang() {\n\t\tconst TEXT: &str = \"Hello, world!\";\n\n\t\tlet (_td, repo) = repo_init();\n\n\t\tlet hook = b\"echo \\\"$@\\\"\\nexit 42\";\n\n\t\tcreate_hook(&repo, HOOK_PRE_COMMIT, hook);\n\n\t\tlet hook =\n\t\t\tHookPaths::new(&repo, None, HOOK_PRE_COMMIT).unwrap();\n\n\t\tassert!(hook.found());\n\n\t\tlet result = hook.run_hook(&[TEXT]).unwrap();\n\n\t\tlet HookResult::Run(response) = result else {\n\t\t\tunreachable!(\"run_hook should've run\");\n\t\t};\n\n\t\tlet stdout = response.stdout.as_str().trim_ascii_end();\n\n\t\tassert_eq!(response.code, 42);\n\t\tassert_eq!(response.hook, hook.hook);\n\t\tassert_eq!(stdout, TEXT, \"{:?} != {TEXT:?}\", stdout);\n\t\tassert!(response.stderr.is_empty());\n\t}\n\n\t#[test]\n\tfn test_no_hook_found() {\n\t\tlet (_td, repo) = repo_init();\n\n\t\tlet res = hooks_pre_commit(&repo, None).unwrap();\n\t\tassert_eq!(res, HookResult::NoHookFound);\n\t}\n\n\t#[test]\n\tfn test_other_path() {\n\t\tlet (td, repo) = repo_init();\n\n\t\tlet hook = b\"#!/bin/sh\nexit 0\n        \";\n\n\t\tlet custom_hooks_path = td.path().join(\".myhooks\");\n\n\t\tstd::fs::create_dir(dbg!(&custom_hooks_path)).unwrap();\n\t\tcreate_hook_in_path(\n\t\t\tdbg!(custom_hooks_path.join(HOOK_PRE_COMMIT).as_path()),\n\t\t\thook,\n\t\t);\n\n\t\tlet res =\n\t\t\thooks_pre_commit(&repo, Some(&[\"../.myhooks\"])).unwrap();\n\n\t\tassert!(res.is_successful());\n\t}\n\n\t#[test]\n\tfn test_other_path_precedence() {\n\t\tlet (td, repo) = repo_init();\n\n\t\t{\n\t\t\tlet hook = b\"#!/bin/sh\nexit 0\n        \";\n\n\t\t\tcreate_hook(&repo, HOOK_PRE_COMMIT, hook);\n\t\t}\n\n\t\t{\n\t\t\tlet reject_hook = b\"#!/bin/sh\nexit 1\n        \";\n\n\t\t\tlet custom_hooks_path = td.path().join(\".myhooks\");\n\t\t\tstd::fs::create_dir(dbg!(&custom_hooks_path)).unwrap();\n\t\t\tcreate_hook_in_path(\n\t\t\t\tdbg!(custom_hooks_path\n\t\t\t\t\t.join(HOOK_PRE_COMMIT)\n\t\t\t\t\t.as_path()),\n\t\t\t\treject_hook,\n\t\t\t);\n\t\t}\n\n\t\tlet res =\n\t\t\thooks_pre_commit(&repo, Some(&[\"../.myhooks\"])).unwrap();\n\n\t\tassert!(res.is_successful());\n\t}\n\n\t#[test]\n\tfn test_pre_commit_fail_sh() {\n\t\tlet (_td, repo) = repo_init();\n\n\t\tlet hook = b\"#!/bin/sh\necho 'rejected'\nexit 1\n        \";\n\n\t\tcreate_hook(&repo, HOOK_PRE_COMMIT, hook);\n\t\tlet res = hooks_pre_commit(&repo, None).unwrap();\n\t\tassert!(!res.is_successful());\n\t}\n\n\t#[test]\n\tfn test_env_containing_path() {\n\t\tconst PATH_EXPORT: &str = \"export PATH\";\n\n\t\tlet (_td, repo) = repo_init();\n\n\t\tlet hook = b\"#!/bin/sh\nexport\nexit 1\n        \";\n\n\t\tcreate_hook(&repo, HOOK_PRE_COMMIT, hook);\n\t\tlet res = hooks_pre_commit(&repo, None).unwrap();\n\n\t\tlet HookResult::Run(response) = res else {\n\t\t\tunreachable!()\n\t\t};\n\n\t\tassert!(\n\t\t\tresponse\n\t\t\t\t.stdout\n\t\t\t\t.lines()\n\t\t\t\t.any(|line| line.starts_with(PATH_EXPORT)),\n\t\t\t\"Could not find line starting with {PATH_EXPORT:?} in: {:?}\",\n\t\t\tresponse.stdout\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_pre_commit_fail_hookspath() {\n\t\tlet (_td, repo) = repo_init();\n\t\tlet hooks = TempDir::new().unwrap();\n\n\t\tlet hook = b\"#!/bin/sh\necho 'rejected'\nexit 1\n        \";\n\n\t\tcreate_hook_in_path(&hooks.path().join(\"pre-commit\"), hook);\n\n\t\trepo.config()\n\t\t\t.unwrap()\n\t\t\t.set_str(\n\t\t\t\t\"core.hooksPath\",\n\t\t\t\thooks.path().as_os_str().to_str().unwrap(),\n\t\t\t)\n\t\t\t.unwrap();\n\n\t\tlet res = hooks_pre_commit(&repo, None).unwrap();\n\n\t\tlet HookResult::Run(response) = res else {\n\t\t\tunreachable!()\n\t\t};\n\n\t\tassert_eq!(response.code, 1);\n\t\tassert_eq!(&response.stdout, \"rejected\\n\");\n\t}\n\n\t#[test]\n\tfn test_pre_commit_fail_bare() {\n\t\tlet (_td, repo) = repo_init_bare();\n\n\t\tlet hook = b\"#!/bin/sh\necho 'rejected'\nexit 1\n        \";\n\n\t\tcreate_hook(&repo, HOOK_PRE_COMMIT, hook);\n\t\tlet res = hooks_pre_commit(&repo, None).unwrap();\n\t\tassert!(!res.is_successful());\n\t}\n\n\t#[test]\n\tfn test_pre_commit_py() {\n\t\tlet (_td, repo) = repo_init();\n\n\t\t// mirror how python pre-commit sets itself up\n\t\t#[cfg(not(windows))]\n\t\tlet hook = b\"#!/usr/bin/env python\nimport sys\nsys.exit(0)\n        \";\n\t\t#[cfg(windows)]\n\t\tlet hook = b\"#!/bin/env python.exe\nimport sys\nsys.exit(0)\n        \";\n\n\t\tcreate_hook(&repo, HOOK_PRE_COMMIT, hook);\n\t\tlet res = hooks_pre_commit(&repo, None).unwrap();\n\t\tassert!(res.is_successful(), \"{res:?}\");\n\t}\n\n\t#[test]\n\tfn test_pre_commit_fail_py() {\n\t\tlet (_td, repo) = repo_init();\n\n\t\t// mirror how python pre-commit sets itself up\n\t\t#[cfg(not(windows))]\n\t\tlet hook = b\"#!/usr/bin/env python\nimport sys\nsys.exit(1)\n        \";\n\t\t#[cfg(windows)]\n\t\tlet hook = b\"#!/bin/env python.exe\nimport sys\nsys.exit(1)\n        \";\n\n\t\tcreate_hook(&repo, HOOK_PRE_COMMIT, hook);\n\t\tlet res = hooks_pre_commit(&repo, None).unwrap();\n\t\tassert!(!res.is_successful());\n\t}\n\n\t#[test]\n\tfn test_hooks_commit_msg_reject() {\n\t\tlet (_td, repo) = repo_init();\n\n\t\tlet hook = b\"#!/bin/sh\n\techo 'msg' > \\\"$1\\\"\n\techo 'rejected'\n\texit 1\n        \";\n\n\t\tcreate_hook(&repo, HOOK_COMMIT_MSG, hook);\n\n\t\tlet mut msg = String::from(\"test\");\n\t\tlet res = hooks_commit_msg(&repo, None, &mut msg).unwrap();\n\n\t\tlet HookResult::Run(response) = res else {\n\t\t\tunreachable!()\n\t\t};\n\n\t\tassert_eq!(response.code, 1);\n\t\tassert_eq!(&response.stdout, \"rejected\\n\");\n\n\t\tassert_eq!(msg, String::from(\"msg\\n\"));\n\t}\n\n\t#[test]\n\tfn test_commit_msg_no_block_but_alter() {\n\t\tlet (_td, repo) = repo_init();\n\n\t\tlet hook = b\"#!/bin/sh\necho 'msg' > \\\"$1\\\"\nexit 0\n        \";\n\n\t\tcreate_hook(&repo, HOOK_COMMIT_MSG, hook);\n\n\t\tlet mut msg = String::from(\"test\");\n\t\tlet res = hooks_commit_msg(&repo, None, &mut msg).unwrap();\n\n\t\tassert!(res.is_successful());\n\t\tassert_eq!(msg, String::from(\"msg\\n\"));\n\t}\n\n\t#[test]\n\tfn test_hook_pwd_in_bare_without_workdir() {\n\t\tlet (_td, repo) = repo_init_bare();\n\t\tlet git_root = repo.path().to_path_buf();\n\n\t\tlet hook =\n\t\t\tHookPaths::new(&repo, None, HOOK_POST_COMMIT).unwrap();\n\n\t\tassert_eq!(hook.pwd, git_root);\n\t}\n\n\t#[test]\n\tfn test_hook_pwd() {\n\t\tlet (_td, repo) = repo_init();\n\t\tlet git_root = repo.path().to_path_buf();\n\n\t\tlet hook =\n\t\t\tHookPaths::new(&repo, None, HOOK_POST_COMMIT).unwrap();\n\n\t\tassert_eq!(hook.pwd, git_root.parent().unwrap());\n\t}\n\n\t#[test]\n\tfn test_hooks_prep_commit_msg_success() {\n\t\tlet (_td, repo) = repo_init();\n\n\t\tlet hook = b\"#!/bin/sh\necho \\\"msg:$2\\\" > \\\"$1\\\"\nexit 0\n        \";\n\n\t\tcreate_hook(&repo, HOOK_PREPARE_COMMIT_MSG, hook);\n\n\t\tlet mut msg = String::from(\"test\");\n\t\tlet res = hooks_prepare_commit_msg(\n\t\t\t&repo,\n\t\t\tNone,\n\t\t\tPrepareCommitMsgSource::Message,\n\t\t\t&mut msg,\n\t\t)\n\t\t.unwrap();\n\n\t\tassert!(res.is_successful());\n\t\tassert_eq!(msg, String::from(\"msg:message\\n\"));\n\t}\n\n\t#[test]\n\tfn test_hooks_prep_commit_msg_reject() {\n\t\tlet (_td, repo) = repo_init();\n\n\t\tlet hook = b\"#!/bin/sh\necho \\\"$2,$3\\\" > \\\"$1\\\"\necho 'rejected'\nexit 2\n        \";\n\n\t\tcreate_hook(&repo, HOOK_PREPARE_COMMIT_MSG, hook);\n\n\t\tlet mut msg = String::from(\"test\");\n\t\tlet res = hooks_prepare_commit_msg(\n\t\t\t&repo,\n\t\t\tNone,\n\t\t\tPrepareCommitMsgSource::Commit(git2::Oid::zero()),\n\t\t\t&mut msg,\n\t\t)\n\t\t.unwrap();\n\n\t\tlet HookResult::Run(response) = res else {\n\t\t\tunreachable!()\n\t\t};\n\n\t\tassert_eq!(response.code, 2);\n\t\tassert_eq!(&response.stdout, \"rejected\\n\");\n\n\t\tassert_eq!(\n\t\t\tmsg,\n\t\t\tString::from(\n\t\t\t\t\"commit,0000000000000000000000000000000000000000\\n\"\n\t\t\t)\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_pre_push_sh() {\n\t\tlet (_td, repo) = repo_init();\n\n\t\tlet hook = b\"#!/bin/sh\nexit 0\n\t\";\n\n\t\tcreate_hook(&repo, HOOK_PRE_PUSH, hook);\n\n\t\tlet branch = head_branch(&repo);\n\t\tlet updates = [branch_update(\n\t\t\t&repo,\n\t\t\tSome(\"origin\"),\n\t\t\t&branch,\n\t\t\tNone,\n\t\t\tfalse,\n\t\t)];\n\n\t\tlet res = hooks_pre_push(\n\t\t\t&repo,\n\t\t\tNone,\n\t\t\tSome(\"origin\"),\n\t\t\t\"https://example.com/repo.git\",\n\t\t\t&updates,\n\t\t)\n\t\t.unwrap();\n\n\t\tassert!(res.is_successful());\n\t}\n\n\t#[test]\n\tfn test_pre_push_fail_sh() {\n\t\tlet (_td, repo) = repo_init();\n\n\t\tlet hook = b\"#!/bin/sh\necho 'failed'\nexit 3\n\t\";\n\t\tcreate_hook(&repo, HOOK_PRE_PUSH, hook);\n\n\t\tlet branch = head_branch(&repo);\n\t\tlet updates = [branch_update(\n\t\t\t&repo,\n\t\t\tSome(\"origin\"),\n\t\t\t&branch,\n\t\t\tNone,\n\t\t\tfalse,\n\t\t)];\n\n\t\tlet res = hooks_pre_push(\n\t\t\t&repo,\n\t\t\tNone,\n\t\t\tSome(\"origin\"),\n\t\t\t\"https://example.com/repo.git\",\n\t\t\t&updates,\n\t\t)\n\t\t.unwrap();\n\t\tlet HookResult::Run(response) = res else {\n\t\t\tunreachable!()\n\t\t};\n\t\tassert_eq!(response.code, 3);\n\t\tassert_eq!(&response.stdout, \"failed\\n\");\n\t}\n\n\t#[test]\n\tfn test_pre_push_no_remote_name() {\n\t\tlet (_td, repo) = repo_init();\n\n\t\tlet hook = b\"#!/bin/sh\n# Verify that when remote is None, URL is passed for both arguments\necho \\\"arg1=$1 arg2=$2\\\"\nexit 0\n\t\";\n\n\t\tcreate_hook(&repo, HOOK_PRE_PUSH, hook);\n\n\t\tlet branch = head_branch(&repo);\n\t\tlet updates =\n\t\t\t[branch_update(&repo, None, &branch, None, false)];\n\n\t\tlet res = hooks_pre_push(\n\t\t\t&repo,\n\t\t\tNone,\n\t\t\tNone,\n\t\t\t\"https://example.com/repo.git\",\n\t\t\t&updates,\n\t\t)\n\t\t.unwrap();\n\n\t\tlet HookResult::Run(response) = res else {\n\t\t\tpanic!(\"Expected Run result, got: {res:?}\");\n\t\t};\n\n\t\tassert!(response.is_successful());\n\t\t// When remote is None, URL should be passed for both arguments\n\t\tassert_eq!(\n\t\t\tresponse.stdout,\n\t\t\t\"arg1=https://example.com/repo.git arg2=https://example.com/repo.git\\n\"\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_pre_push_with_arguments() {\n\t\tlet (_td, repo) = repo_init();\n\n\t\tlet hook = b\"#!/bin/sh\necho \\\"remote_name=$1\\\"\necho \\\"remote_url=$2\\\"\nexit 0\n\t\";\n\n\t\tcreate_hook(&repo, HOOK_PRE_PUSH, hook);\n\n\t\tlet branch = head_branch(&repo);\n\t\tlet updates = [branch_update(\n\t\t\t&repo,\n\t\t\tSome(\"origin\"),\n\t\t\t&branch,\n\t\t\tNone,\n\t\t\tfalse,\n\t\t)];\n\n\t\tlet res = hooks_pre_push(\n\t\t\t&repo,\n\t\t\tNone,\n\t\t\tSome(\"origin\"),\n\t\t\t\"https://example.com/repo.git\",\n\t\t\t&updates,\n\t\t)\n\t\t.unwrap();\n\n\t\tlet HookResult::Run(response) = res else {\n\t\t\tunreachable!(\"Expected Run result, got: {res:?}\")\n\t\t};\n\n\t\tassert!(response.is_successful());\n\t\tassert_eq!(\n\t\t\tresponse.stdout,\n\t\t\t\"remote_name=origin\\nremote_url=https://example.com/repo.git\\n\"\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_pre_push_multiple_updates() {\n\t\tlet (_td, repo) = repo_init();\n\n\t\tlet hook = b\"#!/bin/sh\ncat\nexit 0\n\t\";\n\n\t\tcreate_hook(&repo, HOOK_PRE_PUSH, hook);\n\n\t\tlet branch = head_branch(&repo);\n\t\tlet branch_update = branch_update(\n\t\t\t&repo,\n\t\t\tSome(\"origin\"),\n\t\t\t&branch,\n\t\t\tNone,\n\t\t\tfalse,\n\t\t);\n\n\t\t// create a tag to add a second refspec\n\t\tlet head_commit =\n\t\t\trepo.head().unwrap().peel_to_commit().unwrap();\n\t\trepo.tag_lightweight(\"v1\", head_commit.as_object(), false)\n\t\t\t.unwrap();\n\t\tlet tag_ref = repo.find_reference(\"refs/tags/v1\").unwrap();\n\t\tlet tag_oid = tag_ref.target().unwrap();\n\t\tlet tag_update = PrePushRef::new(\n\t\t\t\"refs/tags/v1\",\n\t\t\tSome(tag_oid),\n\t\t\t\"refs/tags/v1\",\n\t\t\tNone,\n\t\t);\n\n\t\tlet updates = [branch_update, tag_update];\n\t\tlet expected_stdin = PrePushRef::to_stdin(&updates);\n\n\t\tlet res = hooks_pre_push(\n\t\t\t&repo,\n\t\t\tNone,\n\t\t\tSome(\"origin\"),\n\t\t\t\"https://example.com/repo.git\",\n\t\t\t&updates,\n\t\t)\n\t\t.unwrap();\n\n\t\tlet HookResult::Run(response) = res else {\n\t\t\tunreachable!(\"Expected Run result, got: {res:?}\")\n\t\t};\n\n\t\tassert!(\n\t\t\tresponse.is_successful(),\n\t\t\t\"Hook should succeed: stdout {} stderr {}\",\n\t\t\tresponse.stdout,\n\t\t\tresponse.stderr\n\t\t);\n\t\tassert_eq!(\n\t\t\tresponse.stdout, expected_stdin,\n\t\t\t\"stdin should include all refspec lines\"\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_pre_push_delete_ref_uses_zero_oid() {\n\t\tlet (_td, repo) = repo_init();\n\n\t\tlet hook = b\"#!/bin/sh\ncat\nexit 0\n\t\";\n\n\t\tcreate_hook(&repo, HOOK_PRE_PUSH, hook);\n\n\t\tlet branch = head_branch(&repo);\n\t\tlet updates = [branch_update(\n\t\t\t&repo,\n\t\t\tSome(\"origin\"),\n\t\t\t&branch,\n\t\t\tNone,\n\t\t\ttrue,\n\t\t)];\n\t\tlet expected_stdin = PrePushRef::to_stdin(&updates);\n\n\t\tlet res = hooks_pre_push(\n\t\t\t&repo,\n\t\t\tNone,\n\t\t\tSome(\"origin\"),\n\t\t\t\"https://example.com/repo.git\",\n\t\t\t&updates,\n\t\t)\n\t\t.unwrap();\n\n\t\tlet HookResult::Run(response) = res else {\n\t\t\tunreachable!(\"Expected Run result, got: {res:?}\")\n\t\t};\n\n\t\tassert!(response.is_successful());\n\t\tassert_eq!(response.stdout, expected_stdin);\n\t}\n\n\t#[test]\n\tfn test_pre_push_stdin() {\n\t\tlet (_td, repo) = repo_init();\n\n\t\tlet hook = b\"#!/bin/sh\ncat\nexit 0\n\t\t\";\n\n\t\tcreate_hook(&repo, HOOK_PRE_PUSH, hook);\n\n\t\tlet branch = head_branch(&repo);\n\t\tlet updates = [branch_update(\n\t\t\t&repo,\n\t\t\tSome(\"origin\"),\n\t\t\t&branch,\n\t\t\tNone,\n\t\t\tfalse,\n\t\t)];\n\t\tlet expected_stdin = PrePushRef::to_stdin(&updates);\n\n\t\tlet res = hooks_pre_push(\n\t\t\t&repo,\n\t\t\tNone,\n\t\t\tSome(\"origin\"),\n\t\t\t\"https://github.com/user/repo.git\",\n\t\t\t&updates,\n\t\t)\n\t\t.unwrap();\n\n\t\tlet HookResult::Run(response) = res else {\n\t\t\tunreachable!(\"Expected Run result, got: {res:?}\")\n\t\t};\n\n\t\tassert!(response.is_successful());\n\t\tassert_eq!(response.stdout, expected_stdin);\n\t}\n\n\t#[test]\n\tfn test_pre_push_uses_push_target_remote_not_upstream() {\n\t\tlet (_td, repo) = repo_init();\n\n\t\t// repo_init() already creates an initial commit on master\n\t\tlet head = repo.head().unwrap();\n\t\tlet local_commit = head.target().unwrap();\n\n\t\t// Set up scenario:\n\t\t// - Local master is at local_commit (latest)\n\t\t// - origin/master exists at local_commit (fully synced - upstream)\n\t\t// - backup/master exists at old_commit (behind/different)\n\t\t// - Branch tracks origin/master as upstream\n\t\t// - We push to \"backup\" remote\n\t\t// - Expected: remote SHA should be old_commit (not origin/master)\n\n\t\t// Create origin/master tracking branch (at same commit as local)\n\t\trepo.reference(\n\t\t\t\"refs/remotes/origin/master\",\n\t\t\tlocal_commit,\n\t\t\ttrue,\n\t\t\t\"create origin/master\",\n\t\t)\n\t\t.unwrap();\n\n\t\t// Create backup/master at a different commit\n\t\tlet sig = repo.signature().unwrap();\n\t\tlet tree_id = {\n\t\t\tlet mut index = repo.index().unwrap();\n\t\t\tindex.write_tree().unwrap()\n\t\t};\n\t\tlet tree = repo.find_tree(tree_id).unwrap();\n\t\tlet old_commit = repo\n\t\t\t.commit(None, &sig, &sig, \"old backup commit\", &tree, &[])\n\t\t\t.unwrap();\n\n\t\trepo.reference(\n\t\t\t\"refs/remotes/backup/master\",\n\t\t\told_commit,\n\t\t\ttrue,\n\t\t\t\"create backup/master at old commit\",\n\t\t)\n\t\t.unwrap();\n\n\t\t// Configure upstream to origin\n\t\t{\n\t\t\tlet mut config = repo.config().unwrap();\n\t\t\tconfig.set_str(\"branch.master.remote\", \"origin\").unwrap();\n\t\t\tconfig\n\t\t\t\t.set_str(\"branch.master.merge\", \"refs/heads/master\")\n\t\t\t\t.unwrap();\n\t\t}\n\n\t\tlet hook = b\"#!/bin/sh\ncat\nexit 0\n\";\n\n\t\tcreate_hook(&repo, HOOK_PRE_PUSH, hook);\n\n\t\tlet branch = head_branch(&repo);\n\t\tlet updates = [branch_update(\n\t\t\t&repo,\n\t\t\tSome(\"backup\"),\n\t\t\t&branch,\n\t\t\tNone,\n\t\t\tfalse,\n\t\t)];\n\t\tlet expected_stdin = PrePushRef::to_stdin(&updates);\n\n\t\tlet res = hooks_pre_push(\n\t\t\t&repo,\n\t\t\tNone,\n\t\t\tSome(\"backup\"),\n\t\t\t\"https://github.com/user/backup-repo.git\",\n\t\t\t&updates,\n\t\t)\n\t\t.unwrap();\n\n\t\tlet HookResult::Run(response) = res else {\n\t\t\tpanic!(\"Expected Run result, got: {res:?}\")\n\t\t};\n\n\t\tassert!(response.is_successful());\n\t\tassert_eq!(response.stdout, expected_stdin);\n\t}\n}\n"
  },
  {
    "path": "git2-testing/Cargo.toml",
    "content": "[package]\nname = \"git2-testing\"\nversion = \"0.1.0\"\nauthors = [\"extrawurst <mail@rusticorn.com>\"]\nedition = \"2021\"\ndescription = \"convenience functions to write unittests on top of git2-rs\"\nhomepage = \"https://github.com/gitui-org/gitui\"\nrepository = \"https://github.com/gitui-org/gitui\"\nreadme = \"README.md\"\nlicense = \"MIT\"\ncategories = [\"development-tools\"]\nkeywords = [\"git\"]\n\n[dependencies]\nenv_logger = \"0.11\"\ngit2 = \">=0.17\"\nlog = \"0.4\"\ntempfile = \"3\"\n"
  },
  {
    "path": "git2-testing/README.md",
    "content": "# git2-testing\n\n*convenience functions on top of git2-rs for convenient unittest repository generation*\n\n"
  },
  {
    "path": "git2-testing/src/lib.rs",
    "content": "#![deny(mismatched_lifetime_syntaxes)]\n\nuse git2::Repository;\nuse tempfile::TempDir;\n\n/// initialize test repo in temp path\npub fn repo_init_empty() -> (TempDir, Repository) {\n\tinit_log();\n\n\tsandbox_config_files();\n\n\tlet td = TempDir::new().unwrap();\n\tlet repo = Repository::init(td.path()).unwrap();\n\t{\n\t\tlet mut config = repo.config().unwrap();\n\t\tconfig.set_str(\"user.name\", \"name\").unwrap();\n\t\tconfig.set_str(\"user.email\", \"email\").unwrap();\n\t}\n\n\t(td, repo)\n}\n\n/// initialize test repo in temp path with an empty first commit\npub fn repo_init() -> (TempDir, Repository) {\n\tinit_log();\n\n\tsandbox_config_files();\n\n\tlet td = TempDir::new().unwrap();\n\tlet repo = Repository::init(td.path()).unwrap();\n\t{\n\t\tlet mut config = repo.config().unwrap();\n\t\tconfig.set_str(\"user.name\", \"name\").unwrap();\n\t\tconfig.set_str(\"user.email\", \"email\").unwrap();\n\n\t\tlet mut index = repo.index().unwrap();\n\t\tlet id = index.write_tree().unwrap();\n\n\t\tlet tree = repo.find_tree(id).unwrap();\n\t\tlet sig = repo.signature().unwrap();\n\t\trepo.commit(Some(\"HEAD\"), &sig, &sig, \"initial\", &tree, &[])\n\t\t\t.unwrap();\n\t}\n\n\t(td, repo)\n}\n\n// init log\nfn init_log() {\n\tlet _ = env_logger::builder()\n\t\t.is_test(true)\n\t\t.filter_level(log::LevelFilter::Trace)\n\t\t.try_init();\n}\n\n/// Same as `repo_init`, but the repo is a bare repo (--bare)\npub fn repo_init_bare() -> (TempDir, Repository) {\n\tinit_log();\n\n\tlet tmp_repo_dir = TempDir::new().unwrap();\n\tlet bare_repo =\n\t\tRepository::init_bare(tmp_repo_dir.path()).unwrap();\n\n\t(tmp_repo_dir, bare_repo)\n}\n\n/// Calling `set_search_path` with an empty directory makes sure that there\n/// is no git config interfering with our tests (for example user-local\n/// `.gitconfig`).\n#[allow(unsafe_code)]\nfn sandbox_config_files() {\n\tuse git2::{opts::set_search_path, ConfigLevel};\n\tuse std::sync::Once;\n\n\tstatic INIT: Once = Once::new();\n\n\t// Adapted from https://github.com/rust-lang/cargo/pull/9035\n\tINIT.call_once(|| unsafe {\n\t\tlet temp_dir = TempDir::new().unwrap();\n\t\tlet path = temp_dir.path();\n\n\t\tset_search_path(ConfigLevel::System, path).unwrap();\n\t\tset_search_path(ConfigLevel::Global, path).unwrap();\n\t\tset_search_path(ConfigLevel::XDG, path).unwrap();\n\t\tset_search_path(ConfigLevel::ProgramData, path).unwrap();\n\t});\n}\n"
  },
  {
    "path": "invalidstring/Cargo.toml",
    "content": "[package]\nname = \"invalidstring\"\nversion = \"0.1.3\"\nauthors = [\"extrawurst <mail@rusticorn.com>\"]\nedition = \"2021\"\ndescription = \"just for testing invalid string data\"\nhomepage = \"https://github.com/gitui-org/gitui\"\nrepository = \"https://github.com/gitui-org/gitui\"\nreadme = \"README.md\"\nlicense = \"MIT\"\ncategories = [\"development-tools\", \"development-tools::testing\", \"encoding\"]\nkeywords = [\"string\"]\n"
  },
  {
    "path": "invalidstring/README.md",
    "content": "# invalidstring\n\n*just for testing invalid string data*\n\nThis crate is part of the [gitui](http://gitui.org) project. We need this to be a separate crate so that `asyncgit` can remain forbidding `unsafe`."
  },
  {
    "path": "invalidstring/src/lib.rs",
    "content": "#![deny(mismatched_lifetime_syntaxes)]\n\n/// uses unsafe to postfix the string with invalid utf8 data\n#[allow(invalid_from_utf8_unchecked)]\npub fn invalid_utf8(prefix: &str) -> String {\n\tlet bytes = b\"\\xc3\\x73\";\n\n\tunsafe {\n\t\tformat!(\"{prefix}{}\", std::str::from_utf8_unchecked(bytes))\n\t}\n}\n"
  },
  {
    "path": "rust-toolchain.toml",
    "content": "[toolchain]\nchannel = \"stable\"\nprofile = \"default\"\n"
  },
  {
    "path": "rustfmt.toml",
    "content": "max_width=70\nhard_tabs=true\nnewline_style=\"Unix\""
  },
  {
    "path": "scopetime/Cargo.toml",
    "content": "[package]\nname = \"scopetime\"\nversion = \"0.1.2\"\nauthors = [\"extrawurst <mail@rusticorn.com>\"]\nedition = \"2021\"\ndescription = \"log runtime of arbitrary scope\"\nhomepage = \"https://github.com/gitui-org/gitui\"\nrepository = \"https://github.com/gitui-org/gitui\"\nlicense = \"MIT\"\nreadme = \"README.md\"\ncategories = [\"development-tools::profiling\"]\nkeywords = [\"profiling\", \"logging\"]\n\n[features]\ndefault = []\nenabled = []\n\n[dependencies]\nlog = \"0.4\"\n"
  },
  {
    "path": "scopetime/README.md",
    "content": "# scopetime\n\n*log runtime of arbitrary scope*\n\nThis crate is part of the [gitui](http://gitui.org) project and can be used to annotate arbitrary scopes to `trace` their execution times via `log`:\n\nin your crate:\n```\n[dependencies]\nscopetime = \"0.1\"\n```\n\nin your code:\n```rust\nfn foo(){\n    scope_time!(\"foo\");\n\n    // ... do something u wanna measure\n}\n```\n\nthe resulting log looks something like this:\n```\n19:45:00 [TRACE] (7) scopetime: [scopetime/src/lib.rs:34] scopetime: 2 ms [my_crate::foo] @my_crate/src/bar.rs:5\n```\n"
  },
  {
    "path": "scopetime/src/lib.rs",
    "content": "//! simple macro to insert a scope based runtime measure that logs the result\n\n#![forbid(unsafe_code)]\n#![deny(mismatched_lifetime_syntaxes, unused_imports)]\n#![deny(clippy::unwrap_used)]\n#![deny(clippy::perf)]\n\nuse std::time::Instant;\n\npub struct ScopeTimeLog<'a> {\n\ttitle: &'a str,\n\tmod_path: &'a str,\n\tfile: &'a str,\n\tline: u32,\n\ttime: Instant,\n}\n\nimpl<'a> ScopeTimeLog<'a> {\n\tpub fn new(\n\t\tmod_path: &'a str,\n\t\ttitle: &'a str,\n\t\tfile: &'a str,\n\t\tline: u32,\n\t) -> Self {\n\t\tSelf {\n\t\t\ttitle,\n\t\t\tmod_path,\n\t\t\tfile,\n\t\t\tline,\n\t\t\ttime: Instant::now(),\n\t\t}\n\t}\n}\n\nimpl Drop for ScopeTimeLog<'_> {\n\tfn drop(&mut self) {\n\t\tlog::trace!(\n\t\t\t\"scopetime: {:?} ms [{}::{}] @{}:{}\",\n\t\t\tself.time.elapsed().as_millis(),\n\t\t\tself.mod_path,\n\t\t\tself.title,\n\t\t\tself.file,\n\t\t\tself.line,\n\t\t);\n\t}\n}\n\n/// measures runtime of scope and prints it into log\n#[cfg(feature = \"enabled\")]\n#[macro_export]\nmacro_rules! scope_time {\n\t($target:literal) => {\n\t\t#[allow(unused_variables)]\n\t\tlet time = $crate::ScopeTimeLog::new(\n\t\t\tmodule_path!(),\n\t\t\t$target,\n\t\t\tfile!(),\n\t\t\tline!(),\n\t\t);\n\t};\n}\n\n#[doc(hidden)]\n#[cfg(not(feature = \"enabled\"))]\n#[macro_export]\nmacro_rules! scope_time {\n\t($target:literal) => {};\n}\n"
  },
  {
    "path": "src/app.rs",
    "content": "use crate::{\n\taccessors,\n\targs::CliArgs,\n\tcmdbar::CommandBar,\n\tcomponents::{\n\t\tcommand_pump, event_pump, CommandInfo, Component,\n\t\tDrawableComponent, FuzzyFinderTarget,\n\t},\n\tinput::{Input, InputEvent, InputState},\n\tkeys::{key_match, KeyConfig, SharedKeyConfig},\n\toptions::{Options, SharedOptions},\n\tpopup_stack::PopupStack,\n\tpopups::{\n\t\tAppOption, BlameFilePopup, BranchListPopup,\n\t\tCheckoutOptionPopup, CommitPopup, CompareCommitsPopup,\n\t\tConfirmPopup, CreateBranchPopup, CreateRemotePopup,\n\t\tExternalEditorPopup, FetchPopup, FileRevlogPopup,\n\t\tFuzzyFindPopup, GotoLinePopup, HelpPopup, InspectCommitPopup,\n\t\tLogSearchPopupPopup, MsgPopup, OptionsPopup, PullPopup,\n\t\tPushPopup, PushTagsPopup, RemoteListPopup, RenameBranchPopup,\n\t\tRenameRemotePopup, ResetPopup, RevisionFilesPopup,\n\t\tStashMsgPopup, SubmodulesListPopup, TagCommitPopup,\n\t\tTagListPopup, UpdateRemoteUrlPopup,\n\t},\n\tqueue::{\n\t\tAction, AppTabs, InternalEvent, NeedsUpdate, Queue,\n\t\tStackablePopupOpen,\n\t},\n\tsetup_popups,\n\tstrings::{self, ellipsis_trim_start, order},\n\ttabs::{FilesTab, Revlog, StashList, Stashing, Status},\n\ttry_or_popup,\n\tui::style::{SharedTheme, Theme},\n\tAsyncAppNotification, AsyncNotification,\n};\nuse anyhow::{bail, Result};\nuse asyncgit::{\n\tsync::{\n\t\tself,\n\t\tutils::{repo_work_dir, undo_last_commit},\n\t\tRepoPath, RepoPathRef,\n\t},\n\tAsyncGitNotification, PushType,\n};\nuse crossbeam_channel::Sender;\nuse crossterm::event::{Event, KeyEvent};\nuse ratatui::{\n\tlayout::{\n\t\tAlignment, Constraint, Direction, Layout, Margin, Rect,\n\t},\n\ttext::{Line, Span},\n\twidgets::{Block, Borders, Paragraph, Tabs},\n\tFrame,\n};\nuse std::{\n\tcell::{Cell, RefCell},\n\tpath::{Path, PathBuf},\n\trc::Rc,\n};\nuse unicode_width::UnicodeWidthStr;\n\n#[derive(Clone)]\npub enum QuitState {\n\tNone,\n\tClose,\n\tOpenSubmodule(RepoPath),\n}\n\n/// the main app type\npub struct App {\n\trepo: RepoPathRef,\n\tdo_quit: QuitState,\n\thelp_popup: HelpPopup,\n\tmsg_popup: MsgPopup,\n\tconfirm_popup: ConfirmPopup,\n\tcommit_popup: CommitPopup,\n\tblame_file_popup: BlameFilePopup,\n\tfile_revlog_popup: FileRevlogPopup,\n\tstashmsg_popup: StashMsgPopup,\n\tinspect_commit_popup: InspectCommitPopup,\n\tcompare_commits_popup: CompareCommitsPopup,\n\texternal_editor_popup: ExternalEditorPopup,\n\trevision_files_popup: RevisionFilesPopup,\n\tfuzzy_find_popup: FuzzyFindPopup,\n\tlog_search_popup: LogSearchPopupPopup,\n\tpush_popup: PushPopup,\n\tpush_tags_popup: PushTagsPopup,\n\tpull_popup: PullPopup,\n\tfetch_popup: FetchPopup,\n\ttag_commit_popup: TagCommitPopup,\n\tcreate_branch_popup: CreateBranchPopup,\n\tcreate_remote_popup: CreateRemotePopup,\n\trename_remote_popup: RenameRemotePopup,\n\tupdate_remote_url_popup: UpdateRemoteUrlPopup,\n\tremotes_popup: RemoteListPopup,\n\trename_branch_popup: RenameBranchPopup,\n\tselect_branch_popup: BranchListPopup,\n\toptions_popup: OptionsPopup,\n\tsubmodule_popup: SubmodulesListPopup,\n\ttags_popup: TagListPopup,\n\treset_popup: ResetPopup,\n\tcheckout_option_popup: CheckoutOptionPopup,\n\tcmdbar: RefCell<CommandBar>,\n\ttab: usize,\n\trevlog: Revlog,\n\tstatus_tab: Status,\n\tstashing_tab: Stashing,\n\tstashlist_tab: StashList,\n\tfiles_tab: FilesTab,\n\tqueue: Queue,\n\ttheme: SharedTheme,\n\tkey_config: SharedKeyConfig,\n\tinput: Input,\n\tpopup_stack: PopupStack,\n\toptions: SharedOptions,\n\trepo_path_text: String,\n\tgoto_line_popup: GotoLinePopup,\n\n\t// \"Flags\"\n\trequires_redraw: Cell<bool>,\n\tfile_to_open: Option<String>,\n}\n\npub struct Environment {\n\tpub queue: Queue,\n\tpub theme: SharedTheme,\n\tpub key_config: SharedKeyConfig,\n\tpub repo: RepoPathRef,\n\tpub options: SharedOptions,\n\tpub sender_git: Sender<AsyncGitNotification>,\n\tpub sender_app: Sender<AsyncAppNotification>,\n}\n\n/// The need to construct a \"whatever\" environment only arises in testing right now\n#[cfg(test)]\nimpl Environment {\n\tpub fn test_env() -> Self {\n\t\tuse crossbeam_channel::unbounded;\n\t\tSelf {\n\t\t\tqueue: Queue::new(),\n\t\t\ttheme: Default::default(),\n\t\t\tkey_config: Default::default(),\n\t\t\trepo: RefCell::new(RepoPath::Path(Default::default())),\n\t\t\toptions: Rc::new(RefCell::new(Options::test_env())),\n\t\t\tsender_git: unbounded().0,\n\t\t\tsender_app: unbounded().0,\n\t\t}\n\t}\n}\n\n// public interface\nimpl App {\n\t///\n\t#[allow(clippy::too_many_lines)]\n\tpub fn new(\n\t\tcliargs: CliArgs,\n\t\tsender_git: Sender<AsyncGitNotification>,\n\t\tsender_app: Sender<AsyncAppNotification>,\n\t\tinput: Input,\n\t\ttheme: Theme,\n\t\tkey_config: KeyConfig,\n\t) -> Result<Self> {\n\t\tlet repo = RefCell::new(cliargs.repo_path.clone());\n\t\tlog::trace!(\"open repo at: {:?}\", &repo);\n\n\t\tlet repo_path_text =\n\t\t\trepo_work_dir(&repo.borrow()).unwrap_or_default();\n\n\t\tlet env = Environment {\n\t\t\tqueue: Queue::new(),\n\t\t\ttheme: Rc::new(theme),\n\t\t\tkey_config: Rc::new(key_config),\n\t\t\toptions: Options::new(repo.clone()),\n\t\t\trepo,\n\t\t\tsender_git,\n\t\t\tsender_app,\n\t\t};\n\n\t\tlet mut select_file: Option<PathBuf> = None;\n\t\tlet tab = if let Some(file) = cliargs.select_file {\n\t\t\t// convert to relative git path\n\t\t\tif let Ok(abs) = file.canonicalize() {\n\t\t\t\tif let Ok(path) = abs.strip_prefix(\n\t\t\t\t\tenv.repo.borrow().gitpath().canonicalize()?,\n\t\t\t\t) {\n\t\t\t\t\tselect_file = Some(Path::new(\".\").join(path));\n\t\t\t\t}\n\t\t\t}\n\t\t\t2\n\t\t} else {\n\t\t\tenv.options.borrow().current_tab()\n\t\t};\n\n\t\tlet mut app = Self {\n\t\t\tinput,\n\t\t\tconfirm_popup: ConfirmPopup::new(&env),\n\t\t\tcommit_popup: CommitPopup::new(&env),\n\t\t\tblame_file_popup: BlameFilePopup::new(\n\t\t\t\t&env,\n\t\t\t\t&strings::blame_title(&env.key_config),\n\t\t\t),\n\t\t\tfile_revlog_popup: FileRevlogPopup::new(&env),\n\t\t\trevision_files_popup: RevisionFilesPopup::new(&env),\n\t\t\tstashmsg_popup: StashMsgPopup::new(&env),\n\t\t\tinspect_commit_popup: InspectCommitPopup::new(&env),\n\t\t\tcompare_commits_popup: CompareCommitsPopup::new(&env),\n\t\t\texternal_editor_popup: ExternalEditorPopup::new(&env),\n\t\t\tpush_popup: PushPopup::new(&env),\n\t\t\tpush_tags_popup: PushTagsPopup::new(&env),\n\t\t\treset_popup: ResetPopup::new(&env),\n\t\t\tpull_popup: PullPopup::new(&env),\n\t\t\tfetch_popup: FetchPopup::new(&env),\n\t\t\ttag_commit_popup: TagCommitPopup::new(&env),\n\t\t\tcreate_branch_popup: CreateBranchPopup::new(&env),\n\t\t\tcreate_remote_popup: CreateRemotePopup::new(&env),\n\t\t\trename_remote_popup: RenameRemotePopup::new(&env),\n\t\t\tupdate_remote_url_popup: UpdateRemoteUrlPopup::new(&env),\n\t\t\tremotes_popup: RemoteListPopup::new(&env),\n\t\t\trename_branch_popup: RenameBranchPopup::new(&env),\n\t\t\tselect_branch_popup: BranchListPopup::new(&env),\n\t\t\ttags_popup: TagListPopup::new(&env),\n\t\t\toptions_popup: OptionsPopup::new(&env),\n\t\t\tsubmodule_popup: SubmodulesListPopup::new(&env),\n\t\t\tlog_search_popup: LogSearchPopupPopup::new(&env),\n\t\t\tfuzzy_find_popup: FuzzyFindPopup::new(&env),\n\t\t\tdo_quit: QuitState::None,\n\t\t\tcmdbar: RefCell::new(CommandBar::new(\n\t\t\t\tenv.theme.clone(),\n\t\t\t\tenv.key_config.clone(),\n\t\t\t)),\n\t\t\thelp_popup: HelpPopup::new(&env),\n\t\t\tmsg_popup: MsgPopup::new(&env),\n\t\t\trevlog: Revlog::new(&env),\n\t\t\tstatus_tab: Status::new(&env),\n\t\t\tstashing_tab: Stashing::new(&env),\n\t\t\tstashlist_tab: StashList::new(&env),\n\t\t\tfiles_tab: FilesTab::new(&env, select_file),\n\t\t\tcheckout_option_popup: CheckoutOptionPopup::new(&env),\n\t\t\tgoto_line_popup: GotoLinePopup::new(&env),\n\t\t\ttab: 0,\n\t\t\tqueue: env.queue,\n\t\t\ttheme: env.theme,\n\t\t\toptions: env.options,\n\t\t\tkey_config: env.key_config,\n\t\t\trequires_redraw: Cell::new(false),\n\t\t\tfile_to_open: None,\n\t\t\trepo: env.repo,\n\t\t\trepo_path_text,\n\t\t\tpopup_stack: PopupStack::default(),\n\t\t};\n\n\t\tapp.set_tab(tab)?;\n\n\t\tOk(app)\n\t}\n\n\t///\n\tpub fn draw(&self, f: &mut Frame) -> Result<()> {\n\t\tlet fsize = f.area();\n\n\t\tself.cmdbar.borrow_mut().refresh_width(fsize.width);\n\n\t\tlet chunks_main = Layout::default()\n\t\t\t.direction(Direction::Vertical)\n\t\t\t.constraints(\n\t\t\t\t[\n\t\t\t\t\tConstraint::Length(2),\n\t\t\t\t\tConstraint::Min(2),\n\t\t\t\t\tConstraint::Length(self.cmdbar.borrow().height()),\n\t\t\t\t]\n\t\t\t\t.as_ref(),\n\t\t\t)\n\t\t\t.split(fsize);\n\n\t\tself.cmdbar.borrow().draw(f, chunks_main[2]);\n\n\t\tself.draw_top_bar(f, chunks_main[0]);\n\n\t\t//TODO: component property + a macro `fullscreen_popup_open!`\n\t\t// to make this scale better?\n\t\tlet fullscreen_popup_open =\n\t\t\tself.revision_files_popup.is_visible()\n\t\t\t\t|| self.inspect_commit_popup.is_visible()\n\t\t\t\t|| self.compare_commits_popup.is_visible()\n\t\t\t\t|| self.blame_file_popup.is_visible()\n\t\t\t\t|| self.file_revlog_popup.is_visible();\n\n\t\tif !fullscreen_popup_open {\n\t\t\t//TODO: macro because of generic draw call\n\t\t\tmatch self.tab {\n\t\t\t\t0 => self.status_tab.draw(f, chunks_main[1])?,\n\t\t\t\t1 => self.revlog.draw(f, chunks_main[1])?,\n\t\t\t\t2 => self.files_tab.draw(f, chunks_main[1])?,\n\t\t\t\t3 => self.stashing_tab.draw(f, chunks_main[1])?,\n\t\t\t\t4 => self.stashlist_tab.draw(f, chunks_main[1])?,\n\t\t\t\t_ => bail!(\"unknown tab\"),\n\t\t\t}\n\t\t}\n\n\t\tself.draw_popups(f)?;\n\n\t\tOk(())\n\t}\n\n\t///\n\tpub fn event(&mut self, ev: InputEvent) -> Result<()> {\n\t\tlog::trace!(\"event: {ev:?}\");\n\n\t\tif let InputEvent::Input(ev) = ev {\n\t\t\tif self.check_hard_exit(&ev) || self.check_quit(&ev) {\n\t\t\t\treturn Ok(());\n\t\t\t}\n\n\t\t\tlet mut flags = NeedsUpdate::empty();\n\n\t\t\tif event_pump(&ev, self.components_mut().as_mut_slice())?\n\t\t\t\t.is_consumed()\n\t\t\t{\n\t\t\t\tflags.insert(NeedsUpdate::COMMANDS);\n\t\t\t} else if let Event::Key(k) = &ev {\n\t\t\t\tlet new_flags = if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.tab_toggle,\n\t\t\t\t) {\n\t\t\t\t\tself.toggle_tabs(false)?;\n\t\t\t\t\tNeedsUpdate::COMMANDS\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.tab_toggle_reverse,\n\t\t\t\t) {\n\t\t\t\t\tself.toggle_tabs(true)?;\n\t\t\t\t\tNeedsUpdate::COMMANDS\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.tab_status,\n\t\t\t\t) || key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.tab_log,\n\t\t\t\t) || key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.tab_files,\n\t\t\t\t) || key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.tab_stashing,\n\t\t\t\t) || key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.tab_stashes,\n\t\t\t\t) {\n\t\t\t\t\tself.switch_tab(k)?;\n\t\t\t\t\tNeedsUpdate::COMMANDS\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.cmd_bar_toggle,\n\t\t\t\t) {\n\t\t\t\t\tself.cmdbar.borrow_mut().toggle_more();\n\t\t\t\t\tNeedsUpdate::empty()\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.open_options,\n\t\t\t\t) {\n\t\t\t\t\tself.options_popup.show()?;\n\t\t\t\t\tNeedsUpdate::ALL\n\t\t\t\t} else {\n\t\t\t\t\tNeedsUpdate::empty()\n\t\t\t\t};\n\n\t\t\t\tflags.insert(new_flags);\n\t\t\t}\n\n\t\t\tself.process_queue(flags)?;\n\t\t} else if let InputEvent::State(polling_state) = ev {\n\t\t\tself.external_editor_popup.hide();\n\t\t\tif matches!(polling_state, InputState::Paused) {\n\t\t\t\tlet result =\n\t\t\t\t\tif let Some(path) = self.file_to_open.take() {\n\t\t\t\t\t\tExternalEditorPopup::open_file_in_editor(\n\t\t\t\t\t\t\t&self.repo.borrow(),\n\t\t\t\t\t\t\tPath::new(&path),\n\t\t\t\t\t\t)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlet changes =\n\t\t\t\t\t\t\tself.status_tab.get_files_changes()?;\n\t\t\t\t\t\tself.commit_popup.show_editor(changes)\n\t\t\t\t\t};\n\n\t\t\t\tif let Err(e) = result {\n\t\t\t\t\tlet msg =\n\t\t\t\t\t\tformat!(\"failed to launch editor:\\n{e}\");\n\t\t\t\t\tlog::error!(\"{}\", msg.as_str());\n\t\t\t\t\tself.msg_popup.show_error(msg.as_str())?;\n\t\t\t\t}\n\n\t\t\t\tself.requires_redraw.set(true);\n\t\t\t\tself.input.set_polling(true);\n\t\t\t}\n\t\t}\n\n\t\tOk(())\n\t}\n\n\t//TODO: do we need this?\n\t/// forward ticking to components that require it\n\tpub fn update(&mut self) -> Result<()> {\n\t\tlog::trace!(\"update\");\n\n\t\tself.commit_popup.update();\n\t\tself.status_tab.update()?;\n\t\tself.revlog.update()?;\n\t\tself.files_tab.update()?;\n\t\tself.stashing_tab.update()?;\n\t\tself.stashlist_tab.update()?;\n\t\tself.reset_popup.update()?;\n\n\t\tself.update_commands();\n\n\t\tOk(())\n\t}\n\n\t///\n\tpub fn update_async(\n\t\t&mut self,\n\t\tev: AsyncNotification,\n\t) -> Result<()> {\n\t\tlog::trace!(\"update_async: {ev:?}\");\n\n\t\tif let AsyncNotification::Git(ev) = ev {\n\t\t\tself.status_tab.update_git(ev)?;\n\t\t\tself.stashing_tab.update_git(ev)?;\n\t\t\tself.revlog.update_git(ev)?;\n\t\t\tself.file_revlog_popup.update_git(ev)?;\n\t\t\tself.inspect_commit_popup.update_git(ev)?;\n\t\t\tself.compare_commits_popup.update_git(ev)?;\n\t\t\tself.push_popup.update_git(ev)?;\n\t\t\tself.push_tags_popup.update_git(ev)?;\n\t\t\tself.pull_popup.update_git(ev);\n\t\t\tself.fetch_popup.update_git(ev);\n\t\t\tself.select_branch_popup.update_git(ev)?;\n\t\t}\n\n\t\tself.files_tab.update_async(ev)?;\n\t\tself.blame_file_popup.update_async(ev)?;\n\t\tself.revision_files_popup.update(ev)?;\n\t\tself.tags_popup.update(ev);\n\n\t\t//TODO: better system for this\n\t\t// can we simply process the queue here and everyone just uses the queue to schedule a cmd update?\n\t\tself.process_queue(NeedsUpdate::COMMANDS)?;\n\n\t\tOk(())\n\t}\n\n\t///\n\tpub fn is_quit(&self) -> bool {\n\t\t!matches!(self.do_quit, QuitState::None)\n\t\t\t|| self.input.is_aborted()\n\t}\n\n\t///\n\tpub fn quit_state(&self) -> QuitState {\n\t\tself.do_quit.clone()\n\t}\n\n\t///\n\tpub fn any_work_pending(&self) -> bool {\n\t\tself.status_tab.anything_pending()\n\t\t\t|| self.revlog.any_work_pending()\n\t\t\t|| self.stashing_tab.anything_pending()\n\t\t\t|| self.files_tab.anything_pending()\n\t\t\t|| self.blame_file_popup.any_work_pending()\n\t\t\t|| self.file_revlog_popup.any_work_pending()\n\t\t\t|| self.inspect_commit_popup.any_work_pending()\n\t\t\t|| self.compare_commits_popup.any_work_pending()\n\t\t\t|| self.input.is_state_changing()\n\t\t\t|| self.push_popup.any_work_pending()\n\t\t\t|| self.push_tags_popup.any_work_pending()\n\t\t\t|| self.pull_popup.any_work_pending()\n\t\t\t|| self.fetch_popup.any_work_pending()\n\t\t\t|| self.revision_files_popup.any_work_pending()\n\t\t\t|| self.tags_popup.any_work_pending()\n\t}\n\n\t///\n\tpub fn requires_redraw(&self) -> bool {\n\t\tif self.requires_redraw.get() {\n\t\t\tself.requires_redraw.set(false);\n\t\t\ttrue\n\t\t} else {\n\t\t\tfalse\n\t\t}\n\t}\n}\n\n// private impls\nimpl App {\n\taccessors!(\n\t\tself,\n\t\t[\n\t\t\tlog_search_popup,\n\t\t\tfuzzy_find_popup,\n\t\t\tmsg_popup,\n\t\t\tconfirm_popup,\n\t\t\tcommit_popup,\n\t\t\tgoto_line_popup,\n\t\t\tblame_file_popup,\n\t\t\tfile_revlog_popup,\n\t\t\tstashmsg_popup,\n\t\t\tinspect_commit_popup,\n\t\t\tcompare_commits_popup,\n\t\t\texternal_editor_popup,\n\t\t\tpush_popup,\n\t\t\tpush_tags_popup,\n\t\t\tpull_popup,\n\t\t\tfetch_popup,\n\t\t\ttag_commit_popup,\n\t\t\treset_popup,\n\t\t\tcheckout_option_popup,\n\t\t\tcreate_branch_popup,\n\t\t\tcreate_remote_popup,\n\t\t\trename_remote_popup,\n\t\t\tupdate_remote_url_popup,\n\t\t\tremotes_popup,\n\t\t\trename_branch_popup,\n\t\t\tselect_branch_popup,\n\t\t\trevision_files_popup,\n\t\t\tsubmodule_popup,\n\t\t\ttags_popup,\n\t\t\toptions_popup,\n\t\t\thelp_popup,\n\t\t\trevlog,\n\t\t\tstatus_tab,\n\t\t\tfiles_tab,\n\t\t\tstashing_tab,\n\t\t\tstashlist_tab\n\t\t]\n\t);\n\n\tsetup_popups!(\n\t\tself,\n\t\t[\n\t\t\tcommit_popup,\n\t\t\tstashmsg_popup,\n\t\t\thelp_popup,\n\t\t\tinspect_commit_popup,\n\t\t\tcompare_commits_popup,\n\t\t\tblame_file_popup,\n\t\t\tfile_revlog_popup,\n\t\t\texternal_editor_popup,\n\t\t\ttag_commit_popup,\n\t\t\tselect_branch_popup,\n\t\t\tremotes_popup,\n\t\t\tcreate_remote_popup,\n\t\t\trename_remote_popup,\n\t\t\tupdate_remote_url_popup,\n\t\t\tsubmodule_popup,\n\t\t\ttags_popup,\n\t\t\treset_popup,\n\t\t\tcheckout_option_popup,\n\t\t\tcreate_branch_popup,\n\t\t\trename_branch_popup,\n\t\t\trevision_files_popup,\n\t\t\tfuzzy_find_popup,\n\t\t\tlog_search_popup,\n\t\t\tpush_popup,\n\t\t\tpush_tags_popup,\n\t\t\tpull_popup,\n\t\t\tfetch_popup,\n\t\t\toptions_popup,\n\t\t\tconfirm_popup,\n\t\t\tmsg_popup,\n\t\t\tgoto_line_popup\n\t\t]\n\t);\n\n\tfn check_quit(&mut self, ev: &Event) -> bool {\n\t\tif self.any_popup_visible() {\n\t\t\treturn false;\n\t\t}\n\t\tif let Event::Key(e) = ev {\n\t\t\tif key_match(e, self.key_config.keys.quit) {\n\t\t\t\tself.do_quit = QuitState::Close;\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\tfalse\n\t}\n\n\tfn check_hard_exit(&mut self, ev: &Event) -> bool {\n\t\tif let Event::Key(e) = ev {\n\t\t\tif key_match(e, self.key_config.keys.exit) {\n\t\t\t\tself.do_quit = QuitState::Close;\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\tfalse\n\t}\n\n\tfn get_tabs(&mut self) -> Vec<&mut dyn Component> {\n\t\tvec![\n\t\t\t&mut self.status_tab,\n\t\t\t&mut self.revlog,\n\t\t\t&mut self.files_tab,\n\t\t\t&mut self.stashing_tab,\n\t\t\t&mut self.stashlist_tab,\n\t\t]\n\t}\n\n\tfn toggle_tabs(&mut self, reverse: bool) -> Result<()> {\n\t\tlet tabs_len = self.get_tabs().len();\n\t\tlet new_tab = if reverse {\n\t\t\tself.tab.wrapping_sub(1).min(tabs_len.saturating_sub(1))\n\t\t} else {\n\t\t\tself.tab.saturating_add(1) % tabs_len\n\t\t};\n\n\t\tself.set_tab(new_tab)\n\t}\n\n\tfn switch_tab(&mut self, k: &KeyEvent) -> Result<()> {\n\t\tif key_match(k, self.key_config.keys.tab_status) {\n\t\t\tself.switch_to_tab(&AppTabs::Status)?;\n\t\t} else if key_match(k, self.key_config.keys.tab_log) {\n\t\t\tself.switch_to_tab(&AppTabs::Log)?;\n\t\t} else if key_match(k, self.key_config.keys.tab_files) {\n\t\t\tself.switch_to_tab(&AppTabs::Files)?;\n\t\t} else if key_match(k, self.key_config.keys.tab_stashing) {\n\t\t\tself.switch_to_tab(&AppTabs::Stashing)?;\n\t\t} else if key_match(k, self.key_config.keys.tab_stashes) {\n\t\t\tself.switch_to_tab(&AppTabs::Stashlist)?;\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn set_tab(&mut self, tab: usize) -> Result<()> {\n\t\tlet tabs = self.get_tabs();\n\t\tfor (i, t) in tabs.into_iter().enumerate() {\n\t\t\tif tab == i {\n\t\t\t\tt.show()?;\n\t\t\t} else {\n\t\t\t\tt.hide();\n\t\t\t}\n\t\t}\n\n\t\tself.tab = tab;\n\t\tself.options.borrow_mut().set_current_tab(tab);\n\n\t\tOk(())\n\t}\n\n\tfn switch_to_tab(&mut self, tab: &AppTabs) -> Result<()> {\n\t\tmatch tab {\n\t\t\tAppTabs::Status => self.set_tab(0)?,\n\t\t\tAppTabs::Log => self.set_tab(1)?,\n\t\t\tAppTabs::Files => self.set_tab(2)?,\n\t\t\tAppTabs::Stashing => self.set_tab(3)?,\n\t\t\tAppTabs::Stashlist => self.set_tab(4)?,\n\t\t}\n\t\tOk(())\n\t}\n\n\tfn update_commands(&mut self) {\n\t\tif self.help_popup.is_visible() {\n\t\t\tself.help_popup.set_cmds(self.commands(true));\n\t\t}\n\t\tself.cmdbar.borrow_mut().set_cmds(self.commands(false));\n\t}\n\n\tfn process_queue(&mut self, flags: NeedsUpdate) -> Result<()> {\n\t\tlet mut flags = flags;\n\t\tlet new_flags = self.process_internal_events()?;\n\t\tflags.insert(new_flags);\n\n\t\tif flags.contains(NeedsUpdate::ALL) {\n\t\t\tself.update()?;\n\t\t}\n\t\t//TODO: make this a queue event?\n\t\t//NOTE: set when any tree component changed selection\n\t\tif flags.contains(NeedsUpdate::DIFF) {\n\t\t\tself.status_tab.update_diff()?;\n\t\t\tself.inspect_commit_popup.update_diff()?;\n\t\t\tself.compare_commits_popup.update_diff()?;\n\t\t\tself.file_revlog_popup.update_diff()?;\n\t\t}\n\t\tif flags.contains(NeedsUpdate::COMMANDS) {\n\t\t\tself.update_commands();\n\t\t}\n\t\tif flags.contains(NeedsUpdate::BRANCHES) {\n\t\t\tself.select_branch_popup.update_branches()?;\n\t\t}\n\t\tif flags.contains(NeedsUpdate::REMOTES) {\n\t\t\tself.remotes_popup.update_remotes()?;\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn open_popup(\n\t\t&mut self,\n\t\tpopup: StackablePopupOpen,\n\t) -> Result<()> {\n\t\tmatch popup {\n\t\t\tStackablePopupOpen::BlameFile(params) => {\n\t\t\t\tself.blame_file_popup.open(params)?;\n\t\t\t}\n\t\t\tStackablePopupOpen::FileRevlog(param) => {\n\t\t\t\tself.file_revlog_popup.open(param)?;\n\t\t\t}\n\t\t\tStackablePopupOpen::FileTree(param) => {\n\t\t\t\tself.revision_files_popup.open(param)?;\n\t\t\t}\n\t\t\tStackablePopupOpen::InspectCommit(param) => {\n\t\t\t\tself.inspect_commit_popup.open(param)?;\n\t\t\t}\n\t\t\tStackablePopupOpen::CompareCommits(param) => {\n\t\t\t\tself.compare_commits_popup.open(param)?;\n\t\t\t}\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn process_internal_events(&mut self) -> Result<NeedsUpdate> {\n\t\tlet mut flags = NeedsUpdate::empty();\n\n\t\tloop {\n\t\t\tlet front = self.queue.pop();\n\t\t\tif let Some(e) = front {\n\t\t\t\tflags.insert(self.process_internal_event(e)?);\n\t\t\t} else {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tself.queue.clear();\n\n\t\tOk(flags)\n\t}\n\n\t#[allow(clippy::too_many_lines)]\n\tfn process_internal_event(\n\t\t&mut self,\n\t\tev: InternalEvent,\n\t) -> Result<NeedsUpdate> {\n\t\tlet mut flags = NeedsUpdate::empty();\n\t\tmatch ev {\n\t\t\tInternalEvent::ConfirmedAction(action) => {\n\t\t\t\tself.process_confirmed_action(action, &mut flags)?;\n\t\t\t}\n\t\t\tInternalEvent::ConfirmAction(action) => {\n\t\t\t\tself.confirm_popup.open(action)?;\n\t\t\t\tflags.insert(NeedsUpdate::COMMANDS);\n\t\t\t}\n\t\t\tInternalEvent::ShowErrorMsg(msg) => {\n\t\t\t\tself.msg_popup.show_error(msg.as_str())?;\n\t\t\t\tflags\n\t\t\t\t\t.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS);\n\t\t\t}\n\t\t\tInternalEvent::ShowInfoMsg(msg) => {\n\t\t\t\tself.msg_popup.show_info(msg.as_str())?;\n\t\t\t\tflags\n\t\t\t\t\t.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS);\n\t\t\t}\n\t\t\tInternalEvent::Update(u) => flags.insert(u),\n\t\t\tInternalEvent::OpenCommit => self.commit_popup.show()?,\n\t\t\tInternalEvent::RewordCommit(id) => {\n\t\t\t\tself.commit_popup.open(Some(id))?;\n\t\t\t}\n\t\t\tInternalEvent::PopupStashing(opts) => {\n\t\t\t\tself.stashmsg_popup.options(opts);\n\t\t\t\tself.stashmsg_popup.show()?;\n\t\t\t}\n\t\t\tInternalEvent::TagCommit(id) => {\n\t\t\t\tself.tag_commit_popup.open(id)?;\n\t\t\t}\n\t\t\tInternalEvent::CreateRemote => {\n\t\t\t\tself.create_remote_popup.open()?;\n\t\t\t}\n\t\t\tInternalEvent::RenameRemote(cur_name) => {\n\t\t\t\tself.rename_remote_popup.open(cur_name)?;\n\t\t\t}\n\t\t\tInternalEvent::UpdateRemoteUrl(remote_name, cur_url) => {\n\t\t\t\tself.update_remote_url_popup\n\t\t\t\t\t.open(remote_name, cur_url)?;\n\t\t\t}\n\t\t\tInternalEvent::ViewRemotes => {\n\t\t\t\tself.remotes_popup.open()?;\n\t\t\t}\n\t\t\tInternalEvent::CreateBranch => {\n\t\t\t\tself.create_branch_popup.open()?;\n\t\t\t}\n\t\t\tInternalEvent::RenameBranch(branch_ref, cur_name) => {\n\t\t\t\tself.rename_branch_popup\n\t\t\t\t\t.open(branch_ref, cur_name)?;\n\t\t\t}\n\t\t\tInternalEvent::SelectBranch => {\n\t\t\t\tself.select_branch_popup.open()?;\n\t\t\t}\n\t\t\tInternalEvent::ViewSubmodules => {\n\t\t\t\tself.submodule_popup.open()?;\n\t\t\t}\n\t\t\tInternalEvent::Tags => {\n\t\t\t\tself.tags_popup.open()?;\n\t\t\t}\n\t\t\tInternalEvent::TabSwitchStatus => self.set_tab(0)?,\n\t\t\tInternalEvent::TabSwitch(tab) => {\n\t\t\t\tself.switch_to_tab(&tab)?;\n\t\t\t\tflags.insert(NeedsUpdate::ALL);\n\t\t\t}\n\t\t\tInternalEvent::SelectCommitInRevlog(id) => {\n\t\t\t\tif let Err(error) = self.revlog.select_commit(id) {\n\t\t\t\t\tself.queue.push(InternalEvent::ShowErrorMsg(\n\t\t\t\t\t\terror.to_string(),\n\t\t\t\t\t));\n\t\t\t\t} else {\n\t\t\t\t\tself.tags_popup.hide();\n\t\t\t\t\tflags.insert(NeedsUpdate::ALL);\n\t\t\t\t}\n\t\t\t}\n\t\t\tInternalEvent::OpenExternalEditor(path) => {\n\t\t\t\tself.input.set_polling(false);\n\t\t\t\tself.external_editor_popup.show()?;\n\t\t\t\tself.file_to_open = path;\n\t\t\t\tflags.insert(NeedsUpdate::COMMANDS);\n\t\t\t}\n\t\t\tInternalEvent::Push(branch, push_type, force, delete) => {\n\t\t\t\tself.push_popup\n\t\t\t\t\t.push(branch, push_type, force, delete)?;\n\t\t\t\tflags.insert(NeedsUpdate::ALL);\n\t\t\t}\n\t\t\tInternalEvent::Pull(branch) => {\n\t\t\t\tif let Err(error) = self.pull_popup.fetch(branch) {\n\t\t\t\t\tself.queue.push(InternalEvent::ShowErrorMsg(\n\t\t\t\t\t\terror.to_string(),\n\t\t\t\t\t));\n\t\t\t\t}\n\t\t\t\tflags.insert(NeedsUpdate::ALL);\n\t\t\t}\n\t\t\tInternalEvent::FetchRemotes => {\n\t\t\t\tif let Err(error) = self.fetch_popup.fetch() {\n\t\t\t\t\tself.queue.push(InternalEvent::ShowErrorMsg(\n\t\t\t\t\t\terror.to_string(),\n\t\t\t\t\t));\n\t\t\t\t}\n\t\t\t\tflags.insert(NeedsUpdate::ALL);\n\t\t\t}\n\t\t\tInternalEvent::PushTags => {\n\t\t\t\tself.push_tags_popup.push_tags()?;\n\t\t\t\tflags.insert(NeedsUpdate::ALL);\n\t\t\t}\n\t\t\tInternalEvent::StatusLastFileMoved => {\n\t\t\t\tself.status_tab.last_file_moved()?;\n\t\t\t}\n\t\t\tInternalEvent::OpenFuzzyFinder(contents, target) => {\n\t\t\t\tself.fuzzy_find_popup.open(contents, target)?;\n\t\t\t\tflags\n\t\t\t\t\t.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS);\n\t\t\t}\n\t\t\tInternalEvent::OpenLogSearchPopup => {\n\t\t\t\tself.log_search_popup.open()?;\n\t\t\t\tflags\n\t\t\t\t\t.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS);\n\t\t\t}\n\t\t\tInternalEvent::OptionSwitched(o) => {\n\t\t\t\tmatch o {\n\t\t\t\t\tAppOption::StatusShowUntracked => {\n\t\t\t\t\t\tself.status_tab.update()?;\n\t\t\t\t\t}\n\t\t\t\t\tAppOption::DiffContextLines\n\t\t\t\t\t| AppOption::DiffIgnoreWhitespaces\n\t\t\t\t\t| AppOption::DiffInterhunkLines => {\n\t\t\t\t\t\tself.status_tab.update_diff()?;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tflags.insert(NeedsUpdate::ALL);\n\t\t\t}\n\t\t\tInternalEvent::FuzzyFinderChanged(\n\t\t\t\tidx,\n\t\t\t\tcontent,\n\t\t\t\ttarget,\n\t\t\t) => {\n\t\t\t\tmatch target {\n\t\t\t\t\tFuzzyFinderTarget::Branches => self\n\t\t\t\t\t\t.select_branch_popup\n\t\t\t\t\t\t.branch_finder_update(idx)?,\n\t\t\t\t\tFuzzyFinderTarget::Files => {\n\t\t\t\t\t\tself.files_tab.file_finder_update(\n\t\t\t\t\t\t\t&PathBuf::from(content.clone()),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tself.revision_files_popup.file_finder_update(\n\t\t\t\t\t\t\t&PathBuf::from(content),\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tflags\n\t\t\t\t\t.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS);\n\t\t\t}\n\t\t\tInternalEvent::OpenPopup(popup) => {\n\t\t\t\tself.open_popup(popup)?;\n\t\t\t\tflags\n\t\t\t\t\t.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS);\n\t\t\t}\n\t\t\tInternalEvent::PopupStackPop => {\n\t\t\t\tif let Some(popup) = self.popup_stack.pop() {\n\t\t\t\t\tself.open_popup(popup)?;\n\t\t\t\t\tflags.insert(\n\t\t\t\t\t\tNeedsUpdate::ALL | NeedsUpdate::COMMANDS,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\tInternalEvent::PopupStackPush(popup) => {\n\t\t\t\tself.popup_stack.push(popup);\n\t\t\t\tflags\n\t\t\t\t\t.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS);\n\t\t\t}\n\t\t\tInternalEvent::OpenRepo { path } => {\n\t\t\t\tlet submodule_repo_path = RepoPath::Path(\n\t\t\t\t\tPath::new(&repo_work_dir(&self.repo.borrow())?)\n\t\t\t\t\t\t.join(path),\n\t\t\t\t);\n\t\t\t\t//TODO: validate this is a valid repo first, so we can show proper error otherwise\n\t\t\t\tself.do_quit =\n\t\t\t\t\tQuitState::OpenSubmodule(submodule_repo_path);\n\t\t\t}\n\t\t\tInternalEvent::OpenResetPopup(id) => {\n\t\t\t\tself.reset_popup.open(id)?;\n\t\t\t}\n\t\t\tInternalEvent::CommitSearch(options) => {\n\t\t\t\tself.revlog.search(options);\n\t\t\t}\n\t\t\tInternalEvent::OpenGotoLinePopup(max_line) => {\n\t\t\t\tself.goto_line_popup.open(max_line);\n\t\t\t}\n\t\t\tInternalEvent::GotoLine(line) => {\n\t\t\t\tif self.blame_file_popup.is_visible() {\n\t\t\t\t\tself.blame_file_popup.goto_line(line);\n\t\t\t\t}\n\t\t\t}\n\t\t\tInternalEvent::CheckoutOption(branch) => {\n\t\t\t\tself.checkout_option_popup.open(branch)?;\n\t\t\t}\n\t\t}\n\n\t\tOk(flags)\n\t}\n\n\tfn process_confirmed_action(\n\t\t&mut self,\n\t\taction: Action,\n\t\tflags: &mut NeedsUpdate,\n\t) -> Result<()> {\n\t\tmatch action {\n\t\t\tAction::Reset(r) => {\n\t\t\t\tself.status_tab.reset(&r);\n\t\t\t}\n\t\t\tAction::StashDrop(_) | Action::StashPop(_) => {\n\t\t\t\tif let Err(e) = self\n\t\t\t\t\t.stashlist_tab\n\t\t\t\t\t.action_confirmed(&self.repo.borrow(), &action)\n\t\t\t\t{\n\t\t\t\t\tself.queue.push(InternalEvent::ShowErrorMsg(\n\t\t\t\t\t\te.to_string(),\n\t\t\t\t\t));\n\t\t\t\t}\n\t\t\t}\n\t\t\tAction::ResetHunk(path, hash) => {\n\t\t\t\tsync::reset_hunk(\n\t\t\t\t\t&self.repo.borrow(),\n\t\t\t\t\t&path,\n\t\t\t\t\thash,\n\t\t\t\t\tSome(self.options.borrow().diff_options()),\n\t\t\t\t)?;\n\t\t\t}\n\t\t\tAction::ResetLines(path, lines) => {\n\t\t\t\tsync::discard_lines(\n\t\t\t\t\t&self.repo.borrow(),\n\t\t\t\t\t&path,\n\t\t\t\t\t&lines,\n\t\t\t\t)?;\n\t\t\t}\n\t\t\tAction::DeleteLocalBranch(branch_ref) => {\n\t\t\t\tif let Err(e) = sync::delete_branch(\n\t\t\t\t\t&self.repo.borrow(),\n\t\t\t\t\t&branch_ref,\n\t\t\t\t) {\n\t\t\t\t\tself.queue.push(InternalEvent::ShowErrorMsg(\n\t\t\t\t\t\te.to_string(),\n\t\t\t\t\t));\n\t\t\t\t}\n\n\t\t\t\tself.select_branch_popup.update_branches()?;\n\t\t\t}\n\t\t\tAction::DeleteRemoteBranch(branch_ref) => {\n\t\t\t\tself.delete_remote_branch(&branch_ref)?;\n\t\t\t}\n\t\t\tAction::DeleteRemote(remote_name) => {\n\t\t\t\tself.delete_remote(&remote_name);\n\t\t\t}\n\t\t\tAction::DeleteTag(tag_name) => {\n\t\t\t\tself.delete_tag(tag_name)?;\n\t\t\t}\n\t\t\tAction::DeleteRemoteTag(tag_name, _remote) => {\n\t\t\t\tself.queue.push(InternalEvent::Push(\n\t\t\t\t\ttag_name,\n\t\t\t\t\tPushType::Tag,\n\t\t\t\t\tfalse,\n\t\t\t\t\ttrue,\n\t\t\t\t));\n\t\t\t}\n\t\t\tAction::ForcePush(branch, force) => {\n\t\t\t\tself.queue.push(InternalEvent::Push(\n\t\t\t\t\tbranch,\n\t\t\t\t\tPushType::Branch,\n\t\t\t\t\tforce,\n\t\t\t\t\tfalse,\n\t\t\t\t));\n\t\t\t}\n\t\t\tAction::PullMerge { rebase, .. } => {\n\t\t\t\tself.pull_popup.try_conflict_free_merge(rebase);\n\t\t\t}\n\t\t\tAction::AbortRevert | Action::AbortMerge => {\n\t\t\t\tself.status_tab.revert_pending_state();\n\t\t\t}\n\t\t\tAction::AbortRebase => {\n\t\t\t\tself.status_tab.abort_rebase();\n\t\t\t}\n\t\t\tAction::UndoCommit => {\n\t\t\t\ttry_or_popup!(\n\t\t\t\t\tself,\n\t\t\t\t\t\"undo commit failed:\",\n\t\t\t\t\tundo_last_commit(&self.repo.borrow())\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tflags.insert(NeedsUpdate::ALL);\n\n\t\tOk(())\n\t}\n\n\tfn delete_tag(&mut self, tag_name: String) -> Result<()> {\n\t\tif let Err(error) =\n\t\t\tsync::delete_tag(&self.repo.borrow(), &tag_name)\n\t\t{\n\t\t\tself.queue\n\t\t\t\t.push(InternalEvent::ShowErrorMsg(error.to_string()));\n\t\t} else {\n\t\t\tlet remote =\n\t\t\t\tsync::get_default_remote(&self.repo.borrow())?;\n\n\t\t\tself.queue.push(InternalEvent::ConfirmAction(\n\t\t\t\tAction::DeleteRemoteTag(tag_name, remote),\n\t\t\t));\n\n\t\t\tself.tags_popup.update_tags()?;\n\t\t}\n\t\tOk(())\n\t}\n\n\tfn delete_remote_branch(\n\t\t&mut self,\n\t\tbranch_ref: &str,\n\t) -> Result<()> {\n\t\tself.queue.push(\n\t\t\t//TODO: check if this is correct based on the fix in `c6abbaf`\n\t\t\tbranch_ref.rsplit('/').next().map_or_else(\n\t\t\t\t|| {\n\t\t\t\t\tInternalEvent::ShowErrorMsg(format!(\n\t\t\t\t\t\t    \"Failed to find the branch name in {branch_ref}\"\n\t\t\t\t\t    ))\n\t\t\t\t},\n\t\t\t\t|name| {\n\t\t\t\t\tInternalEvent::Push(\n\t\t\t\t\t\tname.to_string(),\n\t\t\t\t\t\tPushType::Branch,\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t\ttrue,\n\t\t\t\t\t)\n\t\t\t\t},\n\t\t\t),\n\t\t);\n\n\t\tself.select_branch_popup.update_branches()?;\n\n\t\tOk(())\n\t}\n\n\tfn delete_remote(&self, remote_name: &str) {\n\t\tlet res =\n\t\t\tsync::delete_remote(&self.repo.borrow(), remote_name);\n\t\tmatch res {\n\t\t\tOk(()) => {\n\t\t\t\tself.queue.push(InternalEvent::Update(\n\t\t\t\t\tNeedsUpdate::ALL | NeedsUpdate::REMOTES,\n\t\t\t\t));\n\t\t\t}\n\t\t\tErr(e) => {\n\t\t\t\tlog::error!(\"delete remote: {e:?}\");\n\t\t\t\tself.queue.push(InternalEvent::ShowErrorMsg(\n\t\t\t\t\tformat!(\"delete remote error:\\n{e}\"),\n\t\t\t\t));\n\t\t\t}\n\t\t}\n\t}\n\n\tfn commands(&self, force_all: bool) -> Vec<CommandInfo> {\n\t\tlet mut res = Vec::new();\n\n\t\tcommand_pump(&mut res, force_all, &self.components());\n\n\t\tres.push(CommandInfo::new(\n\t\t\tstrings::commands::find_file(&self.key_config),\n\t\t\t!self.fuzzy_find_popup.is_visible(),\n\t\t\t(!self.any_popup_visible()\n\t\t\t\t&& self.files_tab.is_visible())\n\t\t\t\t|| self.revision_files_popup.is_visible()\n\t\t\t\t|| force_all,\n\t\t));\n\n\t\tres.push(\n\t\t\tCommandInfo::new(\n\t\t\t\tstrings::commands::toggle_tabs(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\t!self.any_popup_visible(),\n\t\t\t)\n\t\t\t.order(order::NAV),\n\t\t);\n\t\tres.push(\n\t\t\tCommandInfo::new(\n\t\t\t\tstrings::commands::toggle_tabs_direct(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t),\n\t\t\t\ttrue,\n\t\t\t\t!self.any_popup_visible(),\n\t\t\t)\n\t\t\t.order(order::NAV),\n\t\t);\n\t\tres.push(\n\t\t\tCommandInfo::new(\n\t\t\t\tstrings::commands::options_popup(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\t!self.any_popup_visible(),\n\t\t\t)\n\t\t\t.order(order::NAV),\n\t\t);\n\n\t\tres.push(\n\t\t\tCommandInfo::new(\n\t\t\t\tstrings::commands::quit(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\t!self.any_popup_visible(),\n\t\t\t)\n\t\t\t.order(100),\n\t\t);\n\n\t\tres\n\t}\n\n\t//TODO: make this dynamic\n\tfn draw_top_bar(&self, f: &mut Frame, r: Rect) {\n\t\tconst DIVIDER_PAD_SPACES: usize = 2;\n\t\tconst SIDE_PADS: usize = 2;\n\t\tconst MARGIN_LEFT_AND_RIGHT: usize = 2;\n\n\t\tlet r = r.inner(Margin {\n\t\t\tvertical: 0,\n\t\t\thorizontal: 1,\n\t\t});\n\n\t\tlet tab_labels = [\n\t\t\tSpan::raw(strings::tab_status(&self.key_config)),\n\t\t\tSpan::raw(strings::tab_log(&self.key_config)),\n\t\t\tSpan::raw(strings::tab_files(&self.key_config)),\n\t\t\tSpan::raw(strings::tab_stashing(&self.key_config)),\n\t\t\tSpan::raw(strings::tab_stashes(&self.key_config)),\n\t\t];\n\t\tlet divider = strings::tab_divider(&self.key_config);\n\n\t\t// heuristic, since tui doesn't provide a way to know\n\t\t// how much space is needed to draw a `Tabs`\n\t\tlet tabs_len: usize =\n\t\t\ttab_labels.iter().map(Span::width).sum::<usize>()\n\t\t\t\t+ tab_labels.len().saturating_sub(1)\n\t\t\t\t\t* (divider.width() + DIVIDER_PAD_SPACES)\n\t\t\t\t+ SIDE_PADS + MARGIN_LEFT_AND_RIGHT;\n\n\t\tlet left_right = Layout::default()\n\t\t\t.direction(Direction::Horizontal)\n\t\t\t.constraints(vec![\n\t\t\t\tConstraint::Length(\n\t\t\t\t\tu16::try_from(tabs_len).unwrap_or(r.width),\n\t\t\t\t),\n\t\t\t\tConstraint::Min(0),\n\t\t\t])\n\t\t\t.split(r);\n\n\t\tlet table_area = r; // use entire area to allow drawing the horizontal separator line\n\t\tlet text_area = left_right[1];\n\n\t\tlet tabs: Vec<Line> =\n\t\t\ttab_labels.into_iter().map(Line::from).collect();\n\n\t\tf.render_widget(\n\t\t\tTabs::new(tabs)\n\t\t\t\t.block(\n\t\t\t\t\tBlock::default()\n\t\t\t\t\t\t.borders(Borders::BOTTOM)\n\t\t\t\t\t\t.border_style(self.theme.block(false)),\n\t\t\t\t)\n\t\t\t\t.style(self.theme.tab(false))\n\t\t\t\t.highlight_style(self.theme.tab(true))\n\t\t\t\t.divider(divider)\n\t\t\t\t.select(self.tab),\n\t\t\ttable_area,\n\t\t);\n\n\t\tf.render_widget(\n\t\t\tParagraph::new(Line::from(vec![Span::styled(\n\t\t\t\tellipsis_trim_start(\n\t\t\t\t\t&self.repo_path_text,\n\t\t\t\t\ttext_area.width as usize,\n\t\t\t\t),\n\t\t\t\tself.theme.title(false),\n\t\t\t)]))\n\t\t\t.alignment(Alignment::Right),\n\t\t\ttext_area,\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "src/args.rs",
    "content": "use crate::bug_report;\nuse anyhow::{anyhow, Context, Result};\nuse asyncgit::sync::RepoPath;\nuse clap::{\n\tbuilder::ArgPredicate, crate_authors, crate_description,\n\tcrate_name, Arg, Command as ClapApp,\n};\nuse simplelog::{Config, LevelFilter, WriteLogger};\nuse std::{\n\tenv,\n\tfs::{self, File},\n\tpath::PathBuf,\n};\n\nconst BUG_REPORT_FLAG_ID: &str = \"bugreport\";\nconst LOG_FILE_FLAG_ID: &str = \"logfile\";\nconst LOGGING_FLAG_ID: &str = \"logging\";\nconst THEME_FLAG_ID: &str = \"theme\";\nconst WORKDIR_FLAG_ID: &str = \"workdir\";\nconst FILE_FLAG_ID: &str = \"file\";\nconst GIT_DIR_FLAG_ID: &str = \"directory\";\nconst WATCHER_FLAG_ID: &str = \"watcher\";\nconst KEY_BINDINGS_FLAG_ID: &str = \"key_bindings\";\nconst KEY_SYMBOLS_FLAG_ID: &str = \"key_symbols\";\nconst DEFAULT_THEME: &str = \"theme.ron\";\nconst DEFAULT_GIT_DIR: &str = \".\";\n\n#[derive(Clone)]\npub struct CliArgs {\n\tpub theme: PathBuf,\n\tpub select_file: Option<PathBuf>,\n\tpub repo_path: RepoPath,\n\tpub notify_watcher: bool,\n\tpub key_bindings_path: Option<PathBuf>,\n\tpub key_symbols_path: Option<PathBuf>,\n}\n\npub fn process_cmdline() -> Result<CliArgs> {\n\tlet app = app();\n\n\tlet arg_matches = app.get_matches();\n\n\tif arg_matches.get_flag(BUG_REPORT_FLAG_ID) {\n\t\tbug_report::generate_bugreport();\n\t\tstd::process::exit(0);\n\t}\n\tif arg_matches.get_flag(LOGGING_FLAG_ID) {\n\t\tlet logfile = arg_matches.get_one::<String>(LOG_FILE_FLAG_ID);\n\t\tsetup_logging(logfile.map(PathBuf::from))?;\n\t}\n\n\tlet workdir = arg_matches\n\t\t.get_one::<String>(WORKDIR_FLAG_ID)\n\t\t.map(PathBuf::from);\n\tlet gitdir =\n\t\targ_matches.get_one::<String>(GIT_DIR_FLAG_ID).map_or_else(\n\t\t\t|| PathBuf::from(DEFAULT_GIT_DIR),\n\t\t\tPathBuf::from,\n\t\t);\n\n\tlet select_file = arg_matches\n\t\t.get_one::<String>(FILE_FLAG_ID)\n\t\t.map(PathBuf::from);\n\n\tlet repo_path = if let Some(w) = workdir {\n\t\tRepoPath::Workdir { gitdir, workdir: w }\n\t} else {\n\t\tRepoPath::Path(gitdir)\n\t};\n\n\tlet arg_theme = arg_matches\n\t\t.get_one::<String>(THEME_FLAG_ID)\n\t\t.map_or_else(|| PathBuf::from(DEFAULT_THEME), PathBuf::from);\n\n\tlet confpath = get_app_config_path()?;\n\tfs::create_dir_all(&confpath).with_context(|| {\n\t\tformat!(\n\t\t\t\"failed to create config directory: {}\",\n\t\t\tconfpath.display()\n\t\t)\n\t})?;\n\tlet theme = confpath.join(arg_theme);\n\n\tlet notify_watcher: bool =\n\t\t*arg_matches.get_one(WATCHER_FLAG_ID).unwrap_or(&false);\n\n\tlet key_bindings_path = arg_matches\n\t\t.get_one::<String>(KEY_BINDINGS_FLAG_ID)\n\t\t.map(PathBuf::from);\n\n\tlet key_symbols_path = arg_matches\n\t\t.get_one::<String>(KEY_SYMBOLS_FLAG_ID)\n\t\t.map(PathBuf::from);\n\n\tOk(CliArgs {\n\t\ttheme,\n\t\tselect_file,\n\t\trepo_path,\n\t\tnotify_watcher,\n\t\tkey_bindings_path,\n\t\tkey_symbols_path,\n\t})\n}\n\nfn app() -> ClapApp {\n\tClapApp::new(crate_name!())\n\t\t.author(crate_authors!())\n\t\t.version(env!(\"GITUI_BUILD_NAME\"))\n\t\t.about(crate_description!())\n\t\t.help_template(\n\t\t\t\"\\\n{before-help}gitui {version}\n{author}\n{about}\n\n{usage-heading} {usage}\n\n{all-args}{after-help}\n\t\t\",\n\t\t)\n\t\t\t.arg(\n\t\t\tArg::new(KEY_BINDINGS_FLAG_ID)\n\t\t\t\t.help(\"Use a custom keybindings file\")\n\t\t\t\t.short('k')\n\t\t\t\t.long(\"key-bindings\")\n\t\t\t\t.value_name(\"KEY_LIST_FILENAME\")\n\t\t\t\t.num_args(1),\n\t\t)\n\t\t\t.arg(\n\t\t\tArg::new(KEY_SYMBOLS_FLAG_ID)\n\t\t\t\t.help(\"Use a custom symbols file\")\n\t\t\t\t.short('s')\n\t\t\t\t.long(\"key-symbols\")\n\t\t\t\t.value_name(\"KEY_SYMBOLS_FILENAME\")\n\t\t\t\t.num_args(1),\n\t\t)\n\t\t.arg(\n\t\t\tArg::new(THEME_FLAG_ID)\n\t\t\t\t.help(\"Set color theme filename loaded from config directory\")\n\t\t\t\t.short('t')\n\t\t\t\t.long(\"theme\")\n\t\t\t\t.value_name(\"THEME_FILE\")\n\t\t\t\t.default_value(DEFAULT_THEME)\n\t\t\t\t.num_args(1),\n\t\t)\n\t\t.arg(\n\t\t\tArg::new(LOGGING_FLAG_ID)\n\t\t\t\t.help(\"Store logging output into a file (in the cache directory by default)\")\n\t\t\t\t.short('l')\n\t\t\t\t.long(\"logging\")\n                .default_value_if(\"logfile\", ArgPredicate::IsPresent, \"true\")\n\t\t\t\t.action(clap::ArgAction::SetTrue),\n\t\t)\n        .arg(Arg::new(LOG_FILE_FLAG_ID)\n            .help(\"Store logging output into the specified file (implies --logging)\")\n            .long(\"logfile\")\n            .value_name(\"LOG_FILE\"))\n\t\t.arg(\n\t\t\tArg::new(WATCHER_FLAG_ID)\n\t\t\t\t.help(\"Use notify-based file system watcher instead of tick-based update. This is more performant, but can cause issues on some platforms. See https://github.com/gitui-org/gitui/blob/master/FAQ.md#watcher for details.\")\n\t\t\t\t.long(\"watcher\")\n\t\t\t\t.action(clap::ArgAction::SetTrue),\n\t\t)\n\t\t.arg(\n\t\t\tArg::new(BUG_REPORT_FLAG_ID)\n\t\t\t\t.help(\"Generate a bug report\")\n\t\t\t\t.long(\"bugreport\")\n\t\t\t\t.action(clap::ArgAction::SetTrue),\n\t\t)\n\t\t.arg(\n\t\t\tArg::new(FILE_FLAG_ID)\n\t\t\t\t.help(\"Select the file in the file tab\")\n\t\t\t\t.short('f')\n\t\t\t\t.long(\"file\")\n\t\t\t\t.num_args(1),\n\t\t)\n\t\t.arg(\n\t\t\tArg::new(GIT_DIR_FLAG_ID)\n\t\t\t\t.help(\"Set the git directory\")\n\t\t\t\t.short('d')\n\t\t\t\t.long(\"directory\")\n\t\t\t\t.env(\"GIT_DIR\")\n\t\t\t\t.num_args(1),\n\t\t)\n\t\t.arg(\n\t\t\tArg::new(WORKDIR_FLAG_ID)\n\t\t\t\t.help(\"Set the working directory\")\n\t\t\t\t.short('w')\n\t\t\t\t.long(\"workdir\")\n\t\t\t\t.env(\"GIT_WORK_TREE\")\n\t\t\t\t.num_args(1),\n\t\t)\n}\n\nfn setup_logging(path_override: Option<PathBuf>) -> Result<()> {\n\tlet path = if let Some(path) = path_override {\n\t\tpath\n\t} else {\n\t\tlet mut path = get_app_cache_path()?;\n\t\tpath.push(\"gitui.log\");\n\t\tpath\n\t};\n\n\tprintln!(\"Logging enabled. Log written to: {}\", path.display());\n\n\tWriteLogger::init(\n\t\tLevelFilter::Trace,\n\t\tConfig::default(),\n\t\tFile::create(path)?,\n\t)?;\n\n\tOk(())\n}\n\nfn get_app_cache_path() -> Result<PathBuf> {\n\tlet mut path = dirs::cache_dir()\n\t\t.ok_or_else(|| anyhow!(\"failed to find os cache dir.\"))?;\n\n\tpath.push(\"gitui\");\n\tfs::create_dir_all(&path).with_context(|| {\n\t\tformat!(\n\t\t\t\"failed to create cache directory: {}\",\n\t\t\tpath.display()\n\t\t)\n\t})?;\n\tOk(path)\n}\n\npub fn get_app_config_path() -> Result<PathBuf> {\n\tlet mut path = if cfg!(target_os = \"macos\") {\n\t\tdirs::home_dir().map(|h| h.join(\".config\"))\n\t} else {\n\t\tdirs::config_dir()\n\t}\n\t.ok_or_else(|| anyhow!(\"failed to find os config dir.\"))?;\n\n\tpath.push(\"gitui\");\n\tOk(path)\n}\n\n#[test]\nfn verify_app() {\n\tapp().debug_assert();\n}\n"
  },
  {
    "path": "src/bug_report.rs",
    "content": "use bugreport::{\n\tbugreport,\n\tcollector::{\n\t\tCommandLine, CompileTimeInformation, EnvironmentVariables,\n\t\tOperatingSystem, SoftwareVersion,\n\t},\n\tformat::Markdown,\n};\n\npub fn generate_bugreport() {\n\tbugreport!()\n\t\t.info(SoftwareVersion::default())\n\t\t.info(OperatingSystem::default())\n\t\t.info(CompileTimeInformation::default())\n\t\t.info(EnvironmentVariables::list(&[\n\t\t\t\"SHELL\",\n\t\t\t\"EDITOR\",\n\t\t\t\"GIT_EDITOR\",\n\t\t\t\"VISUAL\",\n\t\t]))\n\t\t.info(CommandLine::default())\n\t\t.print::<Markdown>();\n}\n"
  },
  {
    "path": "src/clipboard.rs",
    "content": "use anyhow::{anyhow, Result};\nuse std::io::Write;\nuse std::path::PathBuf;\nuse std::process::{Command, Stdio};\nuse which::which;\n\nfn exec_copy_with_args(\n\tcommand: &str,\n\targs: &[&str],\n\ttext: &str,\n\tpipe_stderr: bool,\n) -> Result<()> {\n\tlet binary = which(command)\n\t\t.ok()\n\t\t.unwrap_or_else(|| PathBuf::from(command));\n\n\tlet mut process = Command::new(binary)\n\t\t.args(args)\n\t\t.stdin(Stdio::piped())\n\t\t.stdout(Stdio::null())\n\t\t.stderr(if pipe_stderr {\n\t\t\tStdio::piped()\n\t\t} else {\n\t\t\tStdio::null()\n\t\t})\n\t\t.spawn()\n\t\t.map_err(|e| anyhow!(\"`{command:?}`: {e:?}\"))?;\n\n\tprocess\n\t\t.stdin\n\t\t.as_mut()\n\t\t.ok_or_else(|| anyhow!(\"`{command:?}`\"))?\n\t\t.write_all(text.as_bytes())\n\t\t.map_err(|e| anyhow!(\"`{command:?}`: {e:?}\"))?;\n\n\tlet out = process\n\t\t.wait_with_output()\n\t\t.map_err(|e| anyhow!(\"`{command:?}`: {e:?}\"))?;\n\n\tif out.status.success() {\n\t\tOk(())\n\t} else {\n\t\tlet msg = if out.stderr.is_empty() {\n\t\t\tformat!(\"{}\", out.status).into()\n\t\t} else {\n\t\t\tString::from_utf8_lossy(&out.stderr)\n\t\t};\n\t\tErr(anyhow!(\"`{command:?}`: {msg}\"))\n\t}\n}\n\n// Implementation taken from https://crates.io/crates/wsl.\n// Using /proc/sys/kernel/osrelease as an authoritative source\n// based on this comment: https://github.com/microsoft/WSL/issues/423#issuecomment-221627364\n#[cfg(all(target_family = \"unix\", not(target_os = \"macos\")))]\nfn is_wsl() -> bool {\n\tif let Ok(b) = std::fs::read(\"/proc/sys/kernel/osrelease\") {\n\t\tif let Ok(s) = std::str::from_utf8(&b) {\n\t\t\tlet a = s.to_ascii_lowercase();\n\t\t\treturn a.contains(\"microsoft\") || a.contains(\"wsl\");\n\t\t}\n\t}\n\tfalse\n}\n\n// Copy text using escape sequence Ps = 5 2.\n// This enables copying even if there is no Wayland or X socket available,\n// e.g. via SSH, as long as it supported by the terminal.\n// See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands\n#[cfg(any(\n\tall(target_family = \"unix\", not(target_os = \"macos\")),\n\ttest\n))]\nfn copy_string_osc52(text: &str, out: &mut impl Write) -> Result<()> {\n\tuse base64::prelude::{Engine, BASE64_STANDARD};\n\tconst OSC52_DESTINATION_CLIPBOARD: char = 'c';\n\twrite!(\n\t\tout,\n\t\t\"\\x1b]52;{destination};{encoded_text}\\x07\",\n\t\tdestination = OSC52_DESTINATION_CLIPBOARD,\n\t\tencoded_text = BASE64_STANDARD.encode(text)\n\t)?;\n\tOk(())\n}\n\n#[cfg(all(target_family = \"unix\", not(target_os = \"macos\")))]\nfn copy_string_wayland(text: &str) -> Result<()> {\n\tif exec_copy_with_args(\"wl-copy\", &[], text, false).is_ok() {\n\t\treturn Ok(());\n\t}\n\n\tcopy_string_osc52(text, &mut std::io::stdout())\n}\n\n#[cfg(all(target_family = \"unix\", not(target_os = \"macos\")))]\nfn copy_string_x(text: &str) -> Result<()> {\n\tif exec_copy_with_args(\n\t\t\"xclip\",\n\t\t&[\"-selection\", \"clipboard\"],\n\t\ttext,\n\t\tfalse,\n\t)\n\t.is_ok()\n\t{\n\t\treturn Ok(());\n\t}\n\n\tif exec_copy_with_args(\"xsel\", &[\"--clipboard\"], text, true)\n\t\t.is_ok()\n\t{\n\t\treturn Ok(());\n\t}\n\n\tcopy_string_osc52(text, &mut std::io::stdout())\n}\n\n#[cfg(all(target_family = \"unix\", not(target_os = \"macos\")))]\npub fn copy_string(text: &str) -> Result<()> {\n\tif std::env::var(\"WAYLAND_DISPLAY\").is_ok() {\n\t\treturn copy_string_wayland(text);\n\t}\n\n\tif is_wsl() {\n\t\treturn exec_copy_with_args(\"clip.exe\", &[], text, false);\n\t}\n\n\tcopy_string_x(text)\n}\n\n#[cfg(any(target_os = \"macos\", windows))]\nfn exec_copy(command: &str, text: &str) -> Result<()> {\n\texec_copy_with_args(command, &[], text, true)\n}\n\n#[cfg(target_os = \"macos\")]\npub fn copy_string(text: &str) -> Result<()> {\n\texec_copy(\"pbcopy\", text)\n}\n\n#[cfg(windows)]\npub fn copy_string(text: &str) -> Result<()> {\n\texec_copy(\"clip\", text)\n}\n\n#[cfg(test)]\nmod tests {\n\t#[test]\n\tfn test_copy_string_osc52() {\n\t\tlet mut buffer = Vec::<u8>::new();\n\t\t{\n\t\t\tlet mut cursor = std::io::Cursor::new(&mut buffer);\n\t\t\tsuper::copy_string_osc52(\"foo\", &mut cursor).unwrap();\n\t\t}\n\t\tlet output = String::from_utf8(buffer).unwrap();\n\t\tassert_eq!(output, \"\\x1b]52;c;Zm9v\\x07\");\n\t}\n}\n"
  },
  {
    "path": "src/cmdbar.rs",
    "content": "use crate::{\n\tcomponents::CommandInfo, keys::SharedKeyConfig, strings,\n\tui::style::SharedTheme,\n};\nuse ratatui::{\n\tlayout::{Alignment, Rect},\n\ttext::{Line, Span},\n\twidgets::Paragraph,\n\tFrame,\n};\nuse std::borrow::Cow;\nuse unicode_width::UnicodeWidthStr;\n\nenum DrawListEntry {\n\tLineBreak,\n\tSplitter,\n\tCommand(Command),\n}\n\nstruct Command {\n\ttxt: String,\n\tenabled: bool,\n}\n\n/// helper to be used while drawing\npub struct CommandBar {\n\tdraw_list: Vec<DrawListEntry>,\n\tcmd_infos: Vec<CommandInfo>,\n\ttheme: SharedTheme,\n\tkey_config: SharedKeyConfig,\n\tlines: u16,\n\twidth: u16,\n\texpandable: bool,\n\texpanded: bool,\n}\n\nconst MORE_WIDTH: u16 = 9;\n\nimpl CommandBar {\n\tpub const fn new(\n\t\ttheme: SharedTheme,\n\t\tkey_config: SharedKeyConfig,\n\t) -> Self {\n\t\tSelf {\n\t\t\tdraw_list: Vec::new(),\n\t\t\tcmd_infos: Vec::new(),\n\t\t\ttheme,\n\t\t\tkey_config,\n\t\t\tlines: 0,\n\t\t\twidth: 0,\n\t\t\texpandable: false,\n\t\t\texpanded: false,\n\t\t}\n\t}\n\n\tpub fn refresh_width(&mut self, width: u16) {\n\t\tif width != self.width {\n\t\t\tself.refresh_list(width);\n\t\t\tself.width = width;\n\t\t}\n\t}\n\n\tfn is_multiline(&self, width: u16) -> bool {\n\t\tlet mut line_width = 0_usize;\n\t\tfor c in &self.cmd_infos {\n\t\t\tlet entry_w =\n\t\t\t\tUnicodeWidthStr::width(c.text.name.as_str());\n\n\t\t\tif line_width + entry_w > width as usize {\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tline_width += entry_w + 1;\n\t\t}\n\n\t\tfalse\n\t}\n\n\tfn refresh_list(&mut self, width: u16) {\n\t\tself.draw_list.clear();\n\n\t\tlet width = if self.is_multiline(width) {\n\t\t\twidth.saturating_sub(MORE_WIDTH)\n\t\t} else {\n\t\t\twidth\n\t\t};\n\n\t\tlet mut line_width = 0_usize;\n\t\tlet mut lines = 1_u16;\n\n\t\tfor c in &self.cmd_infos {\n\t\t\tlet entry_w =\n\t\t\t\tUnicodeWidthStr::width(c.text.name.as_str());\n\n\t\t\tif line_width + entry_w > width as usize {\n\t\t\t\tself.draw_list.push(DrawListEntry::LineBreak);\n\t\t\t\tline_width = 0;\n\t\t\t\tlines += 1;\n\t\t\t} else if line_width > 0 {\n\t\t\t\tself.draw_list.push(DrawListEntry::Splitter);\n\t\t\t}\n\n\t\t\tline_width += entry_w + 1;\n\n\t\t\tself.draw_list.push(DrawListEntry::Command(Command {\n\t\t\t\ttxt: c.text.name.clone(),\n\t\t\t\tenabled: c.enabled,\n\t\t\t}));\n\t\t}\n\n\t\tself.expandable = lines > 1;\n\n\t\tself.lines = lines;\n\t}\n\n\tpub fn set_cmds(&mut self, cmds: Vec<CommandInfo>) {\n\t\tself.cmd_infos = cmds\n\t\t\t.into_iter()\n\t\t\t.filter(CommandInfo::show_in_quickbar)\n\t\t\t.collect::<Vec<_>>();\n\t\tself.cmd_infos.sort_by_key(|e| e.order);\n\t\tself.refresh_list(self.width);\n\t}\n\n\tpub const fn height(&self) -> u16 {\n\t\tif self.expandable && self.expanded {\n\t\t\tself.lines\n\t\t} else {\n\t\t\t1_u16\n\t\t}\n\t}\n\n\tpub const fn toggle_more(&mut self) {\n\t\tif self.expandable {\n\t\t\tself.expanded = !self.expanded;\n\t\t}\n\t}\n\n\tpub fn draw(&self, f: &mut Frame, r: Rect) {\n\t\tif r.width < MORE_WIDTH {\n\t\t\treturn;\n\t\t}\n\t\tlet splitter = Span::raw(Cow::from(strings::cmd_splitter(\n\t\t\t&self.key_config,\n\t\t)));\n\n\t\tlet texts = self\n\t\t\t.draw_list\n\t\t\t.split(|c| matches!(c, DrawListEntry::LineBreak))\n\t\t\t.map(|c_arr| {\n\t\t\t\tLine::from(\n\t\t\t\t\tc_arr\n\t\t\t\t\t\t.iter()\n\t\t\t\t\t\t.map(|c| match c {\n\t\t\t\t\t\t\tDrawListEntry::Command(c) => {\n\t\t\t\t\t\t\t\tSpan::styled(\n\t\t\t\t\t\t\t\t\tCow::from(c.txt.as_str()),\n\t\t\t\t\t\t\t\t\tself.theme.commandbar(c.enabled),\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tDrawListEntry::LineBreak => {\n\t\t\t\t\t\t\t\t// Doesn't exist in split array\n\t\t\t\t\t\t\t\tSpan::raw(\"\")\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tDrawListEntry::Splitter => {\n\t\t\t\t\t\t\t\tsplitter.clone()\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.collect::<Vec<Span>>(),\n\t\t\t\t)\n\t\t\t})\n\t\t\t.collect::<Vec<Line>>();\n\n\t\tf.render_widget(\n\t\t\tParagraph::new(texts).alignment(Alignment::Left),\n\t\t\tr,\n\t\t);\n\n\t\tif self.expandable {\n\t\t\tlet r = Rect::new(\n\t\t\t\tr.width.saturating_sub(MORE_WIDTH),\n\t\t\t\tr.y + r.height.saturating_sub(1),\n\t\t\t\tMORE_WIDTH.min(r.width),\n\t\t\t\t1.min(r.height),\n\t\t\t);\n\n\t\t\tf.render_widget(\n\t\t\t\tParagraph::new(Line::from(vec![Span::raw(\n\t\t\t\t\tCow::from(if self.expanded {\n\t\t\t\t\t\t\"less [.]\"\n\t\t\t\t\t} else {\n\t\t\t\t\t\t\"more [.]\"\n\t\t\t\t\t}),\n\t\t\t\t)]))\n\t\t\t\t.alignment(Alignment::Right),\n\t\t\t\tr,\n\t\t\t);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/components/changes.rs",
    "content": "use super::{\n\tstatus_tree::StatusTreeComponent,\n\tutils::filetree::{FileTreeItem, FileTreeItemKind},\n\tCommandBlocking, DrawableComponent,\n};\nuse crate::{\n\tapp::Environment,\n\tcomponents::{CommandInfo, Component, EventState},\n\tkeys::{key_match, SharedKeyConfig},\n\toptions::SharedOptions,\n\tqueue::{Action, InternalEvent, NeedsUpdate, Queue, ResetItem},\n\tstrings, try_or_popup,\n};\nuse anyhow::Result;\nuse asyncgit::{\n\tsync::{self, RepoPathRef},\n\tStatusItem, StatusItemType,\n};\nuse crossterm::event::Event;\nuse ratatui::{layout::Rect, Frame};\nuse std::path::Path;\n\n///\npub struct ChangesComponent {\n\trepo: RepoPathRef,\n\tfiles: StatusTreeComponent,\n\tis_working_dir: bool,\n\tqueue: Queue,\n\tkey_config: SharedKeyConfig,\n\toptions: SharedOptions,\n}\n\nimpl ChangesComponent {\n\t///\n\tpub fn new(\n\t\tenv: &Environment,\n\t\ttitle: &str,\n\t\tfocus: bool,\n\t\tis_working_dir: bool,\n\t) -> Self {\n\t\tSelf {\n\t\t\tfiles: StatusTreeComponent::new(env, title, focus),\n\t\t\tis_working_dir,\n\t\t\tqueue: env.queue.clone(),\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\toptions: env.options.clone(),\n\t\t\trepo: env.repo.clone(),\n\t\t}\n\t}\n\n\t///\n\tpub fn set_items(&mut self, list: &[StatusItem]) -> Result<()> {\n\t\tself.files.show()?;\n\t\tself.files.update(list)?;\n\t\tOk(())\n\t}\n\n\t///\n\tpub fn selection(&self) -> Option<FileTreeItem> {\n\t\tself.files.selection()\n\t}\n\n\t///\n\tpub fn focus_select(&mut self, focus: bool) {\n\t\tself.files.focus(focus);\n\t\tself.files.show_selection(focus);\n\t}\n\n\t/// returns true if list is empty\n\tpub const fn is_empty(&self) -> bool {\n\t\tself.files.is_empty()\n\t}\n\n\t///\n\tpub fn is_file_selected(&self) -> bool {\n\t\tself.files.is_file_selected()\n\t}\n\n\tfn index_add_remove(&self) -> Result<bool> {\n\t\tif let Some(tree_item) = self.selection() {\n\t\t\tif self.is_working_dir {\n\t\t\t\tif let FileTreeItemKind::File(i) = tree_item.kind {\n\t\t\t\t\tlet path = Path::new(i.path.as_str());\n\t\t\t\t\tmatch i.status {\n\t\t\t\t\t\tStatusItemType::Deleted => {\n\t\t\t\t\t\t\tsync::stage_addremoved(\n\t\t\t\t\t\t\t\t&self.repo.borrow(),\n\t\t\t\t\t\t\t\tpath,\n\t\t\t\t\t\t\t)?;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t_ => sync::stage_add_file(\n\t\t\t\t\t\t\t&self.repo.borrow(),\n\t\t\t\t\t\t\tpath,\n\t\t\t\t\t\t)?,\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tlet config =\n\t\t\t\t\t\tself.options.borrow().status_show_untracked();\n\n\t\t\t\t\t//TODO: check if we can handle the one file case with it as well\n\t\t\t\t\tsync::stage_add_all(\n\t\t\t\t\t\t&self.repo.borrow(),\n\t\t\t\t\t\ttree_item.info.full_path.as_str(),\n\t\t\t\t\t\tconfig,\n\t\t\t\t\t)?;\n\t\t\t\t}\n\n\t\t\t\t//TODO: this might be slow in big repos,\n\t\t\t\t// in theory we should be able to ask the tree structure\n\t\t\t\t// if we are currently on a leaf or a lonely branch that\n\t\t\t\t// would mean that after staging the workdir becomes empty\n\t\t\t\tif sync::is_workdir_clean(\n\t\t\t\t\t&self.repo.borrow(),\n\t\t\t\t\tself.options.borrow().status_show_untracked(),\n\t\t\t\t)? {\n\t\t\t\t\tself.queue\n\t\t\t\t\t\t.push(InternalEvent::StatusLastFileMoved);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// this is a staged entry, so lets unstage it\n\t\t\t\tlet path = tree_item.info.full_path.as_str();\n\t\t\t\tsync::reset_stage(&self.repo.borrow(), path)?;\n\t\t\t}\n\n\t\t\treturn Ok(true);\n\t\t}\n\n\t\tOk(false)\n\t}\n\n\tfn index_add_all(&self) -> Result<()> {\n\t\tlet config = self.options.borrow().status_show_untracked();\n\n\t\tsync::stage_add_all(&self.repo.borrow(), \"*\", config)?;\n\n\t\tself.queue.push(InternalEvent::Update(NeedsUpdate::ALL));\n\n\t\tOk(())\n\t}\n\n\tfn stage_remove_all(&self) -> Result<()> {\n\t\tsync::reset_stage(&self.repo.borrow(), \"*\")?;\n\n\t\tself.queue.push(InternalEvent::Update(NeedsUpdate::ALL));\n\n\t\tOk(())\n\t}\n\n\tfn dispatch_reset_workdir(&self) -> bool {\n\t\tif let Some(tree_item) = self.selection() {\n\t\t\tself.queue.push(InternalEvent::ConfirmAction(\n\t\t\t\tAction::Reset(ResetItem {\n\t\t\t\t\tpath: tree_item.info.full_path,\n\t\t\t\t}),\n\t\t\t));\n\n\t\t\treturn true;\n\t\t}\n\t\tfalse\n\t}\n\n\tfn add_to_ignore(&self) -> bool {\n\t\tif let Some(tree_item) = self.selection() {\n\t\t\tif let Err(e) = sync::add_to_ignore(\n\t\t\t\t&self.repo.borrow(),\n\t\t\t\t&tree_item.info.full_path,\n\t\t\t) {\n\t\t\t\tself.queue.push(InternalEvent::ShowErrorMsg(\n\t\t\t\t\tformat!(\n\t\t\t\t\t\t\"ignore error:\\n{}\\nfile:\\n{:?}\",\n\t\t\t\t\t\te, tree_item.info.full_path\n\t\t\t\t\t),\n\t\t\t\t));\n\t\t\t} else {\n\t\t\t\tself.queue\n\t\t\t\t\t.push(InternalEvent::Update(NeedsUpdate::ALL));\n\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\tfalse\n\t}\n}\n\nimpl DrawableComponent for ChangesComponent {\n\tfn draw(&self, f: &mut Frame, r: Rect) -> Result<()> {\n\t\tself.files.draw(f, r)?;\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for ChangesComponent {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tself.files.commands(out, force_all);\n\n\t\tlet some_selection = self.selection().is_some();\n\n\t\tif self.is_working_dir {\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::stage_all(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\tsome_selection && self.focused(),\n\t\t\t));\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::stage_item(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\tsome_selection && self.focused(),\n\t\t\t));\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::reset_item(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\tsome_selection && self.focused(),\n\t\t\t));\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::ignore_item(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\tsome_selection && self.focused(),\n\t\t\t));\n\t\t} else {\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::unstage_item(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\tsome_selection && self.focused(),\n\t\t\t));\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::unstage_all(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\tsome_selection && self.focused(),\n\t\t\t));\n\t\t}\n\n\t\tCommandBlocking::PassingOn\n\t}\n\n\tfn event(&mut self, ev: &Event) -> Result<EventState> {\n\t\tif self.files.event(ev)?.is_consumed() {\n\t\t\treturn Ok(EventState::Consumed);\n\t\t}\n\n\t\tif self.focused() {\n\t\t\tif let Event::Key(e) = ev {\n\t\t\t\treturn if key_match(\n\t\t\t\t\te,\n\t\t\t\t\tself.key_config.keys.stage_unstage_item,\n\t\t\t\t) {\n\t\t\t\t\ttry_or_popup!(\n\t\t\t\t\t\tself,\n\t\t\t\t\t\t\"staging error:\",\n\t\t\t\t\t\tself.index_add_remove()\n\t\t\t\t\t);\n\n\t\t\t\t\tself.queue.push(InternalEvent::Update(\n\t\t\t\t\t\tNeedsUpdate::ALL,\n\t\t\t\t\t));\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(\n\t\t\t\t\te,\n\t\t\t\t\tself.key_config.keys.status_stage_all,\n\t\t\t\t) && !self.is_empty()\n\t\t\t\t{\n\t\t\t\t\tif self.is_working_dir {\n\t\t\t\t\t\ttry_or_popup!(\n\t\t\t\t\t\t\tself,\n\t\t\t\t\t\t\t\"staging all error:\",\n\t\t\t\t\t\t\tself.index_add_all()\n\t\t\t\t\t\t);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tself.stage_remove_all()?;\n\t\t\t\t\t}\n\t\t\t\t\tself.queue\n\t\t\t\t\t\t.push(InternalEvent::StatusLastFileMoved);\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(\n\t\t\t\t\te,\n\t\t\t\t\tself.key_config.keys.status_reset_item,\n\t\t\t\t) && self.is_working_dir\n\t\t\t\t{\n\t\t\t\t\tOk(self.dispatch_reset_workdir().into())\n\t\t\t\t} else if key_match(\n\t\t\t\t\te,\n\t\t\t\t\tself.key_config.keys.status_ignore_file,\n\t\t\t\t) && self.is_working_dir\n\t\t\t\t\t&& !self.is_empty()\n\t\t\t\t{\n\t\t\t\t\tOk(self.add_to_ignore().into())\n\t\t\t\t} else {\n\t\t\t\t\tOk(EventState::NotConsumed)\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn focused(&self) -> bool {\n\t\tself.files.focused()\n\t}\n\n\tfn focus(&mut self, focus: bool) {\n\t\tself.files.focus(focus);\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.files.is_visible()\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.files.hide();\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.files.show()?;\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "src/components/command.rs",
    "content": "use crate::strings::order;\n\n///\n#[derive(Clone, PartialEq, PartialOrd, Ord, Eq)]\npub struct CommandText {\n\t///\n\tpub name: String,\n\t///\n\tpub desc: &'static str,\n\t///\n\tpub group: &'static str,\n\t///\n\tpub hide_help: bool,\n}\n\nimpl CommandText {\n\t///\n\tpub const fn new(\n\t\tname: String,\n\t\tdesc: &'static str,\n\t\tgroup: &'static str,\n\t) -> Self {\n\t\tSelf {\n\t\t\tname,\n\t\t\tdesc,\n\t\t\tgroup,\n\t\t\thide_help: false,\n\t\t}\n\t}\n\t///\n\tpub const fn hide_help(self) -> Self {\n\t\tlet mut tmp = self;\n\t\ttmp.hide_help = true;\n\t\ttmp\n\t}\n}\n\n///\npub struct CommandInfo {\n\t///\n\tpub text: CommandText,\n\t/// available but not active in the context\n\tpub enabled: bool,\n\t/// will show up in the quick bar\n\tpub quick_bar: bool,\n\n\t/// available in current app state\n\tpub available: bool,\n\t/// used to order commands in quickbar\n\tpub order: i8,\n}\n\nimpl CommandInfo {\n\t///\n\tpub const fn new(\n\t\ttext: CommandText,\n\t\tenabled: bool,\n\t\tavailable: bool,\n\t) -> Self {\n\t\tSelf {\n\t\t\ttext,\n\t\t\tenabled,\n\t\t\tquick_bar: true,\n\t\t\tavailable,\n\t\t\torder: order::AVERAGE,\n\t\t}\n\t}\n\n\t///\n\tpub const fn order(self, order: i8) -> Self {\n\t\tlet mut res = self;\n\t\tres.order = order;\n\t\tres\n\t}\n\n\t///\n\tpub const fn hidden(self) -> Self {\n\t\tlet mut res = self;\n\t\tres.quick_bar = false;\n\t\tres\n\t}\n\n\t///\n\tpub const fn show_in_quickbar(&self) -> bool {\n\t\tself.quick_bar && self.available\n\t}\n}\n"
  },
  {
    "path": "src/components/commit_details/compare_details.rs",
    "content": "use std::borrow::Cow;\n\nuse crate::{\n\tapp::Environment,\n\tcomponents::{\n\t\tcommit_details::style::{style_detail, Detail},\n\t\tdialog_paragraph,\n\t\tutils::time_to_string,\n\t\tCommandBlocking, CommandInfo, Component, DrawableComponent,\n\t\tEventState,\n\t},\n\tstrings::{self},\n\tui::style::SharedTheme,\n};\nuse anyhow::Result;\nuse asyncgit::sync::{\n\tself, commit_files::OldNew, CommitDetails, CommitId, RepoPathRef,\n};\nuse crossterm::event::Event;\nuse ratatui::{\n\tlayout::{Constraint, Direction, Layout, Rect},\n\ttext::{Line, Span, Text},\n\tFrame,\n};\n\npub struct CompareDetailsComponent {\n\trepo: RepoPathRef,\n\tdata: Option<OldNew<CommitDetails>>,\n\ttheme: SharedTheme,\n\tfocused: bool,\n}\n\nimpl CompareDetailsComponent {\n\t///\n\tpub fn new(env: &Environment, focused: bool) -> Self {\n\t\tSelf {\n\t\t\tdata: None,\n\t\t\ttheme: env.theme.clone(),\n\t\t\tfocused,\n\t\t\trepo: env.repo.clone(),\n\t\t}\n\t}\n\n\tpub fn set_commits(&mut self, ids: Option<OldNew<CommitId>>) {\n\t\tself.data = ids.and_then(|ids| {\n\t\t\tlet old = sync::get_commit_details(\n\t\t\t\t&self.repo.borrow(),\n\t\t\t\tids.old,\n\t\t\t)\n\t\t\t.ok()?;\n\t\t\tlet new = sync::get_commit_details(\n\t\t\t\t&self.repo.borrow(),\n\t\t\t\tids.new,\n\t\t\t)\n\t\t\t.ok()?;\n\n\t\t\tSome(OldNew { old, new })\n\t\t});\n\t}\n\n\tfn get_commit_text(&self, data: &CommitDetails) -> Vec<Line<'_>> {\n\t\tlet mut res = vec![\n\t\t\tLine::from(vec![\n\t\t\t\tstyle_detail(&self.theme, &Detail::Author),\n\t\t\t\tSpan::styled(\n\t\t\t\t\tCow::from(format!(\n\t\t\t\t\t\t\"{} <{}>\",\n\t\t\t\t\t\tdata.author.name, data.author.email\n\t\t\t\t\t)),\n\t\t\t\t\tself.theme.text(true, false),\n\t\t\t\t),\n\t\t\t]),\n\t\t\tLine::from(vec![\n\t\t\t\tstyle_detail(&self.theme, &Detail::Date),\n\t\t\t\tSpan::styled(\n\t\t\t\t\tCow::from(time_to_string(\n\t\t\t\t\t\tdata.author.time,\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t)),\n\t\t\t\t\tself.theme.text(true, false),\n\t\t\t\t),\n\t\t\t]),\n\t\t];\n\n\t\tres.push(Line::from(vec![\n\t\t\tstyle_detail(&self.theme, &Detail::Message),\n\t\t\tSpan::styled(\n\t\t\t\tCow::from(\n\t\t\t\t\tdata.message\n\t\t\t\t\t\t.as_ref()\n\t\t\t\t\t\t.map(|msg| msg.subject.clone())\n\t\t\t\t\t\t.unwrap_or_default(),\n\t\t\t\t),\n\t\t\t\tself.theme.text(true, false),\n\t\t\t),\n\t\t]));\n\n\t\tres\n\t}\n}\n\nimpl DrawableComponent for CompareDetailsComponent {\n\tfn draw(&self, f: &mut Frame, rect: Rect) -> Result<()> {\n\t\tlet chunks = Layout::default()\n\t\t\t.direction(Direction::Vertical)\n\t\t\t.constraints(\n\t\t\t\t[Constraint::Length(5), Constraint::Length(5)]\n\t\t\t\t\t.as_ref(),\n\t\t\t)\n\t\t\t.split(rect);\n\n\t\tif let Some(data) = &self.data {\n\t\t\tf.render_widget(\n\t\t\t\tdialog_paragraph(\n\t\t\t\t\t&strings::commit::compare_details_info_title(\n\t\t\t\t\t\ttrue,\n\t\t\t\t\t\tdata.old.short_hash(),\n\t\t\t\t\t),\n\t\t\t\t\tText::from(self.get_commit_text(&data.old)),\n\t\t\t\t\t&self.theme,\n\t\t\t\t\tfalse,\n\t\t\t\t),\n\t\t\t\tchunks[0],\n\t\t\t);\n\n\t\t\tf.render_widget(\n\t\t\t\tdialog_paragraph(\n\t\t\t\t\t&strings::commit::compare_details_info_title(\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t\tdata.new.short_hash(),\n\t\t\t\t\t),\n\t\t\t\t\tText::from(self.get_commit_text(&data.new)),\n\t\t\t\t\t&self.theme,\n\t\t\t\t\tfalse,\n\t\t\t\t),\n\t\t\t\tchunks[1],\n\t\t\t);\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for CompareDetailsComponent {\n\tfn commands(\n\t\t&self,\n\t\t_out: &mut Vec<CommandInfo>,\n\t\t_force_all: bool,\n\t) -> CommandBlocking {\n\t\tCommandBlocking::PassingOn\n\t}\n\n\tfn event(&mut self, _event: &Event) -> Result<EventState> {\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn focused(&self) -> bool {\n\t\tself.focused\n\t}\n\n\tfn focus(&mut self, focus: bool) {\n\t\tself.focused = focus;\n\t}\n}\n"
  },
  {
    "path": "src/components/commit_details/details.rs",
    "content": "use crate::{\n\tapp::Environment,\n\tcomponents::{\n\t\tcommit_details::style::style_detail,\n\t\tdialog_paragraph,\n\t\tutils::{scroll_vertical::VerticalScroll, time_to_string},\n\t\tCommandBlocking, CommandInfo, Component, DrawableComponent,\n\t\tEventState, ScrollType,\n\t},\n\tkeys::{key_match, SharedKeyConfig},\n\tstrings::{self, order},\n\tui::style::SharedTheme,\n};\nuse anyhow::Result;\nuse asyncgit::sync::{\n\tself, CommitDetails, CommitId, CommitMessage, RepoPathRef, Tag,\n};\nuse crossterm::event::Event;\nuse ratatui::{\n\tlayout::{Constraint, Direction, Layout, Rect},\n\tstyle::{Modifier, Style},\n\ttext::{Line, Span, Text},\n\tFrame,\n};\nuse std::{borrow::Cow, cell::Cell};\nuse sync::CommitTags;\n\nuse super::style::Detail;\n\npub struct DetailsComponent {\n\trepo: RepoPathRef,\n\tdata: Option<CommitDetails>,\n\ttags: Vec<Tag>,\n\ttheme: SharedTheme,\n\tfocused: bool,\n\tcurrent_width: Cell<u16>,\n\tscroll: VerticalScroll,\n\tscroll_to_bottom_next_draw: Cell<bool>,\n\tkey_config: SharedKeyConfig,\n}\n\ntype WrappedCommitMessage<'a> =\n\t(Vec<Cow<'a, str>>, Vec<Cow<'a, str>>);\n\nimpl DetailsComponent {\n\t///\n\tpub fn new(env: &Environment, focused: bool) -> Self {\n\t\tSelf {\n\t\t\trepo: env.repo.clone(),\n\t\t\tdata: None,\n\t\t\ttags: Vec::new(),\n\t\t\ttheme: env.theme.clone(),\n\t\t\tfocused,\n\t\t\tscroll_to_bottom_next_draw: Cell::new(false),\n\t\t\tcurrent_width: Cell::new(0),\n\t\t\tscroll: VerticalScroll::new(),\n\t\t\tkey_config: env.key_config.clone(),\n\t\t}\n\t}\n\n\tpub fn set_commit(\n\t\t&mut self,\n\t\tid: Option<CommitId>,\n\t\ttags: Option<CommitTags>,\n\t) {\n\t\tself.tags.clear();\n\n\t\tself.data = id.and_then(|id| {\n\t\t\tsync::get_commit_details(&self.repo.borrow(), id).ok()\n\t\t});\n\n\t\tself.scroll.reset();\n\n\t\tif let Some(tags) = tags {\n\t\t\tself.tags.extend(tags);\n\t\t}\n\t}\n\n\tfn wrap_commit_details(\n\t\tmessage: &CommitMessage,\n\t\twidth: usize,\n\t) -> WrappedCommitMessage<'_> {\n\t\tlet width = width.max(1);\n\t\tlet wrapped_title = bwrap::wrap!(&message.subject, width)\n\t\t\t.lines()\n\t\t\t.map(String::from)\n\t\t\t.map(Cow::from)\n\t\t\t.collect();\n\n\t\tif let Some(ref body) = message.body {\n\t\t\tlet wrapped_message: Vec<Cow<'_, str>> =\n\t\t\t\tbwrap::wrap!(body, width)\n\t\t\t\t\t.lines()\n\t\t\t\t\t.map(String::from)\n\t\t\t\t\t.map(Cow::from)\n\t\t\t\t\t.collect();\n\n\t\t\t(wrapped_title, wrapped_message)\n\t\t} else {\n\t\t\t(wrapped_title, vec![])\n\t\t}\n\t}\n\n\tfn get_wrapped_lines(\n\t\tdata: Option<&CommitDetails>,\n\t\twidth: usize,\n\t) -> WrappedCommitMessage<'_> {\n\t\tif let Some(data) = data {\n\t\t\tif let Some(message) = &data.message {\n\t\t\t\treturn Self::wrap_commit_details(message, width);\n\t\t\t}\n\t\t}\n\n\t\t(vec![], vec![])\n\t}\n\n\tfn get_number_of_lines(\n\t\tdetails: Option<&CommitDetails>,\n\t\twidth: usize,\n\t) -> usize {\n\t\tlet (wrapped_title, wrapped_message) =\n\t\t\tSelf::get_wrapped_lines(details, width);\n\n\t\twrapped_title.len() + wrapped_message.len()\n\t}\n\n\tfn get_theme_for_line(&self, bold: bool) -> Style {\n\t\tif bold {\n\t\t\tself.theme.text(true, false).add_modifier(Modifier::BOLD)\n\t\t} else {\n\t\t\tself.theme.text(true, false)\n\t\t}\n\t}\n\n\tfn get_wrapped_text_message(\n\t\t&self,\n\t\twidth: usize,\n\t\theight: usize,\n\t) -> Vec<Line<'_>> {\n\t\tlet (wrapped_title, wrapped_message) =\n\t\t\tSelf::get_wrapped_lines(self.data.as_ref(), width);\n\n\t\t[&wrapped_title[..], &wrapped_message[..]]\n\t\t\t.concat()\n\t\t\t.iter()\n\t\t\t.enumerate()\n\t\t\t.skip(self.scroll.get_top())\n\t\t\t.take(height)\n\t\t\t.map(|(i, line)| {\n\t\t\t\tLine::from(vec![Span::styled(\n\t\t\t\t\tline.clone(),\n\t\t\t\t\tself.get_theme_for_line(i < wrapped_title.len()),\n\t\t\t\t)])\n\t\t\t})\n\t\t\t.collect()\n\t}\n\n\t#[allow(clippy::too_many_lines)]\n\tfn get_text_info(&self) -> Vec<Line<'_>> {\n\t\tself.data.as_ref().map_or_else(Vec::new, |data| {\n\t\t\tlet mut res = vec![\n\t\t\t\tLine::from(vec![\n\t\t\t\t\tstyle_detail(&self.theme, &Detail::Author),\n\t\t\t\t\tSpan::styled(\n\t\t\t\t\t\tCow::from(format!(\n\t\t\t\t\t\t\t\"{} <{}>\",\n\t\t\t\t\t\t\tdata.author.name, data.author.email\n\t\t\t\t\t\t)),\n\t\t\t\t\t\tself.theme.text(true, false),\n\t\t\t\t\t),\n\t\t\t\t]),\n\t\t\t\tLine::from(vec![\n\t\t\t\t\tstyle_detail(&self.theme, &Detail::Date),\n\t\t\t\t\tSpan::styled(\n\t\t\t\t\t\tCow::from(time_to_string(\n\t\t\t\t\t\t\tdata.author.time,\n\t\t\t\t\t\t\tfalse,\n\t\t\t\t\t\t)),\n\t\t\t\t\t\tself.theme.text(true, false),\n\t\t\t\t\t),\n\t\t\t\t]),\n\t\t\t];\n\n\t\t\tif let Some(ref committer) = data.committer {\n\t\t\t\tres.extend(vec![\n\t\t\t\t\tLine::from(vec![\n\t\t\t\t\t\tstyle_detail(&self.theme, &Detail::Committer),\n\t\t\t\t\t\tSpan::styled(\n\t\t\t\t\t\t\tCow::from(format!(\n\t\t\t\t\t\t\t\t\"{} <{}>\",\n\t\t\t\t\t\t\t\tcommitter.name, committer.email\n\t\t\t\t\t\t\t)),\n\t\t\t\t\t\t\tself.theme.text(true, false),\n\t\t\t\t\t\t),\n\t\t\t\t\t]),\n\t\t\t\t\tLine::from(vec![\n\t\t\t\t\t\tstyle_detail(&self.theme, &Detail::Date),\n\t\t\t\t\t\tSpan::styled(\n\t\t\t\t\t\t\tCow::from(time_to_string(\n\t\t\t\t\t\t\t\tcommitter.time,\n\t\t\t\t\t\t\t\tfalse,\n\t\t\t\t\t\t\t)),\n\t\t\t\t\t\t\tself.theme.text(true, false),\n\t\t\t\t\t\t),\n\t\t\t\t\t]),\n\t\t\t\t]);\n\t\t\t}\n\n\t\t\tres.push(Line::from(vec![\n\t\t\t\tSpan::styled(\n\t\t\t\t\tCow::from(strings::commit::details_sha()),\n\t\t\t\t\tself.theme.text(false, false),\n\t\t\t\t),\n\t\t\t\tSpan::styled(\n\t\t\t\t\tCow::from(data.hash.clone()),\n\t\t\t\t\tself.theme.text(true, false),\n\t\t\t\t),\n\t\t\t]));\n\n\t\t\tif !self.tags.is_empty() {\n\t\t\t\tres.push(Line::from(style_detail(\n\t\t\t\t\t&self.theme,\n\t\t\t\t\t&Detail::Sha,\n\t\t\t\t)));\n\n\t\t\t\tres.push(Line::from(\n\t\t\t\t\titertools::Itertools::intersperse(\n\t\t\t\t\t\tself.tags.iter().map(|tag| {\n\t\t\t\t\t\t\tSpan::styled(\n\t\t\t\t\t\t\t\tCow::from(&tag.name),\n\t\t\t\t\t\t\t\tself.theme.text(true, false),\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t}),\n\t\t\t\t\t\tSpan::styled(\n\t\t\t\t\t\t\tCow::from(\",\"),\n\t\t\t\t\t\t\tself.theme.text(true, false),\n\t\t\t\t\t\t),\n\t\t\t\t\t)\n\t\t\t\t\t.collect::<Vec<Span>>(),\n\t\t\t\t));\n\t\t\t}\n\n\t\t\tres\n\t\t})\n\t}\n\n\tfn move_scroll_top(&self, move_type: ScrollType) -> bool {\n\t\tif self.data.is_some() {\n\t\t\tself.scroll.move_top(move_type)\n\t\t} else {\n\t\t\tfalse\n\t\t}\n\t}\n}\n\nimpl DrawableComponent for DetailsComponent {\n\tfn draw(&self, f: &mut Frame, rect: Rect) -> Result<()> {\n\t\tconst CANSCROLL_STRING: &str = \"[\\u{2026}]\";\n\t\tconst EMPTY_STRING: &str = \"\";\n\n\t\tlet chunks = Layout::default()\n\t\t\t.direction(Direction::Vertical)\n\t\t\t.constraints(\n\t\t\t\t[Constraint::Length(8), Constraint::Min(10)].as_ref(),\n\t\t\t)\n\t\t\t.split(rect);\n\n\t\tf.render_widget(\n\t\t\tdialog_paragraph(\n\t\t\t\t&strings::commit::details_info_title(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t),\n\t\t\t\tText::from(self.get_text_info()),\n\t\t\t\t&self.theme,\n\t\t\t\tfalse,\n\t\t\t),\n\t\t\tchunks[0],\n\t\t);\n\n\t\t// We have to take the border into account which is one character on\n\t\t// each side.\n\t\tlet border_width: u16 = 2;\n\n\t\tlet width = chunks[1].width.saturating_sub(border_width);\n\t\tlet height = chunks[1].height.saturating_sub(border_width);\n\n\t\tself.current_width.set(width);\n\n\t\tlet number_of_lines = Self::get_number_of_lines(\n\t\t\tself.data.as_ref(),\n\t\t\tusize::from(width),\n\t\t);\n\n\t\tself.scroll.update_no_selection(\n\t\t\tnumber_of_lines,\n\t\t\tusize::from(height),\n\t\t);\n\n\t\tif self.scroll_to_bottom_next_draw.get() {\n\t\t\tself.scroll.move_top(ScrollType::End);\n\t\t\tself.scroll_to_bottom_next_draw.set(false);\n\t\t}\n\n\t\tlet can_scroll = usize::from(height) < number_of_lines;\n\n\t\tf.render_widget(\n\t\t\tdialog_paragraph(\n\t\t\t\t&format!(\n\t\t\t\t\t\"{} {}\",\n\t\t\t\t\tstrings::commit::details_message_title(\n\t\t\t\t\t\t&self.key_config,\n\t\t\t\t\t),\n\t\t\t\t\tif !self.focused && can_scroll {\n\t\t\t\t\t\tCANSCROLL_STRING\n\t\t\t\t\t} else {\n\t\t\t\t\t\tEMPTY_STRING\n\t\t\t\t\t}\n\t\t\t\t),\n\t\t\t\tText::from(self.get_wrapped_text_message(\n\t\t\t\t\twidth as usize,\n\t\t\t\t\theight as usize,\n\t\t\t\t)),\n\t\t\t\t&self.theme,\n\t\t\t\tself.focused,\n\t\t\t),\n\t\t\tchunks[1],\n\t\t);\n\n\t\tif self.focused {\n\t\t\tself.scroll.draw(f, chunks[1], &self.theme);\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for DetailsComponent {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tlet width = usize::from(self.current_width.get());\n\t\tlet number_of_lines =\n\t\t\tSelf::get_number_of_lines(self.data.as_ref(), width);\n\n\t\tout.push(\n\t\t\tCommandInfo::new(\n\t\t\t\tstrings::commands::navigate_commit_message(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t),\n\t\t\t\tnumber_of_lines > 0,\n\t\t\t\tself.focused || force_all,\n\t\t\t)\n\t\t\t.order(order::NAV),\n\t\t);\n\n\t\tCommandBlocking::PassingOn\n\t}\n\n\tfn event(&mut self, event: &Event) -> Result<EventState> {\n\t\tif self.focused {\n\t\t\tif let Event::Key(e) = event {\n\t\t\t\treturn Ok(\n\t\t\t\t\tif key_match(e, self.key_config.keys.move_up) {\n\t\t\t\t\t\tself.move_scroll_top(ScrollType::Up).into()\n\t\t\t\t\t} else if key_match(\n\t\t\t\t\t\te,\n\t\t\t\t\t\tself.key_config.keys.move_down,\n\t\t\t\t\t) {\n\t\t\t\t\t\tself.move_scroll_top(ScrollType::Down).into()\n\t\t\t\t\t} else if key_match(\n\t\t\t\t\t\te,\n\t\t\t\t\t\tself.key_config.keys.page_up,\n\t\t\t\t\t) {\n\t\t\t\t\t\tself.move_scroll_top(ScrollType::PageUp)\n\t\t\t\t\t\t\t.into()\n\t\t\t\t\t} else if key_match(\n\t\t\t\t\t\te,\n\t\t\t\t\t\tself.key_config.keys.page_down,\n\t\t\t\t\t) {\n\t\t\t\t\t\tself.move_scroll_top(ScrollType::PageDown)\n\t\t\t\t\t\t\t.into()\n\t\t\t\t\t} else if key_match(e, self.key_config.keys.home)\n\t\t\t\t\t\t|| key_match(e, self.key_config.keys.shift_up)\n\t\t\t\t\t{\n\t\t\t\t\t\tself.move_scroll_top(ScrollType::Home).into()\n\t\t\t\t\t} else if key_match(e, self.key_config.keys.end)\n\t\t\t\t\t\t|| key_match(\n\t\t\t\t\t\t\te,\n\t\t\t\t\t\t\tself.key_config.keys.shift_down,\n\t\t\t\t\t\t) {\n\t\t\t\t\t\tself.move_scroll_top(ScrollType::End).into()\n\t\t\t\t\t} else {\n\t\t\t\t\t\tEventState::NotConsumed\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn focused(&self) -> bool {\n\t\tself.focused\n\t}\n\n\tfn focus(&mut self, focus: bool) {\n\t\tif focus {\n\t\t\tself.scroll_to_bottom_next_draw.set(true);\n\t\t} else {\n\t\t\tself.scroll.reset();\n\t\t}\n\n\t\tself.focused = focus;\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\n\tfn get_wrapped_lines(\n\t\tmessage: &CommitMessage,\n\t\twidth: usize,\n\t) -> Vec<Cow<'_, str>> {\n\t\tlet (wrapped_title, wrapped_message) =\n\t\t\tDetailsComponent::wrap_commit_details(message, width);\n\n\t\t[&wrapped_title[..], &wrapped_message[..]].concat()\n\t}\n\n\t#[test]\n\tfn test_textwrap() {\n\t\tlet message = CommitMessage::from(\"Commit message\");\n\n\t\tassert_eq!(\n\t\t\tget_wrapped_lines(&message, 7),\n\t\t\tvec![\"Commit\", \"message\"]\n\t\t);\n\t\tassert_eq!(\n\t\t\tget_wrapped_lines(&message, 14),\n\t\t\tvec![\"Commit message\"]\n\t\t);\n\t\tassert_eq!(\n\t\t\tget_wrapped_lines(&message, 0),\n\t\t\tvec![\"Commit\", \"message\"]\n\t\t);\n\n\t\tlet message_with_newline =\n\t\t\tCommitMessage::from(\"Commit message\\n\");\n\n\t\tassert_eq!(\n\t\t\tget_wrapped_lines(&message_with_newline, 7),\n\t\t\tvec![\"Commit\", \"message\"]\n\t\t);\n\t\tassert_eq!(\n\t\t\tget_wrapped_lines(&message_with_newline, 14),\n\t\t\tvec![\"Commit message\"]\n\t\t);\n\t\tassert_eq!(\n\t\t\tget_wrapped_lines(&message, 0),\n\t\t\tvec![\"Commit\", \"message\"]\n\t\t);\n\n\t\tlet message_with_body = CommitMessage::from(\n\t\t\t\"Commit message\\nFirst line\\nSecond line\",\n\t\t);\n\n\t\tassert_eq!(\n\t\t\tget_wrapped_lines(&message_with_body, 7),\n\t\t\tvec![\n\t\t\t\t\"Commit\", \"message\", \"First\", \"line\", \"Second\",\n\t\t\t\t\"line\"\n\t\t\t]\n\t\t);\n\t\tassert_eq!(\n\t\t\tget_wrapped_lines(&message_with_body, 14),\n\t\t\tvec![\"Commit message\", \"First line\", \"Second line\"]\n\t\t);\n\t\tassert_eq!(\n\t\t\tget_wrapped_lines(&message_with_body, 7),\n\t\t\tvec![\n\t\t\t\t\"Commit\", \"message\", \"First\", \"line\", \"Second\",\n\t\t\t\t\"line\"\n\t\t\t]\n\t\t);\n\t}\n}\n\n#[cfg(test)]\nmod test_line_count {\n\tuse super::*;\n\n\t#[test]\n\tfn test_smoke() {\n\t\tlet commit = CommitDetails {\n\t\t\tmessage: Some(CommitMessage {\n\t\t\t\tsubject: String::from(\"subject line\"),\n\t\t\t\tbody: Some(String::from(\"body lone\")),\n\t\t\t}),\n\t\t\t..CommitDetails::default()\n\t\t};\n\t\tlet lines = DetailsComponent::get_number_of_lines(\n\t\t\tSome(commit.clone()).as_ref(),\n\t\t\t50,\n\t\t);\n\t\tassert_eq!(lines, 2);\n\n\t\tlet lines = DetailsComponent::get_number_of_lines(\n\t\t\tSome(commit).as_ref(),\n\t\t\t8,\n\t\t);\n\t\tassert_eq!(lines, 4);\n\t}\n}\n"
  },
  {
    "path": "src/components/commit_details/mod.rs",
    "content": "mod compare_details;\nmod details;\nmod style;\n\nuse super::{\n\tcommand_pump, event_pump, CommandBlocking, CommandInfo,\n\tComponent, DrawableComponent, EventState, StatusTreeComponent,\n};\nuse crate::{\n\taccessors,\n\tapp::Environment,\n\tkeys::{key_match, SharedKeyConfig},\n\tstrings,\n};\nuse anyhow::Result;\nuse asyncgit::{\n\tsync::{commit_files::OldNew, CommitTags},\n\tAsyncCommitFiles, CommitFilesParams,\n};\nuse compare_details::CompareDetailsComponent;\nuse crossterm::event::Event;\nuse details::DetailsComponent;\nuse ratatui::{\n\tlayout::{Constraint, Direction, Layout, Rect},\n\tFrame,\n};\n\npub struct CommitDetailsComponent {\n\tcommit: Option<CommitFilesParams>,\n\tsingle_details: DetailsComponent,\n\tcompare_details: CompareDetailsComponent,\n\tfile_tree: StatusTreeComponent,\n\tgit_commit_files: AsyncCommitFiles,\n\tvisible: bool,\n\tkey_config: SharedKeyConfig,\n}\n\nimpl CommitDetailsComponent {\n\taccessors!(self, [single_details, compare_details, file_tree]);\n\n\t///\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\tsingle_details: DetailsComponent::new(env, false),\n\t\t\tcompare_details: CompareDetailsComponent::new(env, false),\n\t\t\tgit_commit_files: AsyncCommitFiles::new(\n\t\t\t\tenv.repo.borrow().clone(),\n\t\t\t\t&env.sender_git,\n\t\t\t),\n\t\t\tfile_tree: StatusTreeComponent::new(env, \"\", false),\n\t\t\tvisible: false,\n\t\t\tcommit: None,\n\t\t\tkey_config: env.key_config.clone(),\n\t\t}\n\t}\n\n\tfn get_files_title(&self) -> String {\n\t\tlet files_count = self.file_tree.file_count();\n\n\t\tformat!(\n\t\t\t\"{} {}\",\n\t\t\tstrings::commit::details_files_title(&self.key_config),\n\t\t\tfiles_count\n\t\t)\n\t}\n\n\t///\n\tpub fn set_commits(\n\t\t&mut self,\n\t\tparams: Option<CommitFilesParams>,\n\t\ttags: Option<&CommitTags>,\n\t) -> Result<()> {\n\t\tif params.is_none() {\n\t\t\tself.single_details.set_commit(None, None);\n\t\t\tself.compare_details.set_commits(None);\n\t\t}\n\n\t\tself.commit = params;\n\n\t\tif let Some(id) = params {\n\t\t\tself.file_tree.set_commit(Some(id.id));\n\n\t\t\tif let Some(other) = id.other {\n\t\t\t\tself.compare_details.set_commits(Some(OldNew {\n\t\t\t\t\tnew: id.id,\n\t\t\t\t\told: other,\n\t\t\t\t}));\n\t\t\t} else {\n\t\t\t\tself.single_details\n\t\t\t\t\t.set_commit(Some(id.id), tags.cloned());\n\t\t\t}\n\n\t\t\tif let Some((fetched_id, res)) =\n\t\t\t\tself.git_commit_files.current()?\n\t\t\t{\n\t\t\t\tif fetched_id == id {\n\t\t\t\t\tself.file_tree.update(res.as_slice())?;\n\t\t\t\t\tself.file_tree.set_title(self.get_files_title());\n\n\t\t\t\t\treturn Ok(());\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tself.file_tree.clear()?;\n\t\t\tself.git_commit_files.fetch(id)?;\n\t\t}\n\n\t\tself.file_tree.set_title(self.get_files_title());\n\n\t\tOk(())\n\t}\n\n\t///\n\tpub fn any_work_pending(&self) -> bool {\n\t\tself.git_commit_files.is_pending()\n\t}\n\n\t///\n\tpub const fn files(&self) -> &StatusTreeComponent {\n\t\t&self.file_tree\n\t}\n\n\tfn details_focused(&self) -> bool {\n\t\tself.single_details.focused()\n\t\t\t|| self.compare_details.focused()\n\t}\n\n\tfn set_details_focus(&mut self, focus: bool) {\n\t\tif self.is_compare() {\n\t\t\tself.compare_details.focus(focus);\n\t\t} else {\n\t\t\tself.single_details.focus(focus);\n\t\t}\n\t}\n\n\tfn is_compare(&self) -> bool {\n\t\tself.commit.is_some_and(|p| p.other.is_some())\n\t}\n}\n\nimpl DrawableComponent for CommitDetailsComponent {\n\tfn draw(&self, f: &mut Frame, rect: Rect) -> Result<()> {\n\t\tif !self.visible {\n\t\t\treturn Ok(());\n\t\t}\n\n\t\tlet constraints = if self.is_compare() {\n\t\t\t[Constraint::Length(10), Constraint::Min(0)]\n\t\t} else {\n\t\t\tlet details_focused = self.details_focused();\n\t\t\tlet percentages = if self.file_tree.focused() {\n\t\t\t\t(40, 60)\n\t\t\t} else if details_focused {\n\t\t\t\t(60, 40)\n\t\t\t} else {\n\t\t\t\t(40, 60)\n\t\t\t};\n\n\t\t\t[\n\t\t\t\tConstraint::Percentage(percentages.0),\n\t\t\t\tConstraint::Percentage(percentages.1),\n\t\t\t]\n\t\t};\n\n\t\tlet chunks = Layout::default()\n\t\t\t.direction(Direction::Vertical)\n\t\t\t.constraints(constraints.as_ref())\n\t\t\t.split(rect);\n\n\t\tif self.is_compare() {\n\t\t\tself.compare_details.draw(f, chunks[0])?;\n\t\t} else {\n\t\t\tself.single_details.draw(f, chunks[0])?;\n\t\t}\n\t\tself.file_tree.draw(f, chunks[1])?;\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for CommitDetailsComponent {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.visible || force_all {\n\t\t\tcommand_pump(\n\t\t\t\tout,\n\t\t\t\tforce_all,\n\t\t\t\tself.components().as_slice(),\n\t\t\t);\n\t\t}\n\n\t\tCommandBlocking::PassingOn\n\t}\n\n\tfn event(&mut self, ev: &Event) -> Result<EventState> {\n\t\tif event_pump(ev, self.components_mut().as_mut_slice())?\n\t\t\t.is_consumed()\n\t\t{\n\t\t\tif !self.file_tree.is_visible() {\n\t\t\t\tself.hide();\n\t\t\t}\n\n\t\t\treturn Ok(EventState::Consumed);\n\t\t}\n\n\t\tif self.focused() {\n\t\t\tif let Event::Key(e) = ev {\n\t\t\t\treturn if key_match(e, self.key_config.keys.move_down)\n\t\t\t\t\t&& self.details_focused()\n\t\t\t\t{\n\t\t\t\t\tself.set_details_focus(false);\n\t\t\t\t\tself.file_tree.focus(true);\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(e, self.key_config.keys.move_up)\n\t\t\t\t\t&& self.file_tree.focused()\n\t\t\t\t\t&& !self.is_compare()\n\t\t\t\t{\n\t\t\t\t\tself.file_tree.focus(false);\n\t\t\t\t\tself.set_details_focus(true);\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else {\n\t\t\t\t\tOk(EventState::NotConsumed)\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\tfn hide(&mut self) {\n\t\tself.visible = false;\n\t}\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\t\tself.file_tree.show()?;\n\t\tOk(())\n\t}\n\n\tfn focused(&self) -> bool {\n\t\tself.details_focused() || self.file_tree.focused()\n\t}\n\n\tfn focus(&mut self, focus: bool) {\n\t\tself.single_details.focus(false);\n\t\tself.compare_details.focus(false);\n\t\tself.file_tree.focus(focus);\n\t\tself.file_tree.show_selection(true);\n\t}\n}\n"
  },
  {
    "path": "src/components/commit_details/style.rs",
    "content": "use crate::{strings, ui::style::SharedTheme};\nuse ratatui::text::Span;\nuse std::borrow::Cow;\n\npub enum Detail {\n\tAuthor,\n\tDate,\n\tCommitter,\n\tSha,\n\tMessage,\n}\n\npub fn style_detail<'a>(\n\ttheme: &'a SharedTheme,\n\tfield: &Detail,\n) -> Span<'a> {\n\tmatch field {\n\t\tDetail::Author => Span::styled(\n\t\t\tCow::from(strings::commit::details_author()),\n\t\t\ttheme.text(false, false),\n\t\t),\n\t\tDetail::Date => Span::styled(\n\t\t\tCow::from(strings::commit::details_date()),\n\t\t\ttheme.text(false, false),\n\t\t),\n\t\tDetail::Committer => Span::styled(\n\t\t\tCow::from(strings::commit::details_committer()),\n\t\t\ttheme.text(false, false),\n\t\t),\n\t\tDetail::Sha => Span::styled(\n\t\t\tCow::from(strings::commit::details_tags()),\n\t\t\ttheme.text(false, false),\n\t\t),\n\t\tDetail::Message => Span::styled(\n\t\t\tCow::from(strings::commit::details_message()),\n\t\t\ttheme.text(false, false),\n\t\t),\n\t}\n}\n"
  },
  {
    "path": "src/components/commitlist.rs",
    "content": "use super::utils::logitems::{ItemBatch, LogEntry};\nuse crate::{\n\tapp::Environment,\n\tcomponents::{\n\t\tutils::string_width_align, CommandBlocking, CommandInfo,\n\t\tComponent, DrawableComponent, EventState, ScrollType,\n\t},\n\tkeys::{key_match, SharedKeyConfig},\n\tqueue::{InternalEvent, Queue},\n\tstrings::{self, symbol},\n\ttry_or_popup,\n\tui::style::{SharedTheme, Theme},\n\tui::{calc_scroll_top, draw_scrollbar, Orientation},\n};\nuse anyhow::Result;\nuse asyncgit::sync::{\n\tself, checkout_commit, BranchDetails, BranchInfo, CommitId,\n\tRepoPathRef, Tags,\n};\nuse chrono::{DateTime, Local};\nuse crossterm::event::Event;\nuse indexmap::IndexSet;\nuse itertools::Itertools;\nuse ratatui::{\n\tlayout::{Alignment, Rect},\n\tstyle::Style,\n\ttext::{Line, Span},\n\twidgets::{Block, Borders, Paragraph},\n\tFrame,\n};\nuse std::{\n\tborrow::Cow, cell::Cell, cmp, collections::BTreeMap, rc::Rc,\n\ttime::Instant,\n};\n\nconst ELEMENTS_PER_LINE: usize = 9;\nconst SLICE_SIZE: usize = 1200;\n\n///\npub struct CommitList {\n\trepo: RepoPathRef,\n\ttitle: Box<str>,\n\tselection: usize,\n\thighlighted_selection: Option<usize>,\n\titems: ItemBatch,\n\thighlights: Option<Rc<IndexSet<CommitId>>>,\n\tcommits: IndexSet<CommitId>,\n\t/// The marked commits.\n\t/// `self.marked[].0` holds the commit index into `self.items.items` - used for ordering the list.\n\t/// `self.marked[].1` is the commit id of the marked commit.\n\tmarked: Vec<(usize, CommitId)>,\n\tscroll_state: (Instant, f32),\n\ttags: Option<Tags>,\n\tlocal_branches: BTreeMap<CommitId, Vec<BranchInfo>>,\n\tremote_branches: BTreeMap<CommitId, Vec<BranchInfo>>,\n\tcurrent_size: Cell<Option<(u16, u16)>>,\n\tscroll_top: Cell<usize>,\n\ttheme: SharedTheme,\n\tqueue: Queue,\n\tkey_config: SharedKeyConfig,\n}\n\nimpl CommitList {\n\t///\n\tpub fn new(env: &Environment, title: &str) -> Self {\n\t\tSelf {\n\t\t\trepo: env.repo.clone(),\n\t\t\titems: ItemBatch::default(),\n\t\t\tmarked: Vec::with_capacity(2),\n\t\t\tselection: 0,\n\t\t\thighlighted_selection: None,\n\t\t\tcommits: IndexSet::new(),\n\t\t\thighlights: None,\n\t\t\tscroll_state: (Instant::now(), 0_f32),\n\t\t\ttags: None,\n\t\t\tlocal_branches: BTreeMap::default(),\n\t\t\tremote_branches: BTreeMap::default(),\n\t\t\tcurrent_size: Cell::new(None),\n\t\t\tscroll_top: Cell::new(0),\n\t\t\ttheme: env.theme.clone(),\n\t\t\tqueue: env.queue.clone(),\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\ttitle: title.into(),\n\t\t}\n\t}\n\n\t///\n\tpub const fn tags(&self) -> Option<&Tags> {\n\t\tself.tags.as_ref()\n\t}\n\n\t///\n\tpub fn clear(&mut self) {\n\t\tself.items.clear();\n\t\tself.commits.clear();\n\t}\n\n\t///\n\tpub fn copy_items(&self) -> Vec<CommitId> {\n\t\tself.commits.iter().copied().collect_vec()\n\t}\n\n\t///\n\tpub fn set_tags(&mut self, tags: Tags) {\n\t\tself.tags = Some(tags);\n\t}\n\n\t///\n\tpub fn selected_entry(&self) -> Option<&LogEntry> {\n\t\tself.items.iter().nth(\n\t\t\tself.selection.saturating_sub(self.items.index_offset()),\n\t\t)\n\t}\n\n\t///\n\tpub const fn marked_count(&self) -> usize {\n\t\tself.marked.len()\n\t}\n\n\t///\n\tpub fn clear_marked(&mut self) {\n\t\tself.marked.clear();\n\t}\n\n\t///\n\tpub fn marked_commits(&self) -> Vec<CommitId> {\n\t\tlet (_, commits): (Vec<_>, Vec<CommitId>) =\n\t\t\tself.marked.iter().copied().unzip();\n\n\t\tcommits\n\t}\n\n\t/// Build string of marked or selected (if none are marked) commit ids\n\tfn concat_selected_commit_ids(&self) -> Option<String> {\n\t\tmatch self.marked.as_slice() {\n\t\t\t[] => self\n\t\t\t\t.items\n\t\t\t\t.iter()\n\t\t\t\t.nth(\n\t\t\t\t\tself.selection\n\t\t\t\t\t\t.saturating_sub(self.items.index_offset()),\n\t\t\t\t)\n\t\t\t\t.map(|e| e.id.to_string()),\n\t\t\tmarked => Some(\n\t\t\t\tmarked\n\t\t\t\t\t.iter()\n\t\t\t\t\t.map(|(_idx, commit)| commit.to_string())\n\t\t\t\t\t.join(\" \"),\n\t\t\t),\n\t\t}\n\t}\n\n\t/// Copy currently marked or selected (if none are marked) commit ids\n\t/// to clipboard\n\tpub fn copy_commit_hash(&self) -> Result<()> {\n\t\tif let Some(yank) = self.concat_selected_commit_ids() {\n\t\t\tcrate::clipboard::copy_string(&yank)?;\n\t\t\tself.queue.push(InternalEvent::ShowInfoMsg(\n\t\t\t\tstrings::copy_success(&yank),\n\t\t\t));\n\t\t}\n\t\tOk(())\n\t}\n\n\t///\n\tpub fn checkout(&self) {\n\t\tif let Some(commit_hash) =\n\t\t\tself.selected_entry().map(|entry| entry.id)\n\t\t{\n\t\t\ttry_or_popup!(\n\t\t\t\tself,\n\t\t\t\t\"failed to checkout commit:\",\n\t\t\t\tcheckout_commit(&self.repo.borrow(), commit_hash)\n\t\t\t);\n\t\t}\n\t}\n\n\t///\n\tpub fn set_local_branches(\n\t\t&mut self,\n\t\tlocal_branches: Vec<BranchInfo>,\n\t) {\n\t\tself.local_branches.clear();\n\n\t\tfor local_branch in local_branches {\n\t\t\tself.local_branches\n\t\t\t\t.entry(local_branch.top_commit)\n\t\t\t\t.or_default()\n\t\t\t\t.push(local_branch);\n\t\t}\n\t}\n\n\t///\n\tpub fn set_remote_branches(\n\t\t&mut self,\n\t\tremote_branches: Vec<BranchInfo>,\n\t) {\n\t\tself.remote_branches.clear();\n\n\t\tfor remote_branch in remote_branches {\n\t\t\tself.remote_branches\n\t\t\t\t.entry(remote_branch.top_commit)\n\t\t\t\t.or_default()\n\t\t\t\t.push(remote_branch);\n\t\t}\n\t}\n\n\t///\n\tpub fn set_commits(&mut self, commits: IndexSet<CommitId>) {\n\t\tif commits != self.commits {\n\t\t\tself.items.clear();\n\t\t\tself.commits = commits;\n\t\t\tself.fetch_commits(false);\n\t\t}\n\t}\n\n\t///\n\tpub fn refresh_extend_data(&mut self, commits: Vec<CommitId>) {\n\t\tlet new_commits = !commits.is_empty();\n\t\tself.commits.extend(commits);\n\n\t\tlet selection = self.selection();\n\t\tlet selection_max = self.selection_max();\n\n\t\tif self.needs_data(selection, selection_max) || new_commits {\n\t\t\tself.fetch_commits(false);\n\t\t}\n\t}\n\n\t///\n\tpub fn set_highlighting(\n\t\t&mut self,\n\t\thighlighting: Option<Rc<IndexSet<CommitId>>>,\n\t) {\n\t\t//note: set highlights to none if there is no highlight\n\t\tself.highlights = if highlighting\n\t\t\t.as_ref()\n\t\t\t.is_some_and(|set| set.is_empty())\n\t\t{\n\t\t\tNone\n\t\t} else {\n\t\t\thighlighting\n\t\t};\n\n\t\tself.select_next_highlight();\n\t\tself.set_highlighted_selection_index();\n\t\tself.fetch_commits(true);\n\t}\n\n\t///\n\tpub fn select_commit(&mut self, id: CommitId) -> Result<()> {\n\t\tlet index = self.commits.get_index_of(&id);\n\n\t\tif let Some(index) = index {\n\t\t\tself.selection = index;\n\t\t\tself.set_highlighted_selection_index();\n\t\t\tOk(())\n\t\t} else {\n\t\t\tanyhow::bail!(\"Could not select commit. It might not be loaded yet or it might be on a different branch.\");\n\t\t}\n\t}\n\n\t///\n\tpub fn highlighted_selection_info(&self) -> (usize, usize) {\n\t\tlet amount = self\n\t\t\t.highlights\n\t\t\t.as_ref()\n\t\t\t.map(|highlights| highlights.len())\n\t\t\t.unwrap_or_default();\n\t\t(self.highlighted_selection.unwrap_or_default(), amount)\n\t}\n\n\tfn set_highlighted_selection_index(&mut self) {\n\t\tself.highlighted_selection =\n\t\t\tself.highlights.as_ref().and_then(|highlights| {\n\t\t\t\thighlights.iter().position(|entry| {\n\t\t\t\t\tentry == &self.commits[self.selection]\n\t\t\t\t})\n\t\t\t});\n\t}\n\n\tconst fn selection(&self) -> usize {\n\t\tself.selection\n\t}\n\n\t/// will return view size or None before the first render\n\tconst fn current_size(&self) -> Option<(u16, u16)> {\n\t\tself.current_size.get()\n\t}\n\n\tfn selection_max(&self) -> usize {\n\t\tself.commits.len().saturating_sub(1)\n\t}\n\n\tfn selected_entry_marked(&self) -> bool {\n\t\tself.selected_entry()\n\t\t\t.and_then(|e| self.is_marked(&e.id))\n\t\t\t.unwrap_or_default()\n\t}\n\n\tfn move_selection(&mut self, scroll: ScrollType) -> Result<bool> {\n\t\tlet needs_update = if self.items.highlighting() {\n\t\t\tself.move_selection_highlighting(scroll)?\n\t\t} else {\n\t\t\tself.move_selection_normal(scroll)?\n\t\t};\n\n\t\tOk(needs_update)\n\t}\n\n\tfn move_selection_highlighting(\n\t\t&mut self,\n\t\tscroll: ScrollType,\n\t) -> Result<bool> {\n\t\tlet (current_index, selection_max) =\n\t\t\tself.highlighted_selection_info();\n\n\t\tlet new_index = match scroll {\n\t\t\tScrollType::Up => current_index.saturating_sub(1),\n\t\t\tScrollType::Down => current_index.saturating_add(1),\n\n\t\t\t//TODO: support this?\n\t\t\t// ScrollType::Home => 0,\n\t\t\t// ScrollType::End => self.selection_max(),\n\t\t\t_ => return Ok(false),\n\t\t};\n\n\t\tlet new_index =\n\t\t\tcmp::min(new_index, selection_max.saturating_sub(1));\n\n\t\tlet index_changed = new_index != current_index;\n\n\t\tif !index_changed {\n\t\t\treturn Ok(false);\n\t\t}\n\n\t\tlet new_selected_commit =\n\t\t\tself.highlights.as_ref().and_then(|highlights| {\n\t\t\t\thighlights.iter().nth(new_index).copied()\n\t\t\t});\n\n\t\tif let Some(c) = new_selected_commit {\n\t\t\tself.select_commit(c)?;\n\t\t\treturn Ok(true);\n\t\t}\n\n\t\tOk(false)\n\t}\n\n\tfn move_selection_normal(\n\t\t&mut self,\n\t\tscroll: ScrollType,\n\t) -> Result<bool> {\n\t\tself.update_scroll_speed();\n\n\t\t#[allow(clippy::cast_possible_truncation)]\n\t\tlet speed_int = usize::try_from(self.scroll_state.1 as i64)?.max(1);\n\n\t\tlet page_offset = usize::from(\n\t\t\tself.current_size.get().unwrap_or_default().1,\n\t\t)\n\t\t.saturating_sub(1);\n\n\t\tlet new_selection = match scroll {\n\t\t\tScrollType::Up => {\n\t\t\t\tself.selection.saturating_sub(speed_int)\n\t\t\t}\n\t\t\tScrollType::Down => {\n\t\t\t\tself.selection.saturating_add(speed_int)\n\t\t\t}\n\t\t\tScrollType::PageUp => {\n\t\t\t\tself.selection.saturating_sub(page_offset)\n\t\t\t}\n\t\t\tScrollType::PageDown => {\n\t\t\t\tself.selection.saturating_add(page_offset)\n\t\t\t}\n\t\t\tScrollType::Home => 0,\n\t\t\tScrollType::End => self.selection_max(),\n\t\t};\n\n\t\tlet new_selection =\n\t\t\tcmp::min(new_selection, self.selection_max());\n\t\tlet needs_update = new_selection != self.selection;\n\n\t\tself.selection = new_selection;\n\n\t\tOk(needs_update)\n\t}\n\n\tfn mark(&mut self) {\n\t\tif let Some(e) = self.selected_entry() {\n\t\t\tlet id = e.id;\n\t\t\tlet selected = self\n\t\t\t\t.selection\n\t\t\t\t.saturating_sub(self.items.index_offset());\n\t\t\tif self.is_marked(&id).unwrap_or_default() {\n\t\t\t\tself.marked.retain(|marked| marked.1 != id);\n\t\t\t} else {\n\t\t\t\tself.marked.push((selected, id));\n\n\t\t\t\tself.marked.sort_unstable_by(|first, second| {\n\t\t\t\t\tfirst.0.cmp(&second.0)\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\tfn update_scroll_speed(&mut self) {\n\t\tconst REPEATED_SCROLL_THRESHOLD_MILLIS: u128 = 300;\n\t\tconst SCROLL_SPEED_START: f32 = 0.1_f32;\n\t\tconst SCROLL_SPEED_MAX: f32 = 10_f32;\n\t\tconst SCROLL_SPEED_MULTIPLIER: f32 = 1.05_f32;\n\n\t\tlet now = Instant::now();\n\n\t\tlet since_last_scroll =\n\t\t\tnow.duration_since(self.scroll_state.0);\n\n\t\tself.scroll_state.0 = now;\n\n\t\tlet speed = if since_last_scroll.as_millis()\n\t\t\t< REPEATED_SCROLL_THRESHOLD_MILLIS\n\t\t{\n\t\t\tself.scroll_state.1 * SCROLL_SPEED_MULTIPLIER\n\t\t} else {\n\t\t\tSCROLL_SPEED_START\n\t\t};\n\n\t\tself.scroll_state.1 = speed.min(SCROLL_SPEED_MAX);\n\t}\n\n\tfn is_marked(&self, id: &CommitId) -> Option<bool> {\n\t\tif self.marked.is_empty() {\n\t\t\tNone\n\t\t} else {\n\t\t\tlet found =\n\t\t\t\tself.marked.iter().any(|entry| entry.1 == *id);\n\t\t\tSome(found)\n\t\t}\n\t}\n\n\t#[allow(clippy::too_many_arguments)]\n\tfn get_entry_to_add<'a>(\n\t\t&self,\n\t\te: &'a LogEntry,\n\t\tselected: bool,\n\t\ttags: Option<String>,\n\t\tlocal_branches: Option<String>,\n\t\tremote_branches: Option<String>,\n\t\ttheme: &Theme,\n\t\twidth: usize,\n\t\tnow: DateTime<Local>,\n\t\tmarked: Option<bool>,\n\t) -> Line<'a> {\n\t\tlet mut txt: Vec<Span> = Vec::with_capacity(\n\t\t\tELEMENTS_PER_LINE + if marked.is_some() { 2 } else { 0 },\n\t\t);\n\n\t\tlet normal = !self.items.highlighting()\n\t\t\t|| (self.items.highlighting() && e.highlighted);\n\n\t\tlet splitter_txt = Cow::from(symbol::EMPTY_SPACE);\n\t\tlet splitter = Span::styled(\n\t\t\tsplitter_txt,\n\t\t\tif normal {\n\t\t\t\ttheme.text(true, selected)\n\t\t\t} else {\n\t\t\t\tStyle::default()\n\t\t\t},\n\t\t);\n\n\t\t// marker\n\t\tif let Some(marked) = marked {\n\t\t\ttxt.push(Span::styled(\n\t\t\t\tCow::from(if marked {\n\t\t\t\t\tsymbol::CHECKMARK\n\t\t\t\t} else {\n\t\t\t\t\tsymbol::EMPTY_SPACE\n\t\t\t\t}),\n\t\t\t\ttheme.log_marker(selected),\n\t\t\t));\n\t\t\ttxt.push(splitter.clone());\n\t\t}\n\n\t\tlet style_hash = if normal {\n\t\t\ttheme.commit_hash(selected)\n\t\t} else {\n\t\t\ttheme.commit_unhighlighted()\n\t\t};\n\t\tlet style_time = if normal {\n\t\t\ttheme.commit_time(selected)\n\t\t} else {\n\t\t\ttheme.commit_unhighlighted()\n\t\t};\n\t\tlet style_author = if normal {\n\t\t\ttheme.commit_author(selected)\n\t\t} else {\n\t\t\ttheme.commit_unhighlighted()\n\t\t};\n\t\tlet style_tags = if normal {\n\t\t\ttheme.tags(selected)\n\t\t} else {\n\t\t\ttheme.commit_unhighlighted()\n\t\t};\n\t\tlet style_branches = if normal {\n\t\t\ttheme.branch(selected, true)\n\t\t} else {\n\t\t\ttheme.commit_unhighlighted()\n\t\t};\n\t\tlet style_msg = if normal {\n\t\t\ttheme.text(true, selected)\n\t\t} else {\n\t\t\ttheme.commit_unhighlighted()\n\t\t};\n\n\t\t// commit hash\n\t\ttxt.push(Span::styled(Cow::from(&*e.hash_short), style_hash));\n\n\t\ttxt.push(splitter.clone());\n\n\t\t// commit timestamp\n\t\ttxt.push(Span::styled(\n\t\t\tCow::from(e.time_to_string(now)),\n\t\t\tstyle_time,\n\t\t));\n\n\t\ttxt.push(splitter.clone());\n\n\t\tlet author_width =\n\t\t\t(width.saturating_sub(19) / 3).clamp(3, 20);\n\t\tlet author = string_width_align(&e.author, author_width);\n\n\t\t// commit author\n\t\ttxt.push(Span::styled(author, style_author));\n\n\t\ttxt.push(splitter.clone());\n\n\t\t// commit tags\n\t\tif let Some(tags) = tags {\n\t\t\ttxt.push(splitter.clone());\n\t\t\ttxt.push(Span::styled(tags, style_tags));\n\t\t}\n\n\t\tif let Some(local_branches) = local_branches {\n\t\t\ttxt.push(splitter.clone());\n\t\t\ttxt.push(Span::styled(local_branches, style_branches));\n\t\t}\n\t\tif let Some(remote_branches) = remote_branches {\n\t\t\ttxt.push(splitter.clone());\n\t\t\ttxt.push(Span::styled(remote_branches, style_branches));\n\t\t}\n\n\t\ttxt.push(splitter);\n\n\t\tlet message_width = width.saturating_sub(\n\t\t\ttxt.iter().map(|span| span.content.len()).sum(),\n\t\t);\n\n\t\t// commit msg\n\t\ttxt.push(Span::styled(\n\t\t\tformat!(\"{:message_width$}\", &e.msg),\n\t\t\tstyle_msg,\n\t\t));\n\n\t\tLine::from(txt)\n\t}\n\n\tfn get_text(&self, height: usize, width: usize) -> Vec<Line<'_>> {\n\t\tlet selection = self.relative_selection();\n\n\t\tlet mut txt: Vec<Line> = Vec::with_capacity(height);\n\n\t\tlet now = Local::now();\n\n\t\tlet any_marked = !self.marked.is_empty();\n\n\t\tfor (idx, e) in self\n\t\t\t.items\n\t\t\t.iter()\n\t\t\t.skip(self.scroll_top.get())\n\t\t\t.take(height)\n\t\t\t.enumerate()\n\t\t{\n\t\t\tlet tags =\n\t\t\t\tself.tags.as_ref().and_then(|t| t.get(&e.id)).map(\n\t\t\t\t\t|tags| {\n\t\t\t\t\t\ttags.iter()\n\t\t\t\t\t\t\t.map(|t| format!(\"<{}>\", t.name))\n\t\t\t\t\t\t\t.join(\" \")\n\t\t\t\t\t},\n\t\t\t\t);\n\n\t\t\tlet local_branches =\n\t\t\t\tself.local_branches.get(&e.id).map(|local_branch| {\n\t\t\t\t\tlocal_branch\n\t\t\t\t\t\t.iter()\n\t\t\t\t\t\t.map(|local_branch| {\n\t\t\t\t\t\t\tformat!(\"{{{0}}}\", local_branch.name)\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.join(\" \")\n\t\t\t\t});\n\n\t\t\tlet marked = if any_marked {\n\t\t\t\tself.is_marked(&e.id)\n\t\t\t} else {\n\t\t\t\tNone\n\t\t\t};\n\n\t\t\ttxt.push(self.get_entry_to_add(\n\t\t\t\te,\n\t\t\t\tidx + self.scroll_top.get() == selection,\n\t\t\t\ttags,\n\t\t\t\tlocal_branches,\n\t\t\t\tself.remote_branches_string(e),\n\t\t\t\t&self.theme,\n\t\t\t\twidth,\n\t\t\t\tnow,\n\t\t\t\tmarked,\n\t\t\t));\n\t\t}\n\n\t\ttxt\n\t}\n\n\tfn remote_branches_string(&self, e: &LogEntry) -> Option<String> {\n\t\tself.remote_branches.get(&e.id).and_then(|remote_branches| {\n\t\t\tlet filtered_branches: Vec<_> = remote_branches\n\t\t\t\t.iter()\n\t\t\t\t.filter(|remote_branch| {\n\t\t\t\t\tself.local_branches.get(&e.id).is_none_or(\n\t\t\t\t\t\t|local_branch| {\n\t\t\t\t\t\t\tlocal_branch.iter().any(|local_branch| {\n\t\t\t\t\t\t\t\tlet has_corresponding_local_branch =\n\t\t\t\t\t\t\t\t\tmatch &local_branch.details {\n\t\t\t\t\t\t\t\t\t\tBranchDetails::Local(\n\t\t\t\t\t\t\t\t\t\t\tdetails,\n\t\t\t\t\t\t\t\t\t\t) => details\n\t\t\t\t\t\t\t\t\t\t\t.upstream\n\t\t\t\t\t\t\t\t\t\t\t.as_ref()\n\t\t\t\t\t\t\t\t\t\t\t.is_some_and(\n\t\t\t\t\t\t\t\t\t\t\t\t|upstream| {\n\t\t\t\t\t\t\t\t\t\t\t\t\tupstream.reference == remote_branch.reference\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t\tBranchDetails::Remote(_) => {\n\t\t\t\t\t\t\t\t\t\t\tfalse\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t};\n\n\t\t\t\t\t\t\t\t!has_corresponding_local_branch\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t},\n\t\t\t\t\t)\n\t\t\t\t})\n\t\t\t\t.map(|remote_branch| {\n\t\t\t\t\tformat!(\"[{0}]\", remote_branch.name)\n\t\t\t\t})\n\t\t\t\t.collect();\n\n\t\t\tif filtered_branches.is_empty() {\n\t\t\t\tNone\n\t\t\t} else {\n\t\t\t\tSome(filtered_branches.join(\" \"))\n\t\t\t}\n\t\t})\n\t}\n\n\tfn relative_selection(&self) -> usize {\n\t\tself.selection.saturating_sub(self.items.index_offset())\n\t}\n\n\tfn select_next_highlight(&mut self) {\n\t\tif self.highlights.is_none() {\n\t\t\treturn;\n\t\t}\n\n\t\tlet old_selection = self.selection;\n\n\t\tlet mut offset = 0;\n\t\tloop {\n\t\t\tlet hit_upper_bound =\n\t\t\t\told_selection + offset > self.selection_max();\n\t\t\tlet hit_lower_bound = offset > old_selection;\n\n\t\t\tif !hit_upper_bound {\n\t\t\t\tself.selection = old_selection + offset;\n\n\t\t\t\tif self.selection_highlighted() {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif !hit_lower_bound {\n\t\t\t\tself.selection = old_selection - offset;\n\n\t\t\t\tif self.selection_highlighted() {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif hit_lower_bound && hit_upper_bound {\n\t\t\t\tself.selection = old_selection;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\toffset += 1;\n\t\t}\n\t}\n\n\tfn selection_highlighted(&self) -> bool {\n\t\tlet commit = self.commits[self.selection];\n\n\t\tself.highlights\n\t\t\t.as_ref()\n\t\t\t.is_some_and(|highlights| highlights.contains(&commit))\n\t}\n\n\tfn needs_data(&self, idx: usize, idx_max: usize) -> bool {\n\t\tself.items.needs_data(idx, idx_max)\n\t}\n\n\t// checks if first entry in items is the same commit as we expect\n\tfn is_list_in_sync(&self) -> bool {\n\t\tself.items\n\t\t\t.index_offset_raw()\n\t\t\t.and_then(|index| {\n\t\t\t\tself.items\n\t\t\t\t\t.iter()\n\t\t\t\t\t.next()\n\t\t\t\t\t.map(|item| item.id == self.commits[index])\n\t\t\t})\n\t\t\t.unwrap_or_default()\n\t}\n\n\tfn fetch_commits(&mut self, force: bool) {\n\t\tlet want_min =\n\t\t\tself.selection().saturating_sub(SLICE_SIZE / 2);\n\t\tlet commits = self.commits.len();\n\n\t\tlet want_min = want_min.min(commits);\n\n\t\tlet index_in_sync = self\n\t\t\t.items\n\t\t\t.index_offset_raw()\n\t\t\t.is_some_and(|index| want_min == index);\n\n\t\tif !index_in_sync || !self.is_list_in_sync() || force {\n\t\t\tlet commits = sync::get_commits_info(\n\t\t\t\t&self.repo.borrow(),\n\t\t\t\tself.commits\n\t\t\t\t\t.iter()\n\t\t\t\t\t.skip(want_min)\n\t\t\t\t\t.take(SLICE_SIZE)\n\t\t\t\t\t.copied()\n\t\t\t\t\t.collect_vec()\n\t\t\t\t\t.as_slice(),\n\t\t\t\tself.current_size()\n\t\t\t\t\t.map_or(100u16, |size| size.0)\n\t\t\t\t\t.into(),\n\t\t\t);\n\n\t\t\tif let Ok(commits) = commits {\n\t\t\t\tself.items.set_items(\n\t\t\t\t\twant_min,\n\t\t\t\t\tcommits,\n\t\t\t\t\tself.highlights.as_ref(),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n}\n\nimpl DrawableComponent for CommitList {\n\tfn draw(&self, f: &mut Frame, area: Rect) -> Result<()> {\n\t\tlet current_size = (\n\t\t\tarea.width.saturating_sub(2),\n\t\t\tarea.height.saturating_sub(2),\n\t\t);\n\t\tself.current_size.set(Some(current_size));\n\n\t\tlet height_in_lines = current_size.1 as usize;\n\t\tlet selection = self.relative_selection();\n\n\t\tself.scroll_top.set(calc_scroll_top(\n\t\t\tself.scroll_top.get(),\n\t\t\theight_in_lines,\n\t\t\tselection,\n\t\t));\n\n\t\tlet title = format!(\n\t\t\t\"{} {}/{}\",\n\t\t\tself.title,\n\t\t\tself.commits.len().saturating_sub(self.selection),\n\t\t\tself.commits.len(),\n\t\t);\n\n\t\tf.render_widget(\n\t\t\tParagraph::new(\n\t\t\t\tself.get_text(\n\t\t\t\t\theight_in_lines,\n\t\t\t\t\tcurrent_size.0 as usize,\n\t\t\t\t),\n\t\t\t)\n\t\t\t.block(\n\t\t\t\tBlock::default()\n\t\t\t\t\t.borders(Borders::ALL)\n\t\t\t\t\t.title(Span::styled(\n\t\t\t\t\t\ttitle.as_str(),\n\t\t\t\t\t\tself.theme.title(true),\n\t\t\t\t\t))\n\t\t\t\t\t.border_style(self.theme.block(true)),\n\t\t\t)\n\t\t\t.alignment(Alignment::Left),\n\t\t\tarea,\n\t\t);\n\n\t\tdraw_scrollbar(\n\t\t\tf,\n\t\t\tarea,\n\t\t\t&self.theme,\n\t\t\tself.commits.len(),\n\t\t\tself.selection,\n\t\t\tOrientation::Vertical,\n\t\t);\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for CommitList {\n\tfn event(&mut self, ev: &Event) -> Result<EventState> {\n\t\tif let Event::Key(k) = ev {\n\t\t\tlet selection_changed =\n\t\t\t\tif key_match(k, self.key_config.keys.move_up) {\n\t\t\t\t\tself.move_selection(ScrollType::Up)?\n\t\t\t\t} else if key_match(k, self.key_config.keys.move_down)\n\t\t\t\t{\n\t\t\t\t\tself.move_selection(ScrollType::Down)?\n\t\t\t\t} else if key_match(k, self.key_config.keys.shift_up)\n\t\t\t\t\t|| key_match(k, self.key_config.keys.home)\n\t\t\t\t{\n\t\t\t\t\tself.move_selection(ScrollType::Home)?\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.shift_down,\n\t\t\t\t) || key_match(k, self.key_config.keys.end)\n\t\t\t\t{\n\t\t\t\t\tself.move_selection(ScrollType::End)?\n\t\t\t\t} else if key_match(k, self.key_config.keys.page_up) {\n\t\t\t\t\tself.move_selection(ScrollType::PageUp)?\n\t\t\t\t} else if key_match(k, self.key_config.keys.page_down)\n\t\t\t\t{\n\t\t\t\t\tself.move_selection(ScrollType::PageDown)?\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.log_mark_commit,\n\t\t\t\t) {\n\t\t\t\t\tself.mark();\n\t\t\t\t\ttrue\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.log_checkout_commit,\n\t\t\t\t) {\n\t\t\t\t\tself.checkout();\n\t\t\t\t\ttrue\n\t\t\t\t} else {\n\t\t\t\t\tfalse\n\t\t\t\t};\n\t\t\treturn Ok(selection_changed.into());\n\t\t}\n\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\t_force_all: bool,\n\t) -> CommandBlocking {\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::scroll(&self.key_config),\n\t\t\tself.selected_entry().is_some(),\n\t\t\ttrue,\n\t\t));\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::commit_list_mark(\n\t\t\t\t&self.key_config,\n\t\t\t\tself.selected_entry_marked(),\n\t\t\t),\n\t\t\ttrue,\n\t\t\ttrue,\n\t\t));\n\t\tCommandBlocking::PassingOn\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse asyncgit::sync::CommitInfo;\n\n\tuse super::*;\n\n\timpl Default for CommitList {\n\t\tfn default() -> Self {\n\t\t\tSelf {\n\t\t\t\ttitle: String::new().into_boxed_str(),\n\t\t\t\tselection: 0,\n\t\t\t\thighlighted_selection: Option::None,\n\t\t\t\thighlights: Option::None,\n\t\t\t\ttags: Option::None,\n\t\t\t\titems: ItemBatch::default(),\n\t\t\t\tcommits: IndexSet::default(),\n\t\t\t\tmarked: Vec::default(),\n\t\t\t\tscroll_top: Cell::default(),\n\t\t\t\tlocal_branches: BTreeMap::default(),\n\t\t\t\tremote_branches: BTreeMap::default(),\n\t\t\t\ttheme: SharedTheme::default(),\n\t\t\t\tkey_config: SharedKeyConfig::default(),\n\t\t\t\tscroll_state: (Instant::now(), 0.0),\n\t\t\t\tcurrent_size: Cell::default(),\n\t\t\t\trepo: RepoPathRef::new(sync::RepoPath::Path(\n\t\t\t\t\tstd::path::PathBuf::default(),\n\t\t\t\t)),\n\t\t\t\tqueue: Queue::default(),\n\t\t\t}\n\t\t}\n\t}\n\n\t#[test]\n\tfn test_string_width_align() {\n\t\tassert_eq!(string_width_align(\"123\", 3), \"123\");\n\t\tassert_eq!(string_width_align(\"123\", 2), \"..\");\n\t\tassert_eq!(string_width_align(\"123\", 3), \"123\");\n\t\tassert_eq!(string_width_align(\"12345\", 6), \"12345 \");\n\t\tassert_eq!(string_width_align(\"1234556\", 4), \"12..\");\n\t}\n\n\t#[test]\n\tfn test_string_width_align_unicode() {\n\t\tassert_eq!(string_width_align(\"äste\", 3), \"ä..\");\n\t\tassert_eq!(\n\t\t\tstring_width_align(\"wüsten äste\", 10),\n\t\t\t\"wüsten ä..\"\n\t\t);\n\t\tassert_eq!(\n\t\t\tstring_width_align(\"Jon Grythe Stødle\", 19),\n\t\t\t\"Jon Grythe Stødle  \"\n\t\t);\n\t}\n\n\t/// Build a commit list with a few commits loaded\n\tfn build_commit_list_with_some_commits() -> CommitList {\n\t\tlet mut items = ItemBatch::default();\n\t\tlet basic_commit_info = CommitInfo {\n\t\t\tmessage: String::default(),\n\t\t\ttime: 0,\n\t\t\tauthor: String::default(),\n\t\t\tid: CommitId::default(),\n\t\t};\n\t\t// This just creates a sequence of fake ordered ids\n\t\t// 0000000000000000000000000000000000000000\n\t\t// 0000000000000000000000000000000000000001\n\t\t// 0000000000000000000000000000000000000002\n\t\t// ...\n\t\titems.set_items(\n\t\t\t2, /* randomly choose an offset */\n\t\t\t(0..20)\n\t\t\t\t.map(|idx| CommitInfo {\n\t\t\t\t\tid: CommitId::from_str_unchecked(&format!(\n\t\t\t\t\t\t\"{idx:040}\",\n\t\t\t\t\t))\n\t\t\t\t\t.unwrap(),\n\t\t\t\t\t..basic_commit_info.clone()\n\t\t\t\t})\n\t\t\t\t.collect(),\n\t\t\tNone,\n\t\t);\n\t\tCommitList {\n\t\t\titems,\n\t\t\tselection: 4, // Randomly select one commit\n\t\t\t..Default::default()\n\t\t}\n\t}\n\n\t/// Build a value for cl.marked based on indices into cl.items\n\tfn build_marked_from_indices(\n\t\tcl: &CommitList,\n\t\tmarked_indices: &[usize],\n\t) -> Vec<(usize, CommitId)> {\n\t\tlet offset = cl.items.index_offset();\n\t\tmarked_indices\n\t\t\t.iter()\n\t\t\t.map(|idx| {\n\t\t\t\t(*idx, cl.items.iter().nth(*idx - offset).unwrap().id)\n\t\t\t})\n\t\t\t.collect()\n\t}\n\n\t#[test]\n\tfn test_copy_commit_list_empty() {\n\t\tassert_eq!(\n\t\t\tCommitList::default().concat_selected_commit_ids(),\n\t\t\tNone\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_copy_commit_none_marked() {\n\t\tlet cl = CommitList {\n\t\t\tselection: 4,\n\t\t\t..build_commit_list_with_some_commits()\n\t\t};\n\t\t// ids from build_commit_list_with_some_commits() are\n\t\t// offset by two, so we expect commit id 2 for\n\t\t// selection = 4\n\t\tassert_eq!(\n\t\t\tcl.concat_selected_commit_ids(),\n\t\t\tSome(String::from(\n\t\t\t\t\"0000000000000000000000000000000000000002\"\n\t\t\t))\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_copy_commit_one_marked() {\n\t\tlet cl = build_commit_list_with_some_commits();\n\t\tlet cl = CommitList {\n\t\t\tmarked: build_marked_from_indices(&cl, &[3]),\n\t\t\t..cl\n\t\t};\n\t\tassert_eq!(\n\t\t\tcl.concat_selected_commit_ids(),\n\t\t\tSome(String::from(\n\t\t\t\t\"0000000000000000000000000000000000000001\",\n\t\t\t))\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_copy_commit_range_marked() {\n\t\tlet cl = build_commit_list_with_some_commits();\n\t\tlet cl = CommitList {\n\t\t\tmarked: build_marked_from_indices(&cl, &[4, 5, 6, 7]),\n\t\t\t..cl\n\t\t};\n\t\tassert_eq!(\n\t\t\tcl.concat_selected_commit_ids(),\n\t\t\tSome(String::from(concat!(\n\t\t\t\t\"0000000000000000000000000000000000000002 \",\n\t\t\t\t\"0000000000000000000000000000000000000003 \",\n\t\t\t\t\"0000000000000000000000000000000000000004 \",\n\t\t\t\t\"0000000000000000000000000000000000000005\"\n\t\t\t)))\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_copy_commit_random_marked() {\n\t\tlet cl = build_commit_list_with_some_commits();\n\t\tlet cl = CommitList {\n\t\t\tmarked: build_marked_from_indices(&cl, &[4, 7]),\n\t\t\t..cl\n\t\t};\n\t\tassert_eq!(\n\t\t\tcl.concat_selected_commit_ids(),\n\t\t\tSome(String::from(concat!(\n\t\t\t\t\"0000000000000000000000000000000000000002 \",\n\t\t\t\t\"0000000000000000000000000000000000000005\"\n\t\t\t)))\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "src/components/cred.rs",
    "content": "use anyhow::Result;\nuse crossterm::event::Event;\nuse ratatui::{layout::Rect, Frame};\n\nuse asyncgit::sync::cred::BasicAuthCredential;\n\nuse crate::app::Environment;\nuse crate::components::{EventState, InputType, TextInputComponent};\nuse crate::keys::key_match;\nuse crate::{\n\tcomponents::{\n\t\tvisibility_blocking, CommandBlocking, CommandInfo, Component,\n\t\tDrawableComponent,\n\t},\n\tkeys::SharedKeyConfig,\n\tstrings,\n};\n\n///\npub struct CredComponent {\n\tvisible: bool,\n\tkey_config: SharedKeyConfig,\n\tinput_username: TextInputComponent,\n\tinput_password: TextInputComponent,\n\tcred: BasicAuthCredential,\n}\n\nimpl CredComponent {\n\t///\n\tpub fn new(env: &Environment) -> Self {\n\t\tlet key_config = env.key_config.clone();\n\t\tSelf {\n\t\t\tvisible: false,\n\t\t\tinput_username: TextInputComponent::new(\n\t\t\t\tenv,\n\t\t\t\t&strings::username_popup_title(&key_config),\n\t\t\t\t&strings::username_popup_msg(&key_config),\n\t\t\t\tfalse,\n\t\t\t)\n\t\t\t.with_input_type(InputType::Singleline),\n\t\t\tinput_password: TextInputComponent::new(\n\t\t\t\tenv,\n\t\t\t\t&strings::password_popup_title(&key_config),\n\t\t\t\t&strings::password_popup_msg(&key_config),\n\t\t\t\tfalse,\n\t\t\t)\n\t\t\t.with_input_type(InputType::Password),\n\t\t\tkey_config,\n\t\t\tcred: BasicAuthCredential::new(None, None),\n\t\t}\n\t}\n\n\tpub fn set_cred(&mut self, cred: BasicAuthCredential) {\n\t\tself.cred = cred;\n\t}\n\n\tpub const fn get_cred(&self) -> &BasicAuthCredential {\n\t\t&self.cred\n\t}\n}\n\nimpl DrawableComponent for CredComponent {\n\tfn draw(&self, f: &mut Frame, rect: Rect) -> Result<()> {\n\t\tif self.visible {\n\t\t\tself.input_username.draw(f, rect)?;\n\t\t\tself.input_password.draw(f, rect)?;\n\t\t}\n\t\tOk(())\n\t}\n}\n\nimpl Component for CredComponent {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.is_visible() || force_all {\n\t\t\tif !force_all {\n\t\t\t\tout.clear();\n\t\t\t}\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::validate_msg(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\ttrue,\n\t\t\t));\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::close_popup(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\ttrue,\n\t\t\t));\n\t\t}\n\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(&mut self, ev: &Event) -> Result<EventState> {\n\t\tif self.visible {\n\t\t\tif let Event::Key(e) = ev {\n\t\t\t\tif key_match(e, self.key_config.keys.exit_popup) {\n\t\t\t\t\tself.hide();\n\t\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t\t}\n\t\t\t\tif self.input_username.event(ev)?.is_consumed()\n\t\t\t\t\t|| self.input_password.event(ev)?.is_consumed()\n\t\t\t\t{\n\t\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t\t} else if key_match(e, self.key_config.keys.enter) {\n\t\t\t\t\tif self.input_username.is_visible() {\n\t\t\t\t\t\tself.cred = BasicAuthCredential::new(\n\t\t\t\t\t\t\tSome(\n\t\t\t\t\t\t\t\tself.input_username\n\t\t\t\t\t\t\t\t\t.get_text()\n\t\t\t\t\t\t\t\t\t.to_string(),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tNone,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tself.input_username.hide();\n\t\t\t\t\t\tself.input_password.show()?;\n\t\t\t\t\t} else if self.input_password.is_visible() {\n\t\t\t\t\t\tself.cred = BasicAuthCredential::new(\n\t\t\t\t\t\t\tself.cred.username.clone(),\n\t\t\t\t\t\t\tSome(\n\t\t\t\t\t\t\t\tself.input_password\n\t\t\t\t\t\t\t\t\t.get_text()\n\t\t\t\t\t\t\t\t\t.to_string(),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tself.input_password.hide();\n\t\t\t\t\t\tself.input_password.clear();\n\t\t\t\t\t\treturn Ok(EventState::NotConsumed);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tself.hide();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn Ok(EventState::Consumed);\n\t\t}\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.cred = BasicAuthCredential::new(None, None);\n\t\tself.visible = false;\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\t\tif self.cred.username.is_none() {\n\t\t\tself.input_username.show()\n\t\t} else if self.cred.password.is_none() {\n\t\t\tself.input_password.show()\n\t\t} else {\n\t\t\tOk(())\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/components/diff.rs",
    "content": "use super::{\n\tutils::scroll_horizontal::HorizontalScroll,\n\tutils::scroll_vertical::VerticalScroll, CommandBlocking,\n\tDirection, DrawableComponent, HorizontalScrollType, ScrollType,\n};\nuse crate::{\n\tapp::Environment,\n\tcomponents::{CommandInfo, Component, EventState},\n\tkeys::{key_match, SharedKeyConfig},\n\toptions::SharedOptions,\n\tqueue::{Action, InternalEvent, NeedsUpdate, Queue, ResetItem},\n\tstring_utils::tabs_to_spaces,\n\tstring_utils::trim_offset,\n\tstrings, try_or_popup,\n\tui::style::SharedTheme,\n};\nuse anyhow::Result;\nuse asyncgit::{\n\thash,\n\tsync::{self, diff::DiffLinePosition, RepoPathRef},\n\tDiffLine, DiffLineType, FileDiff,\n};\nuse bytesize::ByteSize;\nuse crossterm::event::Event;\nuse ratatui::{\n\tlayout::Rect,\n\tsymbols,\n\ttext::{Line, Span},\n\twidgets::{Block, Borders, Paragraph},\n\tFrame,\n};\nuse std::{borrow::Cow, cell::Cell, cmp, path::Path};\n\n#[derive(Default)]\nstruct Current {\n\tpath: String,\n\tis_stage: bool,\n\thash: u64,\n}\n\n///\n#[derive(Clone, Copy)]\nenum Selection {\n\tSingle(usize),\n\tMultiple(usize, usize),\n}\n\nimpl Selection {\n\tconst fn get_start(&self) -> usize {\n\t\tmatch self {\n\t\t\tSelf::Single(start) | Self::Multiple(start, _) => *start,\n\t\t}\n\t}\n\n\tconst fn get_end(&self) -> usize {\n\t\tmatch self {\n\t\t\tSelf::Single(end) | Self::Multiple(_, end) => *end,\n\t\t}\n\t}\n\n\tfn get_top(&self) -> usize {\n\t\tmatch self {\n\t\t\tSelf::Single(start) => *start,\n\t\t\tSelf::Multiple(start, end) => cmp::min(*start, *end),\n\t\t}\n\t}\n\n\tfn get_bottom(&self) -> usize {\n\t\tmatch self {\n\t\t\tSelf::Single(start) => *start,\n\t\t\tSelf::Multiple(start, end) => cmp::max(*start, *end),\n\t\t}\n\t}\n\n\tfn modify(&mut self, direction: Direction, max: usize) {\n\t\tlet start = self.get_start();\n\t\tlet old_end = self.get_end();\n\n\t\t*self = match direction {\n\t\t\tDirection::Up => {\n\t\t\t\tSelf::Multiple(start, old_end.saturating_sub(1))\n\t\t\t}\n\n\t\t\tDirection::Down => {\n\t\t\t\tSelf::Multiple(start, cmp::min(old_end + 1, max))\n\t\t\t}\n\t\t};\n\t}\n\n\tfn contains(&self, index: usize) -> bool {\n\t\tmatch self {\n\t\t\tSelf::Single(start) => index == *start,\n\t\t\tSelf::Multiple(start, end) => {\n\t\t\t\tif start <= end {\n\t\t\t\t\t*start <= index && index <= *end\n\t\t\t\t} else {\n\t\t\t\t\t*end <= index && index <= *start\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n///\npub struct DiffComponent {\n\trepo: RepoPathRef,\n\tdiff: Option<FileDiff>,\n\tlongest_line: usize,\n\tpending: bool,\n\tselection: Selection,\n\tselected_hunk: Option<usize>,\n\tcurrent_size: Cell<(u16, u16)>,\n\tfocused: bool,\n\tcurrent: Current,\n\tvertical_scroll: VerticalScroll,\n\thorizontal_scroll: HorizontalScroll,\n\tqueue: Queue,\n\ttheme: SharedTheme,\n\tkey_config: SharedKeyConfig,\n\tis_immutable: bool,\n\toptions: SharedOptions,\n}\n\nimpl DiffComponent {\n\t///\n\tpub fn new(env: &Environment, is_immutable: bool) -> Self {\n\t\tSelf {\n\t\t\tfocused: false,\n\t\t\tqueue: env.queue.clone(),\n\t\t\tcurrent: Current::default(),\n\t\t\tpending: false,\n\t\t\tselected_hunk: None,\n\t\t\tdiff: None,\n\t\t\tlongest_line: 0,\n\t\t\tcurrent_size: Cell::new((0, 0)),\n\t\t\tselection: Selection::Single(0),\n\t\t\tvertical_scroll: VerticalScroll::new(),\n\t\t\thorizontal_scroll: HorizontalScroll::new(),\n\t\t\ttheme: env.theme.clone(),\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\tis_immutable,\n\t\t\trepo: env.repo.clone(),\n\t\t\toptions: env.options.clone(),\n\t\t}\n\t}\n\t///\n\tfn can_scroll(&self) -> bool {\n\t\tself.diff.as_ref().is_some_and(|diff| diff.lines > 1)\n\t}\n\t///\n\tpub fn current(&self) -> (String, bool) {\n\t\t(self.current.path.clone(), self.current.is_stage)\n\t}\n\t///\n\tpub fn clear(&mut self, pending: bool) {\n\t\tself.current = Current::default();\n\t\tself.diff = None;\n\t\tself.longest_line = 0;\n\t\tself.vertical_scroll.reset();\n\t\tself.horizontal_scroll.reset();\n\t\tself.selection = Selection::Single(0);\n\t\tself.selected_hunk = None;\n\t\tself.pending = pending;\n\t}\n\t///\n\tpub fn update(\n\t\t&mut self,\n\t\tpath: String,\n\t\tis_stage: bool,\n\t\tdiff: FileDiff,\n\t) {\n\t\tself.pending = false;\n\n\t\tlet hash = hash(&diff);\n\n\t\tif self.current.hash != hash {\n\t\t\tlet reset_selection = self.current.path != path;\n\n\t\t\tself.current = Current {\n\t\t\t\tpath,\n\t\t\t\tis_stage,\n\t\t\t\thash,\n\t\t\t};\n\n\t\t\tself.diff = Some(diff);\n\n\t\t\tself.longest_line = self\n\t\t\t\t.diff\n\t\t\t\t.iter()\n\t\t\t\t.flat_map(|diff| diff.hunks.iter())\n\t\t\t\t.flat_map(|hunk| hunk.lines.iter())\n\t\t\t\t.map(|line| {\n\t\t\t\t\tlet converted_content = tabs_to_spaces(\n\t\t\t\t\t\tline.content.as_ref().to_string(),\n\t\t\t\t\t);\n\n\t\t\t\t\tconverted_content.len()\n\t\t\t\t})\n\t\t\t\t.max()\n\t\t\t\t.map_or(0, |len| {\n\t\t\t\t\t// Each hunk uses a 1-character wide vertical bar to its left to indicate\n\t\t\t\t\t// selection.\n\t\t\t\t\tlen + 1\n\t\t\t\t});\n\n\t\t\tif reset_selection {\n\t\t\t\tself.vertical_scroll.reset();\n\t\t\t\tself.selection = Selection::Single(0);\n\t\t\t\tself.update_selection(0);\n\t\t\t} else {\n\t\t\t\tlet old_selection = match self.selection {\n\t\t\t\t\tSelection::Single(line) => line,\n\t\t\t\t\tSelection::Multiple(start, _) => start,\n\t\t\t\t};\n\t\t\t\tself.update_selection(old_selection);\n\t\t\t}\n\t\t}\n\t}\n\n\tfn move_selection(&mut self, move_type: ScrollType) {\n\t\tif let Some(diff) = &self.diff {\n\t\t\tlet max = diff.lines.saturating_sub(1);\n\n\t\t\tlet new_start = match move_type {\n\t\t\t\tScrollType::Down => {\n\t\t\t\t\tself.selection.get_bottom().saturating_add(1)\n\t\t\t\t}\n\t\t\t\tScrollType::Up => {\n\t\t\t\t\tself.selection.get_top().saturating_sub(1)\n\t\t\t\t}\n\t\t\t\tScrollType::Home => 0,\n\t\t\t\tScrollType::End => max,\n\t\t\t\tScrollType::PageDown => {\n\t\t\t\t\tself.selection.get_bottom().saturating_add(\n\t\t\t\t\t\tself.current_size.get().1.saturating_sub(1)\n\t\t\t\t\t\t\tas usize,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tScrollType::PageUp => {\n\t\t\t\t\tself.selection.get_top().saturating_sub(\n\t\t\t\t\t\tself.current_size.get().1.saturating_sub(1)\n\t\t\t\t\t\t\tas usize,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tself.update_selection(new_start);\n\t\t}\n\t}\n\n\tfn update_selection(&mut self, new_start: usize) {\n\t\tif let Some(diff) = &self.diff {\n\t\t\tlet max = diff.lines.saturating_sub(1);\n\t\t\tlet new_start = cmp::min(max, new_start);\n\t\t\tself.selection = Selection::Single(new_start);\n\t\t\tself.selected_hunk =\n\t\t\t\tSelf::find_selected_hunk(diff, new_start);\n\t\t}\n\t}\n\n\tfn lines_count(&self) -> usize {\n\t\tself.diff.as_ref().map_or(0, |diff| diff.lines)\n\t}\n\n\tfn max_scroll_right(&self) -> usize {\n\t\tself.longest_line\n\t\t\t.saturating_sub(self.current_size.get().0.into())\n\t}\n\n\tfn modify_selection(&mut self, direction: Direction) {\n\t\tif self.diff.is_some() {\n\t\t\tself.selection.modify(direction, self.lines_count());\n\t\t}\n\t}\n\n\tfn copy_selection(&self) {\n\t\tif let Some(diff) = &self.diff {\n\t\t\tlet lines_to_copy: Vec<&str> =\n\t\t\t\tdiff.hunks\n\t\t\t\t\t.iter()\n\t\t\t\t\t.flat_map(|hunk| hunk.lines.iter())\n\t\t\t\t\t.enumerate()\n\t\t\t\t\t.filter_map(|(i, line)| {\n\t\t\t\t\t\tif self.selection.contains(i) {\n\t\t\t\t\t\t\tSome(line.content.trim_matches(|c| {\n\t\t\t\t\t\t\t\tc == '\\n' || c == '\\r'\n\t\t\t\t\t\t\t}))\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tNone\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t\t.collect();\n\n\t\t\ttry_or_popup!(\n\t\t\t\tself,\n\t\t\t\t\"copy to clipboard error:\",\n\t\t\t\tcrate::clipboard::copy_string(\n\t\t\t\t\t&lines_to_copy.join(\"\\n\")\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\t}\n\n\tfn find_selected_hunk(\n\t\tdiff: &FileDiff,\n\t\tline_selected: usize,\n\t) -> Option<usize> {\n\t\tlet mut line_cursor = 0_usize;\n\t\tfor (i, hunk) in diff.hunks.iter().enumerate() {\n\t\t\tlet hunk_len = hunk.lines.len();\n\t\t\tlet hunk_min = line_cursor;\n\t\t\tlet hunk_max = line_cursor + hunk_len;\n\n\t\t\tlet hunk_selected =\n\t\t\t\thunk_min <= line_selected && hunk_max > line_selected;\n\n\t\t\tif hunk_selected {\n\t\t\t\treturn Some(i);\n\t\t\t}\n\n\t\t\tline_cursor += hunk_len;\n\t\t}\n\n\t\tNone\n\t}\n\n\tfn get_text(&self, width: u16, height: u16) -> Vec<Line<'_>> {\n\t\tif let Some(diff) = &self.diff {\n\t\t\treturn if diff.hunks.is_empty() {\n\t\t\t\tself.get_text_binary(diff)\n\t\t\t} else {\n\t\t\t\tlet mut res: Vec<Line> = Vec::new();\n\n\t\t\t\tlet min = self.vertical_scroll.get_top();\n\t\t\t\tlet max = min + height as usize;\n\n\t\t\t\tlet mut line_cursor = 0_usize;\n\t\t\t\tlet mut lines_added = 0_usize;\n\n\t\t\t\tfor (i, hunk) in diff.hunks.iter().enumerate() {\n\t\t\t\t\tlet hunk_selected = self.focused()\n\t\t\t\t\t\t&& self.selected_hunk.is_some_and(|s| s == i);\n\n\t\t\t\t\tif lines_added >= height as usize {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tlet hunk_len = hunk.lines.len();\n\t\t\t\t\tlet hunk_min = line_cursor;\n\t\t\t\t\tlet hunk_max = line_cursor + hunk_len;\n\n\t\t\t\t\tif Self::hunk_visible(\n\t\t\t\t\t\thunk_min, hunk_max, min, max,\n\t\t\t\t\t) {\n\t\t\t\t\t\tfor (i, line) in hunk.lines.iter().enumerate()\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tif line_cursor >= min\n\t\t\t\t\t\t\t\t&& line_cursor <= max\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tres.push(Self::get_line_to_add(\n\t\t\t\t\t\t\t\t\twidth,\n\t\t\t\t\t\t\t\t\tline,\n\t\t\t\t\t\t\t\t\tself.focused()\n\t\t\t\t\t\t\t\t\t\t&& self\n\t\t\t\t\t\t\t\t\t\t\t.selection\n\t\t\t\t\t\t\t\t\t\t\t.contains(line_cursor),\n\t\t\t\t\t\t\t\t\thunk_selected,\n\t\t\t\t\t\t\t\t\ti == hunk_len - 1,\n\t\t\t\t\t\t\t\t\t&self.theme,\n\t\t\t\t\t\t\t\t\tself.horizontal_scroll\n\t\t\t\t\t\t\t\t\t\t.get_right(),\n\t\t\t\t\t\t\t\t));\n\t\t\t\t\t\t\t\tlines_added += 1;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tline_cursor += 1;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tline_cursor += hunk_len;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tres\n\t\t\t};\n\t\t}\n\n\t\tvec![]\n\t}\n\n\tfn get_text_binary(&self, diff: &FileDiff) -> Vec<Line<'_>> {\n\t\tlet is_positive = diff.size_delta >= 0;\n\t\tlet delta_byte_size =\n\t\t\tByteSize::b(diff.size_delta.unsigned_abs());\n\t\tlet sign = if is_positive { \"+\" } else { \"-\" };\n\t\tvec![Line::from(vec![\n\t\t\tSpan::raw(Cow::from(\"size: \")),\n\t\t\tSpan::styled(\n\t\t\t\tCow::from(format!(\"{}\", ByteSize::b(diff.sizes.0))),\n\t\t\t\tself.theme.text(false, false),\n\t\t\t),\n\t\t\tSpan::raw(Cow::from(\" -> \")),\n\t\t\tSpan::styled(\n\t\t\t\tCow::from(format!(\"{}\", ByteSize::b(diff.sizes.1))),\n\t\t\t\tself.theme.text(false, false),\n\t\t\t),\n\t\t\tSpan::raw(Cow::from(\" (\")),\n\t\t\tSpan::styled(\n\t\t\t\tCow::from(format!(\"{sign}{delta_byte_size:}\")),\n\t\t\t\tself.theme.diff_line(\n\t\t\t\t\tif is_positive {\n\t\t\t\t\t\tDiffLineType::Add\n\t\t\t\t\t} else {\n\t\t\t\t\t\tDiffLineType::Delete\n\t\t\t\t\t},\n\t\t\t\t\tfalse,\n\t\t\t\t),\n\t\t\t),\n\t\t\tSpan::raw(Cow::from(\")\")),\n\t\t])]\n\t}\n\n\tfn get_line_to_add<'a>(\n\t\twidth: u16,\n\t\tline: &'a DiffLine,\n\t\tselected: bool,\n\t\tselected_hunk: bool,\n\t\tend_of_hunk: bool,\n\t\ttheme: &SharedTheme,\n\t\tscrolled_right: usize,\n\t) -> Line<'a> {\n\t\tlet style = theme.diff_hunk_marker(selected_hunk);\n\n\t\tlet is_content_line =\n\t\t\tmatches!(line.line_type, DiffLineType::None);\n\n\t\tlet left_side_of_line = if end_of_hunk {\n\t\t\tSpan::styled(Cow::from(symbols::line::BOTTOM_LEFT), style)\n\t\t} else {\n\t\t\tmatch line.line_type {\n\t\t\t\tDiffLineType::Header => Span::styled(\n\t\t\t\t\tCow::from(symbols::line::TOP_LEFT),\n\t\t\t\t\tstyle,\n\t\t\t\t),\n\t\t\t\t_ => Span::styled(\n\t\t\t\t\tCow::from(symbols::line::VERTICAL),\n\t\t\t\t\tstyle,\n\t\t\t\t),\n\t\t\t}\n\t\t};\n\n\t\tlet content =\n\t\t\tif !is_content_line && line.content.as_ref().is_empty() {\n\t\t\t\ttheme.line_break()\n\t\t\t} else {\n\t\t\t\ttabs_to_spaces(line.content.as_ref().to_string())\n\t\t\t};\n\t\tlet content = trim_offset(&content, scrolled_right);\n\n\t\tlet filled = if selected {\n\t\t\t// selected line\n\t\t\tformat!(\"{content:w$}\\n\", w = width as usize)\n\t\t} else {\n\t\t\t// weird eof missing eol line\n\t\t\tformat!(\"{content}\\n\")\n\t\t};\n\n\t\tLine::from(vec![\n\t\t\tleft_side_of_line,\n\t\t\tSpan::styled(\n\t\t\t\tCow::from(filled),\n\t\t\t\ttheme.diff_line(line.line_type, selected),\n\t\t\t),\n\t\t])\n\t}\n\n\tconst fn hunk_visible(\n\t\thunk_min: usize,\n\t\thunk_max: usize,\n\t\tmin: usize,\n\t\tmax: usize,\n\t) -> bool {\n\t\t// full overlap\n\t\tif hunk_min <= min && hunk_max >= max {\n\t\t\treturn true;\n\t\t}\n\n\t\t// partly overlap\n\t\tif (hunk_min >= min && hunk_min <= max)\n\t\t\t|| (hunk_max >= min && hunk_max <= max)\n\t\t{\n\t\t\treturn true;\n\t\t}\n\n\t\tfalse\n\t}\n\n\tfn unstage_hunk(&self) -> Result<()> {\n\t\tif let Some(diff) = &self.diff {\n\t\t\tif let Some(hunk) = self.selected_hunk {\n\t\t\t\tlet hash = diff.hunks[hunk].header_hash;\n\t\t\t\tsync::unstage_hunk(\n\t\t\t\t\t&self.repo.borrow(),\n\t\t\t\t\t&self.current.path,\n\t\t\t\t\thash,\n\t\t\t\t\tSome(self.options.borrow().diff_options()),\n\t\t\t\t)?;\n\t\t\t\tself.queue_update();\n\t\t\t}\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn stage_hunk(&self) -> Result<()> {\n\t\tif let Some(diff) = &self.diff {\n\t\t\tif let Some(hunk) = self.selected_hunk {\n\t\t\t\tif diff.untracked {\n\t\t\t\t\tsync::stage_add_file(\n\t\t\t\t\t\t&self.repo.borrow(),\n\t\t\t\t\t\tPath::new(&self.current.path),\n\t\t\t\t\t)?;\n\t\t\t\t} else {\n\t\t\t\t\tlet hash = diff.hunks[hunk].header_hash;\n\t\t\t\t\tsync::stage_hunk(\n\t\t\t\t\t\t&self.repo.borrow(),\n\t\t\t\t\t\t&self.current.path,\n\t\t\t\t\t\thash,\n\t\t\t\t\t\tSome(self.options.borrow().diff_options()),\n\t\t\t\t\t)?;\n\t\t\t\t}\n\n\t\t\t\tself.queue_update();\n\t\t\t}\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn queue_update(&self) {\n\t\tself.queue.push(InternalEvent::Update(NeedsUpdate::ALL));\n\t}\n\n\tfn reset_hunk(&self) {\n\t\tif let Some(diff) = &self.diff {\n\t\t\tif let Some(hunk) = self.selected_hunk {\n\t\t\t\tlet hash = diff.hunks[hunk].header_hash;\n\n\t\t\t\tself.queue.push(InternalEvent::ConfirmAction(\n\t\t\t\t\tAction::ResetHunk(\n\t\t\t\t\t\tself.current.path.clone(),\n\t\t\t\t\t\thash,\n\t\t\t\t\t),\n\t\t\t\t));\n\t\t\t}\n\t\t}\n\t}\n\n\tfn reset_lines(&self) {\n\t\tself.queue.push(InternalEvent::ConfirmAction(\n\t\t\tAction::ResetLines(\n\t\t\t\tself.current.path.clone(),\n\t\t\t\tself.selected_lines(),\n\t\t\t),\n\t\t));\n\t}\n\n\tfn stage_lines(&self) {\n\t\tif let Some(diff) = &self.diff {\n\t\t\t//TODO: support untracked files as well\n\t\t\tif !diff.untracked {\n\t\t\t\tlet selected_lines = self.selected_lines();\n\n\t\t\t\ttry_or_popup!(\n\t\t\t\t\tself,\n\t\t\t\t\t\"(un)stage lines:\",\n\t\t\t\t\tsync::stage_lines(\n\t\t\t\t\t\t&self.repo.borrow(),\n\t\t\t\t\t\t&self.current.path,\n\t\t\t\t\t\tself.is_stage(),\n\t\t\t\t\t\t&selected_lines,\n\t\t\t\t\t)\n\t\t\t\t);\n\n\t\t\t\tself.queue_update();\n\t\t\t}\n\t\t}\n\t}\n\n\tfn selected_lines(&self) -> Vec<DiffLinePosition> {\n\t\tself.diff\n\t\t\t.as_ref()\n\t\t\t.map(|diff| {\n\t\t\t\tdiff.hunks\n\t\t\t\t\t.iter()\n\t\t\t\t\t.flat_map(|hunk| hunk.lines.iter())\n\t\t\t\t\t.enumerate()\n\t\t\t\t\t.filter_map(|(i, line)| {\n\t\t\t\t\t\tlet is_add_or_delete = line.line_type\n\t\t\t\t\t\t\t== DiffLineType::Add\n\t\t\t\t\t\t\t|| line.line_type == DiffLineType::Delete;\n\t\t\t\t\t\tif self.selection.contains(i)\n\t\t\t\t\t\t\t&& is_add_or_delete\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tSome(line.position)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tNone\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t\t.collect()\n\t\t\t})\n\t\t\t.unwrap_or_default()\n\t}\n\n\tfn reset_untracked(&self) {\n\t\tself.queue.push(InternalEvent::ConfirmAction(Action::Reset(\n\t\t\tResetItem {\n\t\t\t\tpath: self.current.path.clone(),\n\t\t\t},\n\t\t)));\n\t}\n\n\tfn stage_unstage_hunk(&self) -> Result<()> {\n\t\tif self.current.is_stage {\n\t\t\tself.unstage_hunk()?;\n\t\t} else {\n\t\t\tself.stage_hunk()?;\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn calc_hunk_move_target(\n\t\t&self,\n\t\tdirection: isize,\n\t) -> Option<usize> {\n\t\tlet diff = self.diff.as_ref()?;\n\t\tif diff.hunks.is_empty() {\n\t\t\treturn None;\n\t\t}\n\t\tlet max = diff.hunks.len() - 1;\n\t\tlet target_index = self.selected_hunk.map_or(0, |i| {\n\t\t\tlet target = if direction >= 0 {\n\t\t\t\ti.saturating_add(direction.unsigned_abs())\n\t\t\t} else {\n\t\t\t\ti.saturating_sub(direction.unsigned_abs())\n\t\t\t};\n\t\t\tstd::cmp::min(max, target)\n\t\t});\n\t\tSome(target_index)\n\t}\n\n\tfn diff_hunk_move_up_down(&mut self, direction: isize) {\n\t\tlet Some(diff) = &self.diff else { return };\n\t\tlet hunk_index = self.calc_hunk_move_target(direction);\n\t\t// return if selected_hunk not change\n\t\tif self.selected_hunk == hunk_index {\n\t\t\treturn;\n\t\t}\n\t\tif let Some(hunk_index) = hunk_index {\n\t\t\tlet line_index = diff\n\t\t\t\t.hunks\n\t\t\t\t.iter()\n\t\t\t\t.take(hunk_index)\n\t\t\t\t.fold(0, |sum, hunk| sum + hunk.lines.len());\n\t\t\tlet hunk = &diff.hunks[hunk_index];\n\t\t\tself.selection = Selection::Single(line_index);\n\t\t\tself.selected_hunk = Some(hunk_index);\n\t\t\tself.vertical_scroll.move_area_to_visible(\n\t\t\t\tself.current_size.get().1 as usize,\n\t\t\t\tline_index,\n\t\t\t\tline_index.saturating_add(hunk.lines.len()),\n\t\t\t);\n\t\t}\n\t}\n\n\tconst fn is_stage(&self) -> bool {\n\t\tself.current.is_stage\n\t}\n}\n\nimpl DrawableComponent for DiffComponent {\n\tfn draw(&self, f: &mut Frame, r: Rect) -> Result<()> {\n\t\tself.current_size.set((\n\t\t\tr.width.saturating_sub(2),\n\t\t\tr.height.saturating_sub(2),\n\t\t));\n\n\t\tlet current_width = self.current_size.get().0;\n\t\tlet current_height = self.current_size.get().1;\n\n\t\tself.vertical_scroll.update(\n\t\t\tself.selection.get_end(),\n\t\t\tself.lines_count(),\n\t\t\tusize::from(current_height),\n\t\t);\n\n\t\tself.horizontal_scroll.update_no_selection(\n\t\t\tself.longest_line,\n\t\t\tcurrent_width.into(),\n\t\t);\n\n\t\tlet title = format!(\n\t\t\t\"{}{}\",\n\t\t\tstrings::title_diff(&self.key_config),\n\t\t\tself.current.path\n\t\t);\n\n\t\tlet txt = if self.pending {\n\t\t\tvec![Line::from(vec![Span::styled(\n\t\t\t\tCow::from(strings::loading_text(&self.key_config)),\n\t\t\t\tself.theme.text(false, false),\n\t\t\t)])]\n\t\t} else {\n\t\t\tself.get_text(r.width, current_height)\n\t\t};\n\n\t\tf.render_widget(\n\t\t\tParagraph::new(txt).block(\n\t\t\t\tBlock::default()\n\t\t\t\t\t.title(Span::styled(\n\t\t\t\t\t\ttitle.as_str(),\n\t\t\t\t\t\tself.theme.title(self.focused()),\n\t\t\t\t\t))\n\t\t\t\t\t.borders(Borders::ALL)\n\t\t\t\t\t.border_style(self.theme.block(self.focused())),\n\t\t\t),\n\t\t\tr,\n\t\t);\n\n\t\tif self.focused() {\n\t\t\tself.vertical_scroll.draw(f, r, &self.theme);\n\n\t\t\tif self.max_scroll_right() > 0 {\n\t\t\t\tself.horizontal_scroll.draw(f, r, &self.theme);\n\t\t\t}\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for DiffComponent {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\t_force_all: bool,\n\t) -> CommandBlocking {\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::scroll(&self.key_config),\n\t\t\tself.can_scroll(),\n\t\t\tself.focused(),\n\t\t));\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::diff_hunk_next(&self.key_config),\n\t\t\tself.calc_hunk_move_target(1) != self.selected_hunk,\n\t\t\tself.focused(),\n\t\t));\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::diff_hunk_prev(&self.key_config),\n\t\t\tself.calc_hunk_move_target(-1) != self.selected_hunk,\n\t\t\tself.focused(),\n\t\t));\n\t\tout.push(\n\t\t\tCommandInfo::new(\n\t\t\t\tstrings::commands::diff_home_end(&self.key_config),\n\t\t\t\tself.can_scroll(),\n\t\t\t\tself.focused(),\n\t\t\t)\n\t\t\t.hidden(),\n\t\t);\n\n\t\tif !self.is_immutable {\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::diff_hunk_remove(&self.key_config),\n\t\t\t\tself.selected_hunk.is_some(),\n\t\t\t\tself.focused() && self.is_stage(),\n\t\t\t));\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::diff_hunk_add(&self.key_config),\n\t\t\t\tself.selected_hunk.is_some(),\n\t\t\t\tself.focused() && !self.is_stage(),\n\t\t\t));\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::diff_hunk_revert(&self.key_config),\n\t\t\t\tself.selected_hunk.is_some(),\n\t\t\t\tself.focused() && !self.is_stage(),\n\t\t\t));\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::diff_lines_revert(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t),\n\t\t\t\t//TODO: only if any modifications are selected\n\t\t\t\ttrue,\n\t\t\t\tself.focused() && !self.is_stage(),\n\t\t\t));\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::diff_lines_stage(&self.key_config),\n\t\t\t\t//TODO: only if any modifications are selected\n\t\t\t\ttrue,\n\t\t\t\tself.focused() && !self.is_stage(),\n\t\t\t));\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::diff_lines_unstage(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t),\n\t\t\t\t//TODO: only if any modifications are selected\n\t\t\t\ttrue,\n\t\t\t\tself.focused() && self.is_stage(),\n\t\t\t));\n\t\t}\n\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::copy(&self.key_config),\n\t\t\ttrue,\n\t\t\tself.focused(),\n\t\t));\n\n\t\tCommandBlocking::PassingOn\n\t}\n\n\t#[allow(clippy::cognitive_complexity, clippy::too_many_lines)]\n\tfn event(&mut self, ev: &Event) -> Result<EventState> {\n\t\tif self.focused() {\n\t\t\tif let Event::Key(e) = ev {\n\t\t\t\treturn if key_match(e, self.key_config.keys.move_down)\n\t\t\t\t{\n\t\t\t\t\tself.move_selection(ScrollType::Down);\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(\n\t\t\t\t\te,\n\t\t\t\t\tself.key_config.keys.shift_down,\n\t\t\t\t) {\n\t\t\t\t\tself.modify_selection(Direction::Down);\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(e, self.key_config.keys.shift_up)\n\t\t\t\t{\n\t\t\t\t\tself.modify_selection(Direction::Up);\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(e, self.key_config.keys.end) {\n\t\t\t\t\tself.move_selection(ScrollType::End);\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(e, self.key_config.keys.home) {\n\t\t\t\t\tself.move_selection(ScrollType::Home);\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(e, self.key_config.keys.move_up) {\n\t\t\t\t\tself.move_selection(ScrollType::Up);\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(e, self.key_config.keys.page_up) {\n\t\t\t\t\tself.move_selection(ScrollType::PageUp);\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(e, self.key_config.keys.page_down)\n\t\t\t\t{\n\t\t\t\t\tself.move_selection(ScrollType::PageDown);\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(\n\t\t\t\t\te,\n\t\t\t\t\tself.key_config.keys.move_right,\n\t\t\t\t) {\n\t\t\t\t\tself.horizontal_scroll\n\t\t\t\t\t\t.move_right(HorizontalScrollType::Right);\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(e, self.key_config.keys.move_left)\n\t\t\t\t{\n\t\t\t\t\tself.horizontal_scroll\n\t\t\t\t\t\t.move_right(HorizontalScrollType::Left);\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(\n\t\t\t\t\te,\n\t\t\t\t\tself.key_config.keys.diff_hunk_next,\n\t\t\t\t) {\n\t\t\t\t\tself.diff_hunk_move_up_down(1);\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(\n\t\t\t\t\te,\n\t\t\t\t\tself.key_config.keys.diff_hunk_prev,\n\t\t\t\t) {\n\t\t\t\t\tself.diff_hunk_move_up_down(-1);\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(\n\t\t\t\t\te,\n\t\t\t\t\tself.key_config.keys.stage_unstage_item,\n\t\t\t\t) && !self.is_immutable\n\t\t\t\t{\n\t\t\t\t\ttry_or_popup!(\n\t\t\t\t\t\tself,\n\t\t\t\t\t\t\"hunk error:\",\n\t\t\t\t\t\tself.stage_unstage_hunk()\n\t\t\t\t\t);\n\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(\n\t\t\t\t\te,\n\t\t\t\t\tself.key_config.keys.status_reset_item,\n\t\t\t\t) && !self.is_immutable\n\t\t\t\t\t&& !self.is_stage()\n\t\t\t\t{\n\t\t\t\t\tif let Some(diff) = &self.diff {\n\t\t\t\t\t\tif diff.untracked {\n\t\t\t\t\t\t\tself.reset_untracked();\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tself.reset_hunk();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(\n\t\t\t\t\te,\n\t\t\t\t\tself.key_config.keys.diff_stage_lines,\n\t\t\t\t) && !self.is_immutable\n\t\t\t\t{\n\t\t\t\t\tself.stage_lines();\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(\n\t\t\t\t\te,\n\t\t\t\t\tself.key_config.keys.diff_reset_lines,\n\t\t\t\t) && !self.is_immutable\n\t\t\t\t\t&& !self.is_stage()\n\t\t\t\t{\n\t\t\t\t\tif let Some(diff) = &self.diff {\n\t\t\t\t\t\t//TODO: reset untracked lines\n\t\t\t\t\t\tif !diff.untracked {\n\t\t\t\t\t\t\tself.reset_lines();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(e, self.key_config.keys.copy) {\n\t\t\t\t\tself.copy_selection();\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else {\n\t\t\t\t\tOk(EventState::NotConsumed)\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn focused(&self) -> bool {\n\t\tself.focused\n\t}\n\tfn focus(&mut self, focus: bool) {\n\t\tself.focused = focus;\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::ui::style::Theme;\n\tuse std::io::Write;\n\tuse std::rc::Rc;\n\tuse tempfile::NamedTempFile;\n\n\t#[test]\n\tfn test_line_break() {\n\t\tlet diff_line = DiffLine {\n\t\t\tcontent: \"\".into(),\n\t\t\tline_type: DiffLineType::Add,\n\t\t\tposition: Default::default(),\n\t\t};\n\n\t\t{\n\t\t\tlet default_theme = Rc::new(Theme::default());\n\n\t\t\tassert_eq!(\n\t\t\t\tDiffComponent::get_line_to_add(\n\t\t\t\t\t4,\n\t\t\t\t\t&diff_line,\n\t\t\t\t\tfalse,\n\t\t\t\t\tfalse,\n\t\t\t\t\tfalse,\n\t\t\t\t\t&default_theme,\n\t\t\t\t\t0\n\t\t\t\t)\n\t\t\t\t.spans\n\t\t\t\t.last()\n\t\t\t\t.unwrap(),\n\t\t\t\t&Span::styled(\n\t\t\t\t\tCow::from(\"¶\\n\"),\n\t\t\t\t\tdefault_theme\n\t\t\t\t\t\t.diff_line(diff_line.line_type, false)\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\t{\n\t\t\tlet mut file = NamedTempFile::new().unwrap();\n\n\t\t\twriteln!(\n\t\t\t\tfile,\n\t\t\t\tr#\"\n(\n\tline_break: Some(\"+\")\n)\n\"#\n\t\t\t)\n\t\t\t.unwrap();\n\n\t\t\tlet theme =\n\t\t\t\tRc::new(Theme::init(&file.path().to_path_buf()));\n\n\t\t\tassert_eq!(\n\t\t\t\tDiffComponent::get_line_to_add(\n\t\t\t\t\t4, &diff_line, false, false, false, &theme, 0\n\t\t\t\t)\n\t\t\t\t.spans\n\t\t\t\t.last()\n\t\t\t\t.unwrap(),\n\t\t\t\t&Span::styled(\n\t\t\t\t\tCow::from(\"+\\n\"),\n\t\t\t\t\ttheme.diff_line(diff_line.line_type, false)\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/components/mod.rs",
    "content": "/*!\nComponents are the visible building blocks in gitui.\n\nThey have a state, handle events, and render to the terminal:\n\n* Some are full screen. That would be all the [`tabs`](super::tabs).\n* Some look like panels, eg [`CommitDetailsComponent`]\n* Some overlap others. They are collected in module [`popups`](super::popups)\n* Some are decorations, eg [`HorizontalScroll`](utils::scroll_horizontal::HorizontalScroll).\n\nComponents can be reused.\nFor example, [`CommitList`] is used in both tab \"revlog\" and tab \"stashlist\".\n\n\n## Composition\n\nIn gitui, composition is driven by code. This means each component must\nhave code that explicitly forwards component function calls like draw,\ncommands and event to the components it is composed of.\n\nOther systems use composition by data: They provide a generic data structure\nthat reflects the visual hierarchy, and uses it at runtime to\ndetermine which code should be executed. This is not how gitui works.\n\n## Traits\n\nThere are two traits defined here:\n* [`Component`] handles events from the user,\n* [`DrawableComponent`] renders to the terminal.\n\nIn the current codebase these are always implemented together, and it probably\nmakes more sense to merge them some time in the future.\nIt is a little strange that you implement `draw()` on a `DrawableComponent`,\nbut have function `hide()` from trait Component which does not know how\nto `draw()`.\n*/\n\nmod changes;\nmod command;\nmod commit_details;\nmod commitlist;\nmod cred;\nmod diff;\nmod revision_files;\nmod status_tree;\nmod syntax_text;\nmod textinput;\nmod utils;\n\npub use self::status_tree::StatusTreeComponent;\npub use changes::ChangesComponent;\npub use command::{CommandInfo, CommandText};\npub use commit_details::CommitDetailsComponent;\npub use commitlist::CommitList;\npub use cred::CredComponent;\npub use diff::DiffComponent;\npub use revision_files::RevisionFilesComponent;\npub use syntax_text::SyntaxTextComponent;\npub use textinput::{InputType, TextInputComponent};\npub use utils::{\n\tfiletree::FileTreeItemKind, logitems::ItemBatch,\n\tscroll_vertical::VerticalScroll, string_width_align,\n\ttime_to_string,\n};\n\nuse crate::ui::style::Theme;\nuse anyhow::Result;\nuse crossterm::event::Event;\nuse ratatui::{\n\tlayout::{Alignment, Rect},\n\ttext::{Span, Text},\n\twidgets::{Block, Borders, Paragraph},\n\tFrame,\n};\n\n/// creates accessors for a list of components\n///\n/// allows generating code to make sure\n/// we always enumerate all components in both getter functions\n#[macro_export]\nmacro_rules! accessors {\n    ($self:ident, [$($element:ident),+]) => {\n        fn components(& $self) -> Vec<&dyn Component> {\n            vec![\n                $(&$self.$element,)+\n            ]\n        }\n\n        fn components_mut(&mut $self) -> Vec<&mut dyn Component> {\n            vec![\n                $(&mut $self.$element,)+\n            ]\n        }\n    };\n}\n\n/// creates a function to determine if any popup is visible\n#[macro_export]\nmacro_rules! any_popup_visible {\n    ($self:ident, [$($element:ident),+]) => {\n        fn any_popup_visible(& $self) -> bool{\n            ($($self.$element.is_visible()) || +)\n        }\n    };\n}\n\n/// creates the draw popup function\n#[macro_export]\nmacro_rules! draw_popups {\n    ($self:ident, [$($element:ident),+]) => {\n        fn draw_popups(& $self, mut f: &mut Frame) -> Result<()>{\n            //TODO: move the layout part out and feed it into `draw_popups`\n            let size = Layout::default()\n            .direction(Direction::Vertical)\n            .constraints(\n                [\n                    Constraint::Min(1),\n                    Constraint::Length($self.cmdbar.borrow().height()),\n                ]\n                .as_ref(),\n            )\n            .split(f.area())[0];\n\n            ($($self.$element.draw(&mut f, size)?) , +);\n\n            return Ok(());\n        }\n    };\n}\n\n/// simply calls\n/// `any_popup_visible`!() and `draw_popups`!() macros\n#[macro_export]\nmacro_rules! setup_popups {\n    ($self:ident, [$($element:ident),+]) => {\n        $crate::any_popup_visible!($self, [$($element),+]);\n        $crate::draw_popups!($self, [ $($element),+ ]);\n    };\n}\n\n/// returns `true` if event was consumed\npub fn event_pump(\n\tev: &Event,\n\tcomponents: &mut [&mut dyn Component],\n) -> Result<EventState> {\n\tfor c in components {\n\t\tif c.event(ev)?.is_consumed() {\n\t\t\treturn Ok(EventState::Consumed);\n\t\t}\n\t}\n\n\tOk(EventState::NotConsumed)\n}\n\n/// helper fn to simplify delegating command\n/// gathering down into child components\n/// see `event_pump`,`accessors`\npub fn command_pump(\n\tout: &mut Vec<CommandInfo>,\n\tforce_all: bool,\n\tcomponents: &[&dyn Component],\n) {\n\tfor c in components {\n\t\tif c.commands(out, force_all) != CommandBlocking::PassingOn\n\t\t\t&& !force_all\n\t\t{\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\n#[derive(Copy, Clone)]\npub enum ScrollType {\n\tUp,\n\tDown,\n\tHome,\n\tEnd,\n\tPageUp,\n\tPageDown,\n}\n\n#[derive(Copy, Clone)]\npub enum HorizontalScrollType {\n\tLeft,\n\tRight,\n}\n\n#[derive(Copy, Clone)]\npub enum Direction {\n\tUp,\n\tDown,\n}\n\n///\n#[derive(PartialEq, Eq)]\npub enum CommandBlocking {\n\tBlocking,\n\tPassingOn,\n}\n\n///\npub fn visibility_blocking<T: Component>(\n\tcomp: &T,\n) -> CommandBlocking {\n\tif comp.is_visible() {\n\t\tCommandBlocking::Blocking\n\t} else {\n\t\tCommandBlocking::PassingOn\n\t}\n}\n\n///\npub trait DrawableComponent {\n\t///\n\tfn draw(&self, f: &mut Frame, rect: Rect) -> Result<()>;\n}\n\n///\n#[derive(PartialEq, Eq)]\npub enum EventState {\n\tConsumed,\n\tNotConsumed,\n}\n\n#[derive(Copy, Clone)]\npub enum FuzzyFinderTarget {\n\tBranches,\n\tFiles,\n}\n\nimpl EventState {\n\tpub fn is_consumed(&self) -> bool {\n\t\t*self == Self::Consumed\n\t}\n}\n\nimpl From<bool> for EventState {\n\tfn from(consumed: bool) -> Self {\n\t\tif consumed {\n\t\t\tSelf::Consumed\n\t\t} else {\n\t\t\tSelf::NotConsumed\n\t\t}\n\t}\n}\n\n/// base component trait\npub trait Component {\n\t///\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking;\n\n\t///\n\tfn event(&mut self, ev: &Event) -> Result<EventState>;\n\n\t///\n\tfn focused(&self) -> bool {\n\t\tfalse\n\t}\n\t/// focus/unfocus this component depending on param\n\tfn focus(&mut self, _focus: bool) {}\n\t///\n\tfn is_visible(&self) -> bool {\n\t\ttrue\n\t}\n\t///\n\tfn hide(&mut self) {}\n\t///\n\tfn show(&mut self) -> Result<()> {\n\t\tOk(())\n\t}\n\n\t///\n\tfn toggle_visible(&mut self) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tself.hide();\n\t\t\tOk(())\n\t\t} else {\n\t\t\tself.show()\n\t\t}\n\t}\n}\n\nfn dialog_paragraph<'a>(\n\ttitle: &'a str,\n\tcontent: Text<'a>,\n\ttheme: &Theme,\n\tfocused: bool,\n) -> Paragraph<'a> {\n\tParagraph::new(content)\n\t\t.block(\n\t\t\tBlock::default()\n\t\t\t\t.title(Span::styled(title, theme.title(focused)))\n\t\t\t\t.borders(Borders::ALL)\n\t\t\t\t.border_style(theme.block(focused)),\n\t\t)\n\t\t.alignment(Alignment::Left)\n}\n"
  },
  {
    "path": "src/components/revision_files.rs",
    "content": "use super::{\n\tutils::scroll_vertical::VerticalScroll, CommandBlocking,\n\tCommandInfo, Component, DrawableComponent, EventState,\n\tFuzzyFinderTarget, SyntaxTextComponent,\n};\nuse crate::{\n\tapp::Environment,\n\tkeys::{key_match, SharedKeyConfig},\n\tpopups::{BlameFileOpen, FileRevOpen},\n\tqueue::{InternalEvent, Queue, StackablePopupOpen},\n\tstrings::{self, order, symbol},\n\ttry_or_popup,\n\tui::{self, common_nav, style::SharedTheme},\n\tAsyncNotification,\n};\nuse anyhow::Result;\nuse asyncgit::{\n\tasyncjob::AsyncSingleJob,\n\tsync::{\n\t\tget_commit_info, CommitId, CommitInfo, RepoPathRef, TreeFile,\n\t},\n\tAsyncGitNotification, AsyncTreeFilesJob,\n};\nuse crossterm::event::Event;\nuse filetreelist::{FileTree, FileTreeItem};\nuse ratatui::{\n\tlayout::{Constraint, Direction, Layout, Rect},\n\ttext::Span,\n\twidgets::{Block, Borders},\n\tFrame,\n};\nuse std::{borrow::Cow, fmt::Write};\nuse std::{\n\tcollections::BTreeSet,\n\tpath::{Path, PathBuf},\n};\nuse unicode_truncate::UnicodeTruncateStr;\nuse unicode_width::UnicodeWidthStr;\n\nenum Focus {\n\tTree,\n\tFile,\n}\n\npub struct RevisionFilesComponent {\n\trepo: RepoPathRef,\n\tqueue: Queue,\n\ttheme: SharedTheme,\n\t//TODO: store TreeFiles in `tree`\n\tfiles: Option<Vec<TreeFile>>,\n\tasync_treefiles: AsyncSingleJob<AsyncTreeFilesJob>,\n\tcurrent_file: SyntaxTextComponent,\n\ttree: FileTree,\n\tscroll: VerticalScroll,\n\tvisible: bool,\n\trevision: Option<CommitInfo>,\n\tfocus: Focus,\n\tkey_config: SharedKeyConfig,\n\tselect_file: Option<PathBuf>,\n}\n\nimpl RevisionFilesComponent {\n\t///\n\tpub fn new(\n\t\tenv: &Environment,\n\t\tselect_file: Option<PathBuf>,\n\t) -> Self {\n\t\tSelf {\n\t\t\tqueue: env.queue.clone(),\n\t\t\ttree: FileTree::default(),\n\t\t\tscroll: VerticalScroll::new(),\n\t\t\tcurrent_file: SyntaxTextComponent::new(env),\n\t\t\ttheme: env.theme.clone(),\n\t\t\tfiles: None,\n\t\t\tasync_treefiles: AsyncSingleJob::new(\n\t\t\t\tenv.sender_git.clone(),\n\t\t\t),\n\t\t\trevision: None,\n\t\t\tfocus: Focus::Tree,\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\trepo: env.repo.clone(),\n\t\t\tselect_file,\n\t\t\tvisible: false,\n\t\t}\n\t}\n\n\t///\n\tpub fn set_commit(&mut self, commit: CommitId) -> Result<()> {\n\t\tself.show()?;\n\n\t\tlet same_id =\n\t\t\tself.revision.as_ref().is_some_and(|c| c.id == commit);\n\n\t\tif !same_id {\n\t\t\tself.files = None;\n\n\t\t\tself.request_files(commit);\n\n\t\t\tself.revision =\n\t\t\t\tSome(get_commit_info(&self.repo.borrow(), &commit)?);\n\t\t}\n\n\t\tOk(())\n\t}\n\n\t///\n\tpub const fn revision(&self) -> Option<&CommitInfo> {\n\t\tself.revision.as_ref()\n\t}\n\n\t///\n\tpub fn update(&mut self, ev: AsyncNotification) -> Result<()> {\n\t\tself.current_file.update(ev);\n\n\t\tif matches!(\n\t\t\tev,\n\t\t\tAsyncNotification::Git(AsyncGitNotification::TreeFiles)\n\t\t) {\n\t\t\tself.refresh_files()?;\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn refresh_files(&mut self) -> Result<(), anyhow::Error> {\n\t\tif let Some(last) = self.async_treefiles.take_last() {\n\t\t\tif let Some(result) = last.result() {\n\t\t\t\tif self\n\t\t\t\t\t.revision\n\t\t\t\t\t.as_ref()\n\t\t\t\t\t.is_some_and(|commit| commit.id == result.commit)\n\t\t\t\t{\n\t\t\t\t\tif let Ok(last) = result.result {\n\t\t\t\t\t\tlet filenames: Vec<&Path> = last\n\t\t\t\t\t\t\t.iter()\n\t\t\t\t\t\t\t.map(|f| f.path.as_path())\n\t\t\t\t\t\t\t.collect();\n\t\t\t\t\t\tself.tree = FileTree::new(\n\t\t\t\t\t\t\t&filenames,\n\t\t\t\t\t\t\t&BTreeSet::new(),\n\t\t\t\t\t\t)?;\n\t\t\t\t\t\tself.tree.collapse_but_root();\n\n\t\t\t\t\t\tself.files = Some(last);\n\n\t\t\t\t\t\tlet select_file = self.select_file.clone();\n\t\t\t\t\t\tself.select_file = None;\n\t\t\t\t\t\tif let Some(file) = select_file {\n\t\t\t\t\t\t\tself.find_file(file.as_path());\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if let Some(rev) = &self.revision {\n\t\t\t\t\tself.request_files(rev.id);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tOk(())\n\t}\n\n\t///\n\tpub fn any_work_pending(&self) -> bool {\n\t\tself.current_file.any_work_pending()\n\t\t\t|| self.async_treefiles.is_pending()\n\t}\n\n\tfn tree_item_to_span<'a>(\n\t\titem: &'a FileTreeItem,\n\t\ttheme: &SharedTheme,\n\t\twidth: usize,\n\t\tselected: bool,\n\t) -> Span<'a> {\n\t\tlet path = item.info().path_str();\n\t\tlet indent = item.info().indent();\n\n\t\tlet indent_str = if indent == 0 {\n\t\t\tString::new()\n\t\t} else {\n\t\t\tformat!(\"{:w$}\", \" \", w = (indent as usize) * 2)\n\t\t};\n\n\t\tlet is_path = item.kind().is_path();\n\t\tlet path_arrow = if is_path {\n\t\t\tif item.kind().is_path_collapsed() {\n\t\t\t\tsymbol::FOLDER_ICON_COLLAPSED\n\t\t\t} else {\n\t\t\t\tsymbol::FOLDER_ICON_EXPANDED\n\t\t\t}\n\t\t} else {\n\t\t\tsymbol::EMPTY_STR\n\t\t};\n\n\t\tlet available_width =\n\t\t\twidth.saturating_sub(indent_str.len() + path_arrow.len());\n\n\t\tlet path = format!(\n\t\t\t\"{indent_str}{path_arrow}{path:available_width$}\"\n\t\t);\n\n\t\tSpan::styled(path, theme.file_tree_item(is_path, selected))\n\t}\n\n\tfn blame(&self) -> bool {\n\t\tself.selected_file_path().is_some_and(|path| {\n\t\t\tself.queue.push(InternalEvent::OpenPopup(\n\t\t\t\tStackablePopupOpen::BlameFile(BlameFileOpen {\n\t\t\t\t\tfile_path: path,\n\t\t\t\t\tcommit_id: self.revision.as_ref().map(|c| c.id),\n\t\t\t\t\tselection: None,\n\t\t\t\t}),\n\t\t\t));\n\n\t\t\ttrue\n\t\t})\n\t}\n\n\tfn file_history(&self) -> bool {\n\t\tself.selected_file_path().is_some_and(|path| {\n\t\t\tself.queue.push(InternalEvent::OpenPopup(\n\t\t\t\tStackablePopupOpen::FileRevlog(FileRevOpen::new(\n\t\t\t\t\tpath,\n\t\t\t\t)),\n\t\t\t));\n\n\t\t\ttrue\n\t\t})\n\t}\n\n\tfn open_finder(&self) {\n\t\tif let Some(files) = self.files.clone() {\n\t\t\tself.queue.push(InternalEvent::OpenFuzzyFinder(\n\t\t\t\tfiles\n\t\t\t\t\t.iter()\n\t\t\t\t\t.map(|a| {\n\t\t\t\t\t\ta.path\n\t\t\t\t\t\t\t.to_str()\n\t\t\t\t\t\t\t.unwrap_or_default()\n\t\t\t\t\t\t\t.to_string()\n\t\t\t\t\t})\n\t\t\t\t\t.collect(),\n\t\t\t\tFuzzyFinderTarget::Files,\n\t\t\t));\n\t\t}\n\t}\n\n\tpub fn find_file(&mut self, file: &Path) {\n\t\tself.tree.collapse_but_root();\n\t\tif self.tree.select_file(file) {\n\t\t\tself.selection_changed();\n\t\t}\n\t}\n\n\tfn selected_file_path_with_prefix(&self) -> Option<String> {\n\t\tself.tree\n\t\t\t.selected_file()\n\t\t\t.map(|file| file.full_path_str().to_string())\n\t}\n\n\tfn selected_file_path(&self) -> Option<String> {\n\t\tself.tree.selected_file().map(|file| {\n\t\t\tfile.full_path_str()\n\t\t\t\t.strip_prefix(\"./\")\n\t\t\t\t.unwrap_or_default()\n\t\t\t\t.to_string()\n\t\t})\n\t}\n\n\tfn selection_changed(&mut self) {\n\t\t//TODO: retrieve TreeFile from tree datastructure\n\t\tif let Some(file) = self.selected_file_path_with_prefix() {\n\t\t\tif let Some(files) = &self.files {\n\t\t\t\tlet path = Path::new(&file);\n\t\t\t\tif let Some(item) =\n\t\t\t\t\tfiles.iter().find(|f| f.path == path)\n\t\t\t\t{\n\t\t\t\t\tif let Ok(path) = path.strip_prefix(\"./\") {\n\t\t\t\t\t\treturn self.current_file.load_file(\n\t\t\t\t\t\t\tpath.to_string_lossy().to_string(),\n\t\t\t\t\t\t\titem,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tself.current_file.clear();\n\t\t\t}\n\t\t}\n\t}\n\n\tfn draw_tree(&self, f: &mut Frame, area: Rect) -> Result<()> {\n\t\tlet tree_height = usize::from(area.height.saturating_sub(2));\n\t\tlet tree_width = usize::from(area.width);\n\n\t\tself.tree.window_height.set(Some(tree_height));\n\n\t\tself.tree.visual_selection().map_or_else(\n\t\t\t|| {\n\t\t\t\tself.scroll.reset();\n\t\t\t},\n\t\t\t|selection| {\n\t\t\t\tself.scroll.update(\n\t\t\t\t\tselection.index,\n\t\t\t\t\tselection.count,\n\t\t\t\t\ttree_height,\n\t\t\t\t);\n\t\t\t},\n\t\t);\n\n\t\tlet items = self\n\t\t\t.tree\n\t\t\t.iterate(self.scroll.get_top(), tree_height)\n\t\t\t.map(|(item, selected)| {\n\t\t\t\tSelf::tree_item_to_span(\n\t\t\t\t\titem,\n\t\t\t\t\t&self.theme,\n\t\t\t\t\ttree_width,\n\t\t\t\t\tselected,\n\t\t\t\t)\n\t\t\t});\n\n\t\tlet is_tree_focused = matches!(self.focus, Focus::Tree);\n\n\t\tlet title = self.title_within(tree_width)?;\n\t\tlet block = Block::default()\n\t\t\t.title(Span::styled(\n\t\t\t\ttitle,\n\t\t\t\tself.theme.title(is_tree_focused),\n\t\t\t))\n\t\t\t.borders(Borders::ALL)\n\t\t\t.border_style(self.theme.block(is_tree_focused));\n\n\t\tif self.files.is_some() {\n\t\t\tui::draw_list_block(f, area, block, items);\n\t\t} else {\n\t\t\tui::draw_list_block(\n\t\t\t\tf,\n\t\t\t\tarea,\n\t\t\t\tblock,\n\t\t\t\tvec![Span::styled(\n\t\t\t\t\tCow::from(strings::loading_text(\n\t\t\t\t\t\t&self.key_config,\n\t\t\t\t\t)),\n\t\t\t\t\tself.theme.text(false, false),\n\t\t\t\t)]\n\t\t\t\t.into_iter(),\n\t\t\t);\n\t\t}\n\n\t\tif is_tree_focused {\n\t\t\tself.scroll.draw(f, area, &self.theme);\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn title_within(&self, tree_width: usize) -> Result<String> {\n\t\tlet mut title = String::from(\"Files at\");\n\t\tlet message = self.revision.as_ref().and_then(|c| {\n\t\t\tlet _ignore =\n\t\t\t\twrite!(title, \" {{{}}}\", c.id.get_short_string());\n\n\t\t\tc.message.lines().next()\n\t\t});\n\n\t\tif let Some(message) = message {\n\t\t\tconst ELLIPSIS: char = '\\u{2026}'; // …\n\n\t\t\tlet available = tree_width\n\t\t\t\t.saturating_sub(title.width())\n\t\t\t\t.saturating_sub(\n\t\t\t\t\t2 /* frame end corners */ + 1 /* space */ + 2, /* square brackets */\n\t\t\t\t);\n\n\t\t\tif message.width() <= available {\n\t\t\t\twrite!(title, \" [{message}]\")?;\n\t\t\t} else if available > 1 {\n\t\t\t\twrite!(\n\t\t\t\t\ttitle,\n\t\t\t\t\t\" [{}{}]\",\n\t\t\t\t\tmessage.unicode_truncate(available - 1).0,\n\t\t\t\t\tELLIPSIS\n\t\t\t\t)?;\n\t\t\t} else {\n\t\t\t\ttitle.push(ELLIPSIS);\n\t\t\t}\n\t\t}\n\n\t\tOk(title)\n\t}\n\n\tfn request_files(&self, commit: CommitId) {\n\t\tself.async_treefiles.spawn(AsyncTreeFilesJob::new(\n\t\t\tself.repo.borrow().clone(),\n\t\t\tcommit,\n\t\t));\n\t}\n}\n\nimpl DrawableComponent for RevisionFilesComponent {\n\tfn draw(&self, f: &mut Frame, area: Rect) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tlet chunks = Layout::default()\n\t\t\t\t.direction(Direction::Horizontal)\n\t\t\t\t.constraints(\n\t\t\t\t\t[\n\t\t\t\t\t\tConstraint::Percentage(40),\n\t\t\t\t\t\tConstraint::Percentage(60),\n\t\t\t\t\t]\n\t\t\t\t\t.as_ref(),\n\t\t\t\t)\n\t\t\t\t.split(area);\n\n\t\t\tself.draw_tree(f, chunks[0])?;\n\n\t\t\tself.current_file.draw(f, chunks[1])?;\n\t\t}\n\t\tOk(())\n\t}\n}\n\nimpl Component for RevisionFilesComponent {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif !self.is_visible() && !force_all {\n\t\t\treturn CommandBlocking::PassingOn;\n\t\t}\n\n\t\tlet is_tree_focused = matches!(self.focus, Focus::Tree);\n\n\t\tif is_tree_focused || force_all {\n\t\t\tout.push(\n\t\t\t\tCommandInfo::new(\n\t\t\t\t\tstrings::commands::blame_file(&self.key_config),\n\t\t\t\t\tself.tree.selected_file().is_some(),\n\t\t\t\t\ttrue,\n\t\t\t\t)\n\t\t\t\t.order(order::NAV),\n\t\t\t);\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::edit_item(&self.key_config),\n\t\t\t\tself.tree.selected_file().is_some(),\n\t\t\t\ttrue,\n\t\t\t));\n\t\t\tout.push(\n\t\t\t\tCommandInfo::new(\n\t\t\t\t\tstrings::commands::open_file_history(\n\t\t\t\t\t\t&self.key_config,\n\t\t\t\t\t),\n\t\t\t\t\tself.tree.selected_file().is_some(),\n\t\t\t\t\ttrue,\n\t\t\t\t)\n\t\t\t\t.order(order::RARE_ACTION),\n\t\t\t);\n\t\t\tout.push(\n\t\t\t\tCommandInfo::new(\n\t\t\t\t\tstrings::commands::copy_path(&self.key_config),\n\t\t\t\t\tself.tree.selected_file().is_some(),\n\t\t\t\t\ttrue,\n\t\t\t\t)\n\t\t\t\t.order(order::RARE_ACTION),\n\t\t\t);\n\t\t\ttree_nav_cmds(&self.tree, &self.key_config, out);\n\t\t} else {\n\t\t\tself.current_file.commands(out, force_all);\n\t\t}\n\n\t\tCommandBlocking::PassingOn\n\t}\n\n\tfn event(\n\t\t&mut self,\n\t\tevent: &crossterm::event::Event,\n\t) -> Result<EventState> {\n\t\tif !self.is_visible() {\n\t\t\treturn Ok(EventState::NotConsumed);\n\t\t}\n\n\t\tif let Event::Key(key) = event {\n\t\t\tlet is_tree_focused = matches!(self.focus, Focus::Tree);\n\t\t\tif is_tree_focused\n\t\t\t\t&& tree_nav(&mut self.tree, &self.key_config, key)\n\t\t\t{\n\t\t\t\tself.selection_changed();\n\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t} else if key_match(key, self.key_config.keys.blame) {\n\t\t\t\tif self.blame() {\n\t\t\t\t\tself.hide();\n\t\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t\t}\n\t\t\t} else if key_match(\n\t\t\t\tkey,\n\t\t\t\tself.key_config.keys.file_history,\n\t\t\t) {\n\t\t\t\tif self.file_history() {\n\t\t\t\t\tself.hide();\n\t\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t\t}\n\t\t\t} else if key_match(key, self.key_config.keys.move_right)\n\t\t\t{\n\t\t\t\tif is_tree_focused {\n\t\t\t\t\tself.focus = Focus::File;\n\t\t\t\t\tself.current_file.focus(true);\n\t\t\t\t\tself.focus(true);\n\t\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t\t}\n\t\t\t} else if key_match(key, self.key_config.keys.move_left) {\n\t\t\t\tif !is_tree_focused {\n\t\t\t\t\tself.focus = Focus::Tree;\n\t\t\t\t\tself.current_file.focus(false);\n\t\t\t\t\tself.focus(false);\n\t\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t\t}\n\t\t\t} else if key_match(key, self.key_config.keys.file_find) {\n\t\t\t\tif is_tree_focused {\n\t\t\t\t\tself.open_finder();\n\t\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t\t}\n\t\t\t} else if key_match(key, self.key_config.keys.edit_file) {\n\t\t\t\tif let Some(file) =\n\t\t\t\t\tself.selected_file_path_with_prefix()\n\t\t\t\t{\n\t\t\t\t\t//Note: switch to status tab so its clear we are\n\t\t\t\t\t// not altering a file inside a revision here\n\t\t\t\t\tself.queue.push(InternalEvent::TabSwitchStatus);\n\t\t\t\t\tself.queue.push(\n\t\t\t\t\t\tInternalEvent::OpenExternalEditor(Some(file)),\n\t\t\t\t\t);\n\t\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t\t}\n\t\t\t} else if key_match(key, self.key_config.keys.copy) {\n\t\t\t\tif let Some(file) = self.selected_file_path() {\n\t\t\t\t\ttry_or_popup!(\n\t\t\t\t\t\tself,\n\t\t\t\t\t\tstrings::POPUP_FAIL_COPY,\n\t\t\t\t\t\tcrate::clipboard::copy_string(&file)\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t} else if !is_tree_focused {\n\t\t\t\treturn self.current_file.event(event);\n\t\t\t}\n\t\t}\n\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.visible = false;\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\t\tself.refresh_files()?;\n\t\tOk(())\n\t}\n}\n\n//TODO: reuse for other tree usages\nfn tree_nav_cmds(\n\ttree: &FileTree,\n\tkey_config: &SharedKeyConfig,\n\tout: &mut Vec<CommandInfo>,\n) {\n\tout.push(\n\t\tCommandInfo::new(\n\t\t\tstrings::commands::navigate_tree(key_config),\n\t\t\t!tree.is_empty(),\n\t\t\ttrue,\n\t\t)\n\t\t.order(order::NAV),\n\t);\n}\n\n//TODO: reuse for other tree usages\nfn tree_nav(\n\ttree: &mut FileTree,\n\tkey_config: &SharedKeyConfig,\n\tkey: &crossterm::event::KeyEvent,\n) -> bool {\n\tif let Some(common_nav) = common_nav(key, key_config) {\n\t\ttree.move_selection(common_nav)\n\t} else if key_match(key, key_config.keys.tree_collapse_recursive)\n\t{\n\t\ttree.collapse_recursive();\n\t\ttrue\n\t} else if key_match(key, key_config.keys.tree_expand_recursive) {\n\t\ttree.expand_recursive();\n\t\ttrue\n\t} else {\n\t\tfalse\n\t}\n}\n"
  },
  {
    "path": "src/components/status_tree.rs",
    "content": "use super::{\n\tutils::{\n\t\tfiletree::{FileTreeItem, FileTreeItemKind},\n\t\tstatustree::{MoveSelection, StatusTree},\n\t},\n\tCommandBlocking, DrawableComponent,\n};\nuse crate::{\n\tapp::Environment,\n\tcomponents::{CommandInfo, Component, EventState},\n\tkeys::{key_match, SharedKeyConfig},\n\tpopups::{BlameFileOpen, FileRevOpen},\n\tqueue::{InternalEvent, NeedsUpdate, Queue, StackablePopupOpen},\n\tstrings::{self, order},\n\tui::{self, style::SharedTheme},\n};\nuse anyhow::Result;\nuse asyncgit::{hash, sync::CommitId, StatusItem, StatusItemType};\nuse crossterm::event::Event;\nuse ratatui::{layout::Rect, text::Span, Frame};\nuse std::{borrow::Cow, cell::Cell, path::Path};\n\n//TODO: use new `filetreelist` crate\n\n///\n#[allow(clippy::struct_excessive_bools)]\npub struct StatusTreeComponent {\n\ttitle: String,\n\ttree: StatusTree,\n\tpending: bool,\n\tcurrent_hash: u64,\n\tfocused: bool,\n\tshow_selection: bool,\n\tqueue: Queue,\n\ttheme: SharedTheme,\n\tkey_config: SharedKeyConfig,\n\tscroll_top: Cell<usize>,\n\tvisible: bool,\n\trevision: Option<CommitId>,\n}\n\nimpl StatusTreeComponent {\n\t///\n\tpub fn new(env: &Environment, title: &str, focus: bool) -> Self {\n\t\tSelf {\n\t\t\ttitle: title.to_string(),\n\t\t\ttree: StatusTree::default(),\n\t\t\tcurrent_hash: 0,\n\t\t\tfocused: focus,\n\t\t\tshow_selection: focus,\n\t\t\tqueue: env.queue.clone(),\n\t\t\ttheme: env.theme.clone(),\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\tscroll_top: Cell::new(0),\n\t\t\tpending: true,\n\t\t\tvisible: false,\n\t\t\trevision: None,\n\t\t}\n\t}\n\n\tpub const fn set_commit(&mut self, revision: Option<CommitId>) {\n\t\tself.revision = revision;\n\t}\n\n\t///\n\tpub fn update(&mut self, list: &[StatusItem]) -> Result<()> {\n\t\tself.pending = false;\n\n\t\tlet new_hash = hash(list);\n\t\tif self.current_hash != new_hash {\n\t\t\tself.tree.update(list)?;\n\t\t\tself.current_hash = new_hash;\n\t\t}\n\n\t\tOk(())\n\t}\n\n\t///\n\tpub fn selection(&self) -> Option<FileTreeItem> {\n\t\tself.tree.selected_item()\n\t}\n\n\t///\n\tpub fn selection_file(&self) -> Option<StatusItem> {\n\t\tself.tree.selected_item().and_then(|f| {\n\t\t\tif let FileTreeItemKind::File(f) = f.kind {\n\t\t\t\tSome(f)\n\t\t\t} else {\n\t\t\t\tNone\n\t\t\t}\n\t\t})\n\t}\n\n\t///\n\tpub const fn show_selection(&mut self, show: bool) {\n\t\tself.show_selection = show;\n\t}\n\n\t/// returns true if list is empty\n\tpub const fn is_empty(&self) -> bool {\n\t\tself.tree.is_empty()\n\t}\n\n\t///\n\tpub const fn file_count(&self) -> usize {\n\t\tself.tree.tree.file_count()\n\t}\n\n\t///\n\tpub fn set_title(&mut self, title: String) {\n\t\tself.title = title;\n\t}\n\n\t///\n\tpub fn clear(&mut self) -> Result<()> {\n\t\tself.current_hash = 0;\n\t\tself.pending = true;\n\t\tself.tree.update(&[])\n\t}\n\n\t///\n\tpub fn is_file_selected(&self) -> bool {\n\t\tself.tree.selected_item().is_some_and(|item| {\n\t\t\tmatch item.kind {\n\t\t\t\tFileTreeItemKind::File(_) => true,\n\t\t\t\tFileTreeItemKind::Path(..) => false,\n\t\t\t}\n\t\t})\n\t}\n\n\tfn move_selection(&mut self, dir: MoveSelection) -> bool {\n\t\tlet changed = self.tree.move_selection(dir);\n\n\t\tif changed {\n\t\t\tself.queue.push(InternalEvent::Update(NeedsUpdate::DIFF));\n\t\t}\n\n\t\tchanged\n\t}\n\n\tconst fn item_status_char(item_type: StatusItemType) -> char {\n\t\tmatch item_type {\n\t\t\tStatusItemType::Modified => 'M',\n\t\t\tStatusItemType::New => '+',\n\t\t\tStatusItemType::Deleted => '-',\n\t\t\tStatusItemType::Renamed => 'R',\n\t\t\tStatusItemType::Typechange => ' ',\n\t\t\tStatusItemType::Conflicted => '!',\n\t\t}\n\t}\n\n\tfn item_to_text<'b>(\n\t\tstring: &str,\n\t\tindent: usize,\n\t\tvisible: bool,\n\t\tfile_item_kind: &FileTreeItemKind,\n\t\twidth: u16,\n\t\tselected: bool,\n\t\ttheme: &'b SharedTheme,\n\t) -> Option<Span<'b>> {\n\t\tlet indent_str = if indent == 0 {\n\t\t\tString::new()\n\t\t} else {\n\t\t\tformat!(\"{:w$}\", \" \", w = indent * 2)\n\t\t};\n\n\t\tif !visible {\n\t\t\treturn None;\n\t\t}\n\n\t\tmatch file_item_kind {\n\t\t\tFileTreeItemKind::File(status_item) => {\n\t\t\t\tlet status_char =\n\t\t\t\t\tSelf::item_status_char(status_item.status);\n\t\t\t\tlet file = Path::new(&status_item.path)\n\t\t\t\t\t.file_name()\n\t\t\t\t\t.and_then(std::ffi::OsStr::to_str)\n\t\t\t\t\t.expect(\"invalid path.\");\n\n\t\t\t\tlet txt = if selected {\n\t\t\t\t\tformat!(\n\t\t\t\t\t\t\"{} {}{:w$}\",\n\t\t\t\t\t\tstatus_char,\n\t\t\t\t\t\tindent_str,\n\t\t\t\t\t\tfile,\n\t\t\t\t\t\tw = width as usize\n\t\t\t\t\t)\n\t\t\t\t} else {\n\t\t\t\t\tformat!(\"{status_char} {indent_str}{file}\")\n\t\t\t\t};\n\n\t\t\t\tSome(Span::styled(\n\t\t\t\t\tCow::from(txt),\n\t\t\t\t\ttheme.item(status_item.status, selected),\n\t\t\t\t))\n\t\t\t}\n\n\t\t\tFileTreeItemKind::Path(path_collapsed) => {\n\t\t\t\tlet collapse_char =\n\t\t\t\t\tif path_collapsed.0 { '▸' } else { '▾' };\n\n\t\t\t\tlet txt = if selected {\n\t\t\t\t\tformat!(\n\t\t\t\t\t\t\"  {}{}{:w$}\",\n\t\t\t\t\t\tindent_str,\n\t\t\t\t\t\tcollapse_char,\n\t\t\t\t\t\tstring,\n\t\t\t\t\t\tw = width as usize\n\t\t\t\t\t)\n\t\t\t\t} else {\n\t\t\t\t\tformat!(\"  {indent_str}{collapse_char}{string}\")\n\t\t\t\t};\n\n\t\t\t\tSome(Span::styled(\n\t\t\t\t\tCow::from(txt),\n\t\t\t\t\ttheme.text(true, selected),\n\t\t\t\t))\n\t\t\t}\n\t\t}\n\t}\n\n\t/// Returns a `Vec<TextDrawInfo>` which is used to draw the `FileTreeComponent` correctly,\n\t/// allowing folders to be folded up if they are alone in their directory\n\tfn build_vec_text_draw_info_for_drawing(\n\t\t&self,\n\t) -> (Vec<TextDrawInfo<'_>>, usize, usize) {\n\t\tlet mut should_skip_over: usize = 0;\n\t\tlet mut selection_offset: usize = 0;\n\t\tlet mut selection_offset_visible: usize = 0;\n\t\tlet mut vec_draw_text_info: Vec<TextDrawInfo> = vec![];\n\t\tlet tree_items = self.tree.tree.items();\n\n\t\tfor (index, item) in tree_items.iter().enumerate() {\n\t\t\tif should_skip_over > 0 {\n\t\t\t\tshould_skip_over -= 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tlet index_above_select =\n\t\t\t\tindex < self.tree.selection.unwrap_or(0);\n\n\t\t\tif !item.info.visible && index_above_select {\n\t\t\t\tselection_offset_visible += 1;\n\t\t\t}\n\n\t\t\tvec_draw_text_info.push(TextDrawInfo {\n\t\t\t\tname: item.info.path.clone(),\n\t\t\t\tindent: item.info.indent,\n\t\t\t\tvisible: item.info.visible,\n\t\t\t\titem_kind: &item.kind,\n\t\t\t});\n\n\t\t\tlet mut idx_temp = index;\n\n\t\t\twhile idx_temp < tree_items.len().saturating_sub(2)\n\t\t\t\t&& tree_items[idx_temp].info.indent\n\t\t\t\t\t< tree_items[idx_temp + 1].info.indent\n\t\t\t{\n\t\t\t\t// fold up the folder/file\n\t\t\t\tidx_temp += 1;\n\t\t\t\tshould_skip_over += 1;\n\n\t\t\t\t// don't fold files up\n\t\t\t\tif let FileTreeItemKind::File(_) =\n\t\t\t\t\t&tree_items[idx_temp].kind\n\t\t\t\t{\n\t\t\t\t\tshould_skip_over -= 1;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\t// don't fold up if more than one folder in folder\n\t\t\t\telse if self\n\t\t\t\t\t.tree\n\t\t\t\t\t.tree\n\t\t\t\t\t.multiple_items_at_path(idx_temp)\n\t\t\t\t{\n\t\t\t\t\tshould_skip_over -= 1;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\t// There is only one item at this level (i.e only one folder in the folder),\n\t\t\t\t// so do fold up\n\n\t\t\t\tlet vec_draw_text_info_len = vec_draw_text_info.len();\n\t\t\t\tvec_draw_text_info[vec_draw_text_info_len - 1]\n\t\t\t\t\t.name += &(String::from(\"/\")\n\t\t\t\t\t+ &tree_items[idx_temp].info.path);\n\t\t\t\tif index_above_select {\n\t\t\t\t\tselection_offset += 1;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t(\n\t\t\tvec_draw_text_info,\n\t\t\tselection_offset,\n\t\t\tselection_offset_visible,\n\t\t)\n\t}\n\n\t// Copy the real path of selected file to clickboard\n\tfn copy_file_path(&self) {\n\t\tif let Some(item) = self.selection() {\n\t\t\tif crate::clipboard::copy_string(&item.info.full_path)\n\t\t\t\t.is_err()\n\t\t\t{\n\t\t\t\tself.queue.push(InternalEvent::ShowErrorMsg(\n\t\t\t\t\tstrings::POPUP_FAIL_COPY.to_string(),\n\t\t\t\t));\n\t\t\t}\n\t\t}\n\t}\n\n\tfn open_history(&mut self) {\n\t\tmatch self.selection_file() {\n\t\t\tSome(status_item)\n\t\t\t\tif !matches!(\n\t\t\t\t\tstatus_item.status,\n\t\t\t\t\tStatusItemType::New\n\t\t\t\t) =>\n\t\t\t{\n\t\t\t\tself.hide();\n\t\t\t\tself.queue.push(InternalEvent::OpenPopup(\n\t\t\t\t\tStackablePopupOpen::FileRevlog(FileRevOpen::new(\n\t\t\t\t\t\tstatus_item.path,\n\t\t\t\t\t)),\n\t\t\t\t));\n\t\t\t}\n\t\t\t_ => {}\n\t\t}\n\t}\n}\n\n/// Used for drawing the `FileTreeComponent`\nstruct TextDrawInfo<'a> {\n\tname: String,\n\tindent: u8,\n\tvisible: bool,\n\titem_kind: &'a FileTreeItemKind,\n}\n\nimpl DrawableComponent for StatusTreeComponent {\n\tfn draw(&self, f: &mut Frame, r: Rect) -> Result<()> {\n\t\tif !self.is_visible() {\n\t\t\treturn Ok(());\n\t\t}\n\n\t\tif self.pending {\n\t\t\tlet items = vec![Span::styled(\n\t\t\t\tCow::from(strings::loading_text(&self.key_config)),\n\t\t\t\tself.theme.text(false, false),\n\t\t\t)];\n\n\t\t\tui::draw_list(\n\t\t\t\tf,\n\t\t\t\tr,\n\t\t\t\tself.title.as_str(),\n\t\t\t\titems.into_iter(),\n\t\t\t\tself.focused,\n\t\t\t\t&self.theme,\n\t\t\t);\n\t\t} else {\n\t\t\tlet (\n\t\t\t\tvec_draw_text_info,\n\t\t\t\tselection_offset,\n\t\t\t\tselection_offset_visible,\n\t\t\t) = self.build_vec_text_draw_info_for_drawing();\n\n\t\t\tlet select = self\n\t\t\t\t.tree\n\t\t\t\t.selection\n\t\t\t\t.map(|idx| idx.saturating_sub(selection_offset))\n\t\t\t\t.unwrap_or_default();\n\t\t\tlet tree_height = r.height.saturating_sub(2) as usize;\n\t\t\tself.tree.window_height.set(Some(tree_height));\n\n\t\t\tself.scroll_top.set(ui::calc_scroll_top(\n\t\t\t\tself.scroll_top.get(),\n\t\t\t\ttree_height,\n\t\t\t\tselect.saturating_sub(selection_offset_visible),\n\t\t\t));\n\n\t\t\tlet items = vec_draw_text_info\n\t\t\t\t.iter()\n\t\t\t\t.enumerate()\n\t\t\t\t.filter_map(|(index, draw_text_info)| {\n\t\t\t\t\tSelf::item_to_text(\n\t\t\t\t\t\t&draw_text_info.name,\n\t\t\t\t\t\tdraw_text_info.indent as usize,\n\t\t\t\t\t\tdraw_text_info.visible,\n\t\t\t\t\t\tdraw_text_info.item_kind,\n\t\t\t\t\t\tr.width,\n\t\t\t\t\t\tself.show_selection && select == index,\n\t\t\t\t\t\t&self.theme,\n\t\t\t\t\t)\n\t\t\t\t})\n\t\t\t\t.skip(self.scroll_top.get());\n\n\t\t\tui::draw_list(\n\t\t\t\tf,\n\t\t\t\tr,\n\t\t\t\tself.title.as_str(),\n\t\t\t\titems,\n\t\t\t\tself.focused,\n\t\t\t\t&self.theme,\n\t\t\t);\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for StatusTreeComponent {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tlet available = self.focused || force_all;\n\t\tlet selection = self.selection_file();\n\t\tlet selected_is_file = selection.is_some();\n\t\tlet tracked = selection.is_some_and(|s| {\n\t\t\t!matches!(s.status, StatusItemType::New)\n\t\t});\n\n\t\tout.push(\n\t\t\tCommandInfo::new(\n\t\t\t\tstrings::commands::navigate_tree(&self.key_config),\n\t\t\t\t!self.is_empty(),\n\t\t\t\tavailable,\n\t\t\t)\n\t\t\t.order(order::NAV),\n\t\t);\n\n\t\tout.push(\n\t\t\tCommandInfo::new(\n\t\t\t\tstrings::commands::blame_file(&self.key_config),\n\t\t\t\tselected_is_file && tracked,\n\t\t\t\tavailable,\n\t\t\t)\n\t\t\t.order(order::RARE_ACTION),\n\t\t);\n\n\t\tout.push(\n\t\t\tCommandInfo::new(\n\t\t\t\tstrings::commands::open_file_history(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t),\n\t\t\t\tselected_is_file && tracked,\n\t\t\t\tavailable,\n\t\t\t)\n\t\t\t.order(order::RARE_ACTION),\n\t\t);\n\n\t\tout.push(\n\t\t\tCommandInfo::new(\n\t\t\t\tstrings::commands::edit_item(&self.key_config),\n\t\t\t\tselected_is_file,\n\t\t\t\tavailable,\n\t\t\t)\n\t\t\t.order(order::RARE_ACTION),\n\t\t);\n\n\t\tout.push(\n\t\t\tCommandInfo::new(\n\t\t\t\tstrings::commands::copy_path(&self.key_config),\n\t\t\t\tselected_is_file,\n\t\t\t\tavailable,\n\t\t\t)\n\t\t\t.order(order::RARE_ACTION),\n\t\t);\n\n\t\tCommandBlocking::PassingOn\n\t}\n\n\tfn event(&mut self, ev: &Event) -> Result<EventState> {\n\t\tif self.focused {\n\t\t\tif let Event::Key(e) = ev {\n\t\t\t\treturn if key_match(e, self.key_config.keys.blame) {\n\t\t\t\t\tmatch self.selection_file() {\n\t\t\t\t\t\tSome(status_item)\n\t\t\t\t\t\t\tif !matches!(\n\t\t\t\t\t\t\t\tstatus_item.status,\n\t\t\t\t\t\t\t\tStatusItemType::New\n\t\t\t\t\t\t\t) =>\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tself.hide();\n\t\t\t\t\t\t\tself.queue.push(\n\t\t\t\t\t\t\t\tInternalEvent::OpenPopup(\n\t\t\t\t\t\t\t\t\tStackablePopupOpen::BlameFile(\n\t\t\t\t\t\t\t\t\t\tBlameFileOpen {\n\t\t\t\t\t\t\t\t\t\t\tfile_path: status_item\n\t\t\t\t\t\t\t\t\t\t\t\t.path,\n\t\t\t\t\t\t\t\t\t\t\tcommit_id: self.revision,\n\t\t\t\t\t\t\t\t\t\t\tselection: None,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t_ => {}\n\t\t\t\t\t}\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(\n\t\t\t\t\te,\n\t\t\t\t\tself.key_config.keys.file_history,\n\t\t\t\t) {\n\t\t\t\t\tself.open_history();\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(e, self.key_config.keys.edit_file)\n\t\t\t\t{\n\t\t\t\t\tif let Some(status_item) = self.selection_file() {\n\t\t\t\t\t\tself.queue.push(\n\t\t\t\t\t\t\tInternalEvent::OpenExternalEditor(Some(\n\t\t\t\t\t\t\t\tstatus_item.path,\n\t\t\t\t\t\t\t)),\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(e, self.key_config.keys.copy) {\n\t\t\t\t\tself.copy_file_path();\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(e, self.key_config.keys.move_down)\n\t\t\t\t{\n\t\t\t\t\tOk(self\n\t\t\t\t\t\t.move_selection(MoveSelection::Down)\n\t\t\t\t\t\t.into())\n\t\t\t\t} else if key_match(e, self.key_config.keys.move_up) {\n\t\t\t\t\tOk(self.move_selection(MoveSelection::Up).into())\n\t\t\t\t} else if key_match(e, self.key_config.keys.home)\n\t\t\t\t\t|| key_match(e, self.key_config.keys.shift_up)\n\t\t\t\t{\n\t\t\t\t\tOk(self\n\t\t\t\t\t\t.move_selection(MoveSelection::Home)\n\t\t\t\t\t\t.into())\n\t\t\t\t} else if key_match(e, self.key_config.keys.end)\n\t\t\t\t\t|| key_match(e, self.key_config.keys.shift_down)\n\t\t\t\t{\n\t\t\t\t\tOk(self.move_selection(MoveSelection::End).into())\n\t\t\t\t} else if key_match(e, self.key_config.keys.page_up) {\n\t\t\t\t\tOk(self\n\t\t\t\t\t\t.move_selection(MoveSelection::PageUp)\n\t\t\t\t\t\t.into())\n\t\t\t\t} else if key_match(e, self.key_config.keys.page_down)\n\t\t\t\t{\n\t\t\t\t\tOk(self\n\t\t\t\t\t\t.move_selection(MoveSelection::PageDown)\n\t\t\t\t\t\t.into())\n\t\t\t\t} else if key_match(e, self.key_config.keys.move_left)\n\t\t\t\t{\n\t\t\t\t\tOk(self\n\t\t\t\t\t\t.move_selection(MoveSelection::Left)\n\t\t\t\t\t\t.into())\n\t\t\t\t} else if key_match(\n\t\t\t\t\te,\n\t\t\t\t\tself.key_config.keys.move_right,\n\t\t\t\t) {\n\t\t\t\t\tOk(self\n\t\t\t\t\t\t.move_selection(MoveSelection::Right)\n\t\t\t\t\t\t.into())\n\t\t\t\t} else {\n\t\t\t\t\tOk(EventState::NotConsumed)\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn focused(&self) -> bool {\n\t\tself.focused\n\t}\n\tfn focus(&mut self, focus: bool) {\n\t\tself.focused = focus;\n\t\tself.show_selection(focus);\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.visible = false;\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\t\tOk(())\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\n\tfn string_vec_to_status(items: &[&str]) -> Vec<StatusItem> {\n\t\titems\n\t\t\t.iter()\n\t\t\t.map(|a| StatusItem {\n\t\t\t\tpath: String::from(*a),\n\t\t\t\tstatus: StatusItemType::Modified,\n\t\t\t})\n\t\t\t.collect::<Vec<_>>()\n\t}\n\n\t#[test]\n\tfn test_correct_scroll_position() {\n\t\tlet items = string_vec_to_status(&[\n\t\t\t\"a/b/b1\", //\n\t\t\t\"a/b/b2\", //\n\t\t\t\"a/c/c1\", //\n\t\t]);\n\n\t\t//0 a/\n\t\t//1   b/\n\t\t//2     b1\n\t\t//3     b2\n\t\t//4  c/\n\t\t//5    c1\n\n\t\t// Set up test terminal\n\t\tlet test_backend =\n\t\t\tratatui::backend::TestBackend::new(100, 100);\n\t\tlet mut terminal = ratatui::Terminal::new(test_backend)\n\t\t\t.expect(\"Unable to set up terminal\");\n\t\tlet mut frame = terminal.get_frame();\n\n\t\t// set up file tree\n\t\tlet mut ftc = StatusTreeComponent::new(\n\t\t\t&Environment::test_env(),\n\t\t\t\"title\",\n\t\t\ttrue,\n\t\t);\n\t\tftc.update(&items)\n\t\t\t.expect(\"Updating FileTreeComponent failed\");\n\n\t\tftc.move_selection(MoveSelection::Down); // Move to b/\n\t\tftc.move_selection(MoveSelection::Left); // Fold b/\n\t\tftc.move_selection(MoveSelection::Down); // Move to c/\n\n\t\tftc.draw(&mut frame, Rect::new(0, 0, 10, 5))\n\t\t\t.expect(\"Draw failed\");\n\n\t\tassert_eq!(ftc.scroll_top.get(), 0); // should still be at top\n\t}\n\n\t#[test]\n\tfn test_correct_foldup_and_not_visible_scroll_position() {\n\t\tlet items = string_vec_to_status(&[\n\t\t\t\"a/b/b1\", //\n\t\t\t\"c/d1\",   //\n\t\t\t\"c/d2\",   //\n\t\t]);\n\n\t\t//0 a/b/\n\t\t//2     b1\n\t\t//3 c/\n\t\t//4   d1\n\t\t//5   d2\n\n\t\t// Set up test terminal\n\t\tlet test_backend =\n\t\t\tratatui::backend::TestBackend::new(100, 100);\n\t\tlet mut terminal = ratatui::Terminal::new(test_backend)\n\t\t\t.expect(\"Unable to set up terminal\");\n\t\tlet mut frame = terminal.get_frame();\n\n\t\t// set up file tree\n\t\tlet mut ftc = StatusTreeComponent::new(\n\t\t\t&Environment::test_env(),\n\t\t\t\"title\",\n\t\t\ttrue,\n\t\t);\n\t\tftc.update(&items)\n\t\t\t.expect(\"Updating FileTreeComponent failed\");\n\n\t\tftc.move_selection(MoveSelection::Left); // Fold a/b/\n\t\tftc.move_selection(MoveSelection::Down); // Move to c/\n\n\t\tftc.draw(&mut frame, Rect::new(0, 0, 10, 5))\n\t\t\t.expect(\"Draw failed\");\n\n\t\tassert_eq!(ftc.scroll_top.get(), 0); // should still be at top\n\t}\n}\n"
  },
  {
    "path": "src/components/syntax_text.rs",
    "content": "use super::{\n\tCommandBlocking, CommandInfo, Component, DrawableComponent,\n\tEventState,\n};\nuse crate::{\n\tapp::Environment,\n\tkeys::SharedKeyConfig,\n\tstring_utils::tabs_to_spaces,\n\tstrings,\n\tui::{\n\t\tself, common_nav, style::SharedTheme, AsyncSyntaxJob,\n\t\tParagraphState, ScrollPos, StatefulParagraph,\n\t},\n\tAsyncAppNotification, AsyncNotification, SyntaxHighlightProgress,\n};\nuse anyhow::Result;\nuse asyncgit::{\n\tasyncjob::AsyncSingleJob,\n\tsync::{self, RepoPathRef, TreeFile},\n\tProgressPercent,\n};\nuse crossterm::event::Event;\nuse filetreelist::MoveSelection;\nuse itertools::Either;\nuse ratatui::{\n\tlayout::Rect,\n\ttext::Text,\n\twidgets::{Block, Borders, Wrap},\n\tFrame,\n};\nuse std::{cell::Cell, path::Path};\n\npub struct SyntaxTextComponent {\n\trepo: RepoPathRef,\n\tcurrent_file: Option<(String, Either<ui::SyntaxText, String>)>,\n\tasync_highlighting: AsyncSingleJob<AsyncSyntaxJob>,\n\tsyntax_progress: Option<ProgressPercent>,\n\tkey_config: SharedKeyConfig,\n\tparagraph_state: Cell<ParagraphState>,\n\tfocused: bool,\n\ttheme: SharedTheme,\n}\n\nimpl SyntaxTextComponent {\n\t///\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\tasync_highlighting: AsyncSingleJob::new(\n\t\t\t\tenv.sender_app.clone(),\n\t\t\t),\n\t\t\tsyntax_progress: None,\n\t\t\tcurrent_file: None,\n\t\t\tparagraph_state: Cell::new(ParagraphState::default()),\n\t\t\tfocused: false,\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\ttheme: env.theme.clone(),\n\t\t\trepo: env.repo.clone(),\n\t\t}\n\t}\n\n\t///\n\tpub fn update(&mut self, ev: AsyncNotification) {\n\t\tif let AsyncNotification::App(\n\t\t\tAsyncAppNotification::SyntaxHighlighting(progress),\n\t\t) = ev\n\t\t{\n\t\t\tmatch progress {\n\t\t\t\tSyntaxHighlightProgress::Progress => {\n\t\t\t\t\tself.syntax_progress =\n\t\t\t\t\t\tself.async_highlighting.progress();\n\t\t\t\t}\n\t\t\t\tSyntaxHighlightProgress::Done => {\n\t\t\t\t\tself.syntax_progress = None;\n\t\t\t\t\tif let Some(job) =\n\t\t\t\t\t\tself.async_highlighting.take_last()\n\t\t\t\t\t{\n\t\t\t\t\t\tif let Some((path, content)) =\n\t\t\t\t\t\t\tself.current_file.as_mut()\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tif let Some(syntax) = job.result() {\n\t\t\t\t\t\t\t\tif syntax.path() == Path::new(path) {\n\t\t\t\t\t\t\t\t\t*content = Either::Left(syntax);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t///\n\tpub fn any_work_pending(&self) -> bool {\n\t\tself.async_highlighting.is_pending()\n\t}\n\n\t///\n\tpub fn clear(&mut self) {\n\t\tself.current_file = None;\n\t}\n\n\t///\n\tpub fn load_file(&mut self, path: String, item: &TreeFile) {\n\t\tlet already_loaded = self\n\t\t\t.current_file\n\t\t\t.as_ref()\n\t\t\t.is_some_and(|(current_file, _)| current_file == &path);\n\n\t\tif !already_loaded {\n\t\t\t//TODO: fetch file content async as well\n\t\t\tmatch sync::tree_file_content(&self.repo.borrow(), item) {\n\t\t\t\tOk(content) => {\n\t\t\t\t\tlet content = tabs_to_spaces(content);\n\t\t\t\t\tself.syntax_progress =\n\t\t\t\t\t\tSome(ProgressPercent::empty());\n\t\t\t\t\tself.async_highlighting.spawn(\n\t\t\t\t\t\tAsyncSyntaxJob::new(\n\t\t\t\t\t\t\tcontent.clone(),\n\t\t\t\t\t\t\tpath.clone(),\n\t\t\t\t\t\t\tself.theme.get_syntax(),\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\n\t\t\t\t\tself.current_file =\n\t\t\t\t\t\tSome((path, Either::Right(content)));\n\t\t\t\t}\n\t\t\t\tErr(e) => {\n\t\t\t\t\tself.current_file = Some((\n\t\t\t\t\t\tpath,\n\t\t\t\t\t\tEither::Right(format!(\n\t\t\t\t\t\t\t\"error loading file: {e}\"\n\t\t\t\t\t\t)),\n\t\t\t\t\t));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfn scroll(&self, nav: MoveSelection) -> bool {\n\t\tlet state = self.paragraph_state.get();\n\n\t\tlet new_scroll_pos = match nav {\n\t\t\tMoveSelection::Down => state.scroll().y.saturating_add(1),\n\t\t\tMoveSelection::Up => state.scroll().y.saturating_sub(1),\n\t\t\tMoveSelection::Top => 0,\n\t\t\tMoveSelection::End => state\n\t\t\t\t.lines()\n\t\t\t\t.saturating_sub(state.height().saturating_sub(2)),\n\t\t\tMoveSelection::PageUp => state\n\t\t\t\t.scroll()\n\t\t\t\t.y\n\t\t\t\t.saturating_sub(state.height().saturating_sub(2)),\n\t\t\tMoveSelection::PageDown => state\n\t\t\t\t.scroll()\n\t\t\t\t.y\n\t\t\t\t.saturating_add(state.height().saturating_sub(2)),\n\t\t\t_ => state.scroll().y,\n\t\t};\n\n\t\tself.set_scroll(new_scroll_pos)\n\t}\n\n\tfn set_scroll(&self, pos: u16) -> bool {\n\t\tlet mut state = self.paragraph_state.get();\n\n\t\tlet new_scroll_pos = pos.min(\n\t\t\tstate\n\t\t\t\t.lines()\n\t\t\t\t.saturating_sub(state.height().saturating_sub(2)),\n\t\t);\n\n\t\tif new_scroll_pos == state.scroll().y {\n\t\t\treturn false;\n\t\t}\n\n\t\tstate.set_scroll(ScrollPos {\n\t\t\tx: 0,\n\t\t\ty: new_scroll_pos,\n\t\t});\n\t\tself.paragraph_state.set(state);\n\n\t\ttrue\n\t}\n}\n\nimpl DrawableComponent for SyntaxTextComponent {\n\tfn draw(&self, f: &mut Frame, area: Rect) -> Result<()> {\n\t\tlet text = self.current_file.as_ref().map_or_else(\n\t\t\t|| Text::from(\"\"),\n\t\t\t|(_, content)| match content {\n\t\t\t\tEither::Left(syn) => syn.into(),\n\t\t\t\tEither::Right(s) => Text::from(s.as_str()),\n\t\t\t},\n\t\t);\n\n\t\tlet title = format!(\n\t\t\t\"{}{}\",\n\t\t\tself.current_file\n\t\t\t\t.as_ref()\n\t\t\t\t.map(|(name, _)| name.clone())\n\t\t\t\t.unwrap_or_default(),\n\t\t\tself.syntax_progress\n\t\t\t\t.map(|p| format!(\" ({}%)\", p.progress))\n\t\t\t\t.unwrap_or_default()\n\t\t);\n\n\t\tlet content = StatefulParagraph::new(text)\n\t\t\t.wrap(Wrap { trim: false })\n\t\t\t.block(\n\t\t\t\tBlock::default()\n\t\t\t\t\t.title(title)\n\t\t\t\t\t.borders(Borders::ALL)\n\t\t\t\t\t.border_style(self.theme.title(self.focused())),\n\t\t\t);\n\n\t\tlet mut state = self.paragraph_state.get();\n\n\t\tf.render_stateful_widget(content, area, &mut state);\n\n\t\tself.paragraph_state.set(state);\n\n\t\tself.set_scroll(state.scroll().y);\n\n\t\tif self.focused() {\n\t\t\tui::draw_scrollbar(\n\t\t\t\tf,\n\t\t\t\tarea,\n\t\t\t\t&self.theme,\n\t\t\t\tusize::from(state.lines().saturating_sub(\n\t\t\t\t\tstate.height().saturating_sub(2),\n\t\t\t\t)),\n\t\t\t\tusize::from(state.scroll().y),\n\t\t\t\tui::Orientation::Vertical,\n\t\t\t);\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for SyntaxTextComponent {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.focused() || force_all {\n\t\t\tout.push(\n\t\t\t\tCommandInfo::new(\n\t\t\t\t\tstrings::commands::scroll(&self.key_config),\n\t\t\t\t\ttrue,\n\t\t\t\t\ttrue,\n\t\t\t\t)\n\t\t\t\t.order(strings::order::NAV),\n\t\t\t);\n\t\t}\n\t\tCommandBlocking::PassingOn\n\t}\n\n\tfn event(\n\t\t&mut self,\n\t\tevent: &crossterm::event::Event,\n\t) -> Result<EventState> {\n\t\tif let Event::Key(key) = event {\n\t\t\tif let Some(nav) = common_nav(key, &self.key_config) {\n\t\t\t\treturn Ok(if self.scroll(nav) {\n\t\t\t\t\tEventState::Consumed\n\t\t\t\t} else {\n\t\t\t\t\tEventState::NotConsumed\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\t///\n\tfn focused(&self) -> bool {\n\t\tself.focused\n\t}\n\n\t/// focus/unfocus this component depending on param\n\tfn focus(&mut self, focus: bool) {\n\t\tself.focused = focus;\n\t}\n}\n"
  },
  {
    "path": "src/components/textinput.rs",
    "content": "use crate::app::Environment;\nuse crate::keys::key_match;\nuse crate::ui::Size;\nuse crate::{\n\tcomponents::{\n\t\tvisibility_blocking, CommandBlocking, CommandInfo, Component,\n\t\tDrawableComponent, EventState,\n\t},\n\tkeys::SharedKeyConfig,\n\tstrings,\n\tui::{self, style::SharedTheme},\n};\nuse anyhow::Result;\nuse crossterm::event::Event;\nuse ratatui::widgets::{Block, Borders};\nuse ratatui::{\n\tlayout::{Alignment, Rect},\n\twidgets::{Clear, Paragraph},\n\tFrame,\n};\nuse std::cell::Cell;\nuse std::cell::OnceCell;\nuse tui_textarea::{CursorMove, Input, Key, Scrolling, TextArea};\n\n///\n#[derive(PartialEq, Eq)]\npub enum InputType {\n\tSingleline,\n\tMultiline,\n\tPassword,\n}\n\n#[derive(PartialEq, Eq)]\nenum SelectionState {\n\tSelecting,\n\tNotSelecting,\n\tSelectionEndPending,\n}\n\ntype TextAreaComponent = TextArea<'static>;\n\n///\npub struct TextInputComponent {\n\ttitle: String,\n\tdefault_msg: String,\n\tselected: Option<bool>,\n\tmsg: OnceCell<String>,\n\tshow_char_count: bool,\n\ttheme: SharedTheme,\n\tkey_config: SharedKeyConfig,\n\tinput_type: InputType,\n\tcurrent_area: Cell<Rect>,\n\tembed: bool,\n\ttextarea: Option<TextAreaComponent>,\n\tselect_state: SelectionState,\n}\n\nimpl TextInputComponent {\n\t///\n\tpub fn new(\n\t\tenv: &Environment,\n\t\ttitle: &str,\n\t\tdefault_msg: &str,\n\t\tshow_char_count: bool,\n\t) -> Self {\n\t\tSelf {\n\t\t\tmsg: OnceCell::default(),\n\t\t\ttheme: env.theme.clone(),\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\tshow_char_count,\n\t\t\ttitle: title.to_string(),\n\t\t\tdefault_msg: default_msg.to_string(),\n\t\t\tselected: None,\n\t\t\tinput_type: InputType::Multiline,\n\t\t\tcurrent_area: Cell::new(Rect::default()),\n\t\t\tembed: false,\n\t\t\ttextarea: None,\n\t\t\tselect_state: SelectionState::NotSelecting,\n\t\t}\n\t}\n\n\t///\n\tpub const fn with_input_type(\n\t\tmut self,\n\t\tinput_type: InputType,\n\t) -> Self {\n\t\tself.input_type = input_type;\n\t\tself\n\t}\n\n\t///\n\tpub fn set_input_type(&mut self, input_type: InputType) {\n\t\tself.clear();\n\t\tself.input_type = input_type;\n\t}\n\n\t/// Clear the `msg`.\n\tpub fn clear(&mut self) {\n\t\tself.msg.take();\n\t\tif self.is_visible() {\n\t\t\tself.show_inner_textarea();\n\t\t}\n\t}\n\n\t/// Get the `msg`.\n\tpub fn get_text(&self) -> &str {\n\t\t// the fancy footwork with the OnceCell is to allow\n\t\t// the reading of msg as a &str.\n\t\t// tui_textarea returns its lines to the caller as &[String]\n\t\t// gitui wants &str of \\n delimited text\n\t\t// it would be simple if this was a mut method. You could\n\t\t// just load up msg from the lines area and return an &str pointing at it\n\t\t// but its not a mut method. So we need to store the text in a OnceCell\n\t\t// The methods that change msg call take() on the cell. That makes\n\t\t// get_or_init run again\n\n\t\tself.msg.get_or_init(|| {\n\t\t\tself.textarea\n\t\t\t\t.as_ref()\n\t\t\t\t.map_or_else(String::new, |ta| ta.lines().join(\"\\n\"))\n\t\t})\n\t}\n\n\t/// screen area (last time we got drawn)\n\tpub const fn get_area(&self) -> Rect {\n\t\tself.current_area.get()\n\t}\n\n\t/// embed into parent draw area\n\tpub const fn embed(&mut self) {\n\t\tself.embed = true;\n\t}\n\n\t///\n\tpub const fn enabled(&mut self, enable: bool) {\n\t\tself.selected = Some(enable);\n\t}\n\n\tfn show_inner_textarea(&mut self) {\n\t\t//\tcreate the textarea and then load it with the text\n\t\t//\tfrom self.msg\n\t\tlet lines: Vec<String> = self\n\t\t\t.msg\n\t\t\t.get()\n\t\t\t.unwrap_or(&String::new())\n\t\t\t.split('\\n')\n\t\t\t.map(ToString::to_string)\n\t\t\t.collect();\n\n\t\tself.textarea = Some({\n\t\t\tlet mut text_area = TextArea::new(lines);\n\t\t\tif self.input_type == InputType::Password {\n\t\t\t\ttext_area.set_mask_char('*');\n\t\t\t}\n\n\t\t\ttext_area\n\t\t\t\t.set_cursor_line_style(self.theme.text(true, false));\n\t\t\ttext_area.set_placeholder_text(self.default_msg.clone());\n\t\t\ttext_area.set_placeholder_style(\n\t\t\t\tself.theme\n\t\t\t\t\t.text(self.selected.unwrap_or_default(), false),\n\t\t\t);\n\t\t\ttext_area.set_style(\n\t\t\t\tself.theme.text(self.selected.unwrap_or(true), false),\n\t\t\t);\n\n\t\t\tif !self.embed {\n\t\t\t\ttext_area.set_block(\n\t\t\t\t\tBlock::default()\n\t\t\t\t\t\t.borders(Borders::ALL)\n\t\t\t\t\t\t.border_style(\n\t\t\t\t\t\t\tratatui::style::Style::default()\n\t\t\t\t\t\t\t\t.add_modifier(\n\t\t\t\t\t\t\t\t\tratatui::style::Modifier::BOLD,\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.title(self.title.clone()),\n\t\t\t\t);\n\t\t\t}\n\t\t\ttext_area\n\t\t});\n\t}\n\n\t/// Set the `msg`.\n\tpub fn set_text(&mut self, msg: String) {\n\t\tself.msg = msg.into();\n\t\tif self.is_visible() {\n\t\t\tself.show_inner_textarea();\n\t\t}\n\t}\n\n\t/// Set the `title`.\n\tpub fn set_title(&mut self, t: String) {\n\t\tself.title = t;\n\t}\n\n\t///\n\tpub fn set_default_msg(&mut self, v: String) {\n\t\tself.default_msg = v;\n\t\tif self.is_visible() {\n\t\t\tself.show_inner_textarea();\n\t\t}\n\t}\n\n\tfn draw_char_count(&self, f: &mut Frame, r: Rect) {\n\t\tlet count = self.get_text().len();\n\t\tif count > 0 {\n\t\t\tlet w = Paragraph::new(format!(\"[{count} chars]\"))\n\t\t\t\t.alignment(Alignment::Right);\n\n\t\t\tlet mut rect = {\n\t\t\t\tlet mut rect = r;\n\t\t\t\trect.y += rect.height.saturating_sub(1);\n\t\t\t\trect\n\t\t\t};\n\n\t\t\trect.x += 1;\n\t\t\trect.width = rect.width.saturating_sub(2);\n\t\t\trect.height = rect\n\t\t\t\t.height\n\t\t\t\t.saturating_sub(rect.height.saturating_sub(1));\n\n\t\t\tf.render_widget(w, rect);\n\t\t}\n\t}\n\n\tfn should_select(&mut self, input: &Input) {\n\t\tif input.key == Key::Null {\n\t\t\treturn;\n\t\t}\n\t\t// Should we start selecting text, stop the current selection, or do nothing?\n\t\t// the end is handled after the ending keystroke\n\n\t\tmatch (&self.select_state, input.shift) {\n\t\t\t(SelectionState::Selecting, true)\n\t\t\t| (SelectionState::NotSelecting, false) => {\n\t\t\t\t// continue selecting or not selecting\n\t\t\t}\n\t\t\t(SelectionState::Selecting, false) => {\n\t\t\t\t// end select\n\t\t\t\tself.select_state =\n\t\t\t\t\tSelectionState::SelectionEndPending;\n\t\t\t}\n\t\t\t(SelectionState::NotSelecting, true) => {\n\t\t\t\t// start select\n\t\t\t\t// this should always work since we are only called\n\t\t\t\t// if we have a textarea to get input\n\t\t\t\tif let Some(ta) = &mut self.textarea {\n\t\t\t\t\tta.start_selection();\n\t\t\t\t\tself.select_state = SelectionState::Selecting;\n\t\t\t\t}\n\t\t\t}\n\t\t\t(SelectionState::SelectionEndPending, _) => {\n\t\t\t\t// this really should not happen because the end pending state\n\t\t\t\t// should have been picked up in the same pass as it was set\n\t\t\t\t// so lets clear it\n\t\t\t\tself.select_state = SelectionState::NotSelecting;\n\t\t\t}\n\t\t}\n\t}\n\n\t#[allow(clippy::too_many_lines, clippy::unnested_or_patterns)]\n\tfn process_inputs(ta: &mut TextArea<'_>, input: &Input) -> bool {\n\t\tmatch input {\n\t\t\tInput {\n\t\t\t\tkey: Key::Char(c),\n\t\t\t\tctrl: false,\n\t\t\t\talt: false,\n\t\t\t\t..\n\t\t\t} => {\n\t\t\t\tta.insert_char(*c);\n\t\t\t\ttrue\n\t\t\t}\n\t\t\tInput {\n\t\t\t\tkey: Key::Tab,\n\t\t\t\tctrl: false,\n\t\t\t\talt: false,\n\t\t\t\t..\n\t\t\t} => {\n\t\t\t\tta.insert_tab();\n\t\t\t\ttrue\n\t\t\t}\n\t\t\tInput {\n\t\t\t\tkey: Key::Char('h'),\n\t\t\t\tctrl: true,\n\t\t\t\talt: false,\n\t\t\t\t..\n\t\t\t}\n\t\t\t| Input {\n\t\t\t\tkey: Key::Backspace,\n\t\t\t\tctrl: false,\n\t\t\t\talt: false,\n\t\t\t\t..\n\t\t\t} => {\n\t\t\t\tta.delete_char();\n\t\t\t\ttrue\n\t\t\t}\n\t\t\tInput {\n\t\t\t\tkey: Key::Char('d'),\n\t\t\t\tctrl: true,\n\t\t\t\talt: false,\n\t\t\t\t..\n\t\t\t}\n\t\t\t| Input {\n\t\t\t\tkey: Key::Delete,\n\t\t\t\tctrl: false,\n\t\t\t\talt: false,\n\t\t\t\t..\n\t\t\t} => {\n\t\t\t\tta.delete_next_char();\n\t\t\t\ttrue\n\t\t\t}\n\t\t\tInput {\n\t\t\t\tkey: Key::Char('k'),\n\t\t\t\tctrl: true,\n\t\t\t\talt: false,\n\t\t\t\t..\n\t\t\t} => {\n\t\t\t\tta.delete_line_by_end();\n\t\t\t\ttrue\n\t\t\t}\n\t\t\tInput {\n\t\t\t\tkey: Key::Char('j'),\n\t\t\t\tctrl: true,\n\t\t\t\talt: false,\n\t\t\t\t..\n\t\t\t} => {\n\t\t\t\tta.delete_line_by_head();\n\t\t\t\ttrue\n\t\t\t}\n\t\t\tInput {\n\t\t\t\tkey: Key::Char('w'),\n\t\t\t\tctrl: true,\n\t\t\t\talt: false,\n\t\t\t\t..\n\t\t\t}\n\t\t\t| Input {\n\t\t\t\tkey: Key::Char('h'),\n\t\t\t\tctrl: false,\n\t\t\t\talt: true,\n\t\t\t\t..\n\t\t\t}\n\t\t\t| Input {\n\t\t\t\tkey: Key::Backspace,\n\t\t\t\tctrl: false,\n\t\t\t\talt: true,\n\t\t\t\t..\n\t\t\t} => {\n\t\t\t\tta.delete_word();\n\t\t\t\ttrue\n\t\t\t}\n\t\t\tInput {\n\t\t\t\tkey: Key::Delete,\n\t\t\t\tctrl: false,\n\t\t\t\talt: true,\n\t\t\t\t..\n\t\t\t}\n\t\t\t| Input {\n\t\t\t\tkey: Key::Char('d'),\n\t\t\t\tctrl: false,\n\t\t\t\talt: true,\n\t\t\t\t..\n\t\t\t} => {\n\t\t\t\tta.delete_next_word();\n\t\t\t\ttrue\n\t\t\t}\n\t\t\tInput {\n\t\t\t\tkey: Key::Char('n'),\n\t\t\t\tctrl: true,\n\t\t\t\talt: false,\n\t\t\t\t..\n\t\t\t}\n\t\t\t| Input {\n\t\t\t\tkey: Key::Down,\n\t\t\t\tctrl: false,\n\t\t\t\talt: false,\n\t\t\t\t..\n\t\t\t} => {\n\t\t\t\tta.move_cursor(CursorMove::Down);\n\t\t\t\ttrue\n\t\t\t}\n\t\t\tInput {\n\t\t\t\tkey: Key::Char('p'),\n\t\t\t\tctrl: true,\n\t\t\t\talt: false,\n\t\t\t\t..\n\t\t\t}\n\t\t\t| Input {\n\t\t\t\tkey: Key::Up,\n\t\t\t\tctrl: false,\n\t\t\t\talt: false,\n\t\t\t\t..\n\t\t\t} => {\n\t\t\t\tta.move_cursor(CursorMove::Up);\n\t\t\t\ttrue\n\t\t\t}\n\t\t\tInput {\n\t\t\t\tkey: Key::Char('f'),\n\t\t\t\tctrl: true,\n\t\t\t\talt: false,\n\t\t\t\t..\n\t\t\t}\n\t\t\t| Input {\n\t\t\t\tkey: Key::Right,\n\t\t\t\tctrl: false,\n\t\t\t\talt: false,\n\t\t\t\t..\n\t\t\t} => {\n\t\t\t\tta.move_cursor(CursorMove::Forward);\n\t\t\t\ttrue\n\t\t\t}\n\t\t\tInput {\n\t\t\t\tkey: Key::Char('b'),\n\t\t\t\tctrl: true,\n\t\t\t\talt: false,\n\t\t\t\t..\n\t\t\t}\n\t\t\t| Input {\n\t\t\t\tkey: Key::Left,\n\t\t\t\tctrl: false,\n\t\t\t\talt: false,\n\t\t\t\t..\n\t\t\t} => {\n\t\t\t\tta.move_cursor(CursorMove::Back);\n\t\t\t\ttrue\n\t\t\t}\n\t\t\tInput {\n\t\t\t\tkey: Key::Char('a'),\n\t\t\t\tctrl: true,\n\t\t\t\talt: false,\n\t\t\t\t..\n\t\t\t}\n\t\t\t| Input { key: Key::Home, .. }\n\t\t\t| Input {\n\t\t\t\tkey: Key::Left | Key::Char('b'),\n\t\t\t\tctrl: true,\n\t\t\t\talt: true,\n\t\t\t\t..\n\t\t\t} => {\n\t\t\t\tta.move_cursor(CursorMove::Head);\n\t\t\t\ttrue\n\t\t\t}\n\t\t\tInput {\n\t\t\t\tkey: Key::Char('e'),\n\t\t\t\tctrl: true,\n\t\t\t\talt: false,\n\t\t\t\t..\n\t\t\t}\n\t\t\t| Input { key: Key::End, .. }\n\t\t\t| Input {\n\t\t\t\tkey: Key::Right | Key::Char('f'),\n\t\t\t\tctrl: true,\n\t\t\t\talt: true,\n\t\t\t\t..\n\t\t\t} => {\n\t\t\t\tta.move_cursor(CursorMove::End);\n\t\t\t\ttrue\n\t\t\t}\n\t\t\tInput {\n\t\t\t\tkey: Key::Char('<'),\n\t\t\t\tctrl: false,\n\t\t\t\talt: true,\n\t\t\t\t..\n\t\t\t}\n\t\t\t| Input {\n\t\t\t\tkey: Key::Up | Key::Char('p'),\n\t\t\t\tctrl: true,\n\t\t\t\talt: true,\n\t\t\t\t..\n\t\t\t} => {\n\t\t\t\tta.move_cursor(CursorMove::Top);\n\t\t\t\ttrue\n\t\t\t}\n\t\t\tInput {\n\t\t\t\tkey: Key::Char('>'),\n\t\t\t\tctrl: false,\n\t\t\t\talt: true,\n\t\t\t\t..\n\t\t\t}\n\t\t\t| Input {\n\t\t\t\tkey: Key::Down | Key::Char('n'),\n\t\t\t\tctrl: true,\n\t\t\t\talt: true,\n\t\t\t\t..\n\t\t\t} => {\n\t\t\t\tta.move_cursor(CursorMove::Bottom);\n\t\t\t\ttrue\n\t\t\t}\n\t\t\tInput {\n\t\t\t\tkey: Key::Char('f'),\n\t\t\t\tctrl: false,\n\t\t\t\talt: true,\n\t\t\t\t..\n\t\t\t}\n\t\t\t| Input {\n\t\t\t\tkey: Key::Right,\n\t\t\t\tctrl: true,\n\t\t\t\talt: false,\n\t\t\t\t..\n\t\t\t} => {\n\t\t\t\tta.move_cursor(CursorMove::WordForward);\n\t\t\t\ttrue\n\t\t\t}\n\t\t\tInput {\n\t\t\t\tkey: Key::Char('b'),\n\t\t\t\tctrl: false,\n\t\t\t\talt: true,\n\t\t\t\t..\n\t\t\t}\n\t\t\t| Input {\n\t\t\t\tkey: Key::Left,\n\t\t\t\tctrl: true,\n\t\t\t\talt: false,\n\t\t\t\t..\n\t\t\t} => {\n\t\t\t\tta.move_cursor(CursorMove::WordBack);\n\t\t\t\ttrue\n\t\t\t}\n\n\t\t\tInput {\n\t\t\t\tkey: Key::Char(']'),\n\t\t\t\tctrl: false,\n\t\t\t\talt: true,\n\t\t\t\t..\n\t\t\t}\n\t\t\t| Input {\n\t\t\t\tkey: Key::Char('n'),\n\t\t\t\tctrl: false,\n\t\t\t\talt: true,\n\t\t\t\t..\n\t\t\t}\n\t\t\t| Input {\n\t\t\t\tkey: Key::Down,\n\t\t\t\tctrl: true,\n\t\t\t\talt: false,\n\t\t\t\t..\n\t\t\t} => {\n\t\t\t\tta.move_cursor(CursorMove::ParagraphForward);\n\t\t\t\ttrue\n\t\t\t}\n\t\t\tInput {\n\t\t\t\tkey: Key::Char('['),\n\t\t\t\tctrl: false,\n\t\t\t\talt: true,\n\t\t\t\t..\n\t\t\t}\n\t\t\t| Input {\n\t\t\t\tkey: Key::Char('p'),\n\t\t\t\tctrl: false,\n\t\t\t\talt: true,\n\t\t\t\t..\n\t\t\t}\n\t\t\t| Input {\n\t\t\t\tkey: Key::Up,\n\t\t\t\tctrl: true,\n\t\t\t\talt: false,\n\t\t\t\t..\n\t\t\t} => {\n\t\t\t\tta.move_cursor(CursorMove::ParagraphBack);\n\t\t\t\ttrue\n\t\t\t}\n\t\t\tInput {\n\t\t\t\tkey: Key::Char('u'),\n\t\t\t\tctrl: true,\n\t\t\t\talt: false,\n\t\t\t\t..\n\t\t\t} => {\n\t\t\t\tta.undo();\n\t\t\t\ttrue\n\t\t\t}\n\t\t\tInput {\n\t\t\t\tkey: Key::Char('r'),\n\t\t\t\tctrl: true,\n\t\t\t\talt: false,\n\t\t\t\t..\n\t\t\t} => {\n\t\t\t\tta.redo();\n\t\t\t\ttrue\n\t\t\t}\n\t\t\tInput {\n\t\t\t\tkey: Key::Char('y'),\n\t\t\t\tctrl: true,\n\t\t\t\talt: false,\n\t\t\t\t..\n\t\t\t} => {\n\t\t\t\tta.paste();\n\t\t\t\ttrue\n\t\t\t}\n\t\t\tInput {\n\t\t\t\tkey: Key::Char('v'),\n\t\t\t\tctrl: true,\n\t\t\t\talt: false,\n\t\t\t\t..\n\t\t\t}\n\t\t\t| Input {\n\t\t\t\tkey: Key::PageDown, ..\n\t\t\t} => {\n\t\t\t\tta.scroll(Scrolling::PageDown);\n\t\t\t\ttrue\n\t\t\t}\n\t\t\tInput {\n\t\t\t\tkey: Key::Char('v'),\n\t\t\t\tctrl: false,\n\t\t\t\talt: true,\n\t\t\t\t..\n\t\t\t}\n\t\t\t| Input {\n\t\t\t\tkey: Key::PageUp, ..\n\t\t\t} => {\n\t\t\t\tta.scroll(Scrolling::PageUp);\n\t\t\t\ttrue\n\t\t\t}\n\t\t\t_ => false,\n\t\t}\n\t}\n}\n\nimpl DrawableComponent for TextInputComponent {\n\tfn draw(&self, f: &mut Frame, rect: Rect) -> Result<()> {\n\t\t// this should always be true since draw should only be being called\n\t\t// is control is visible\n\t\tif let Some(ta) = &self.textarea {\n\t\t\tlet area = if self.embed {\n\t\t\t\trect\n\t\t\t} else if self.input_type == InputType::Multiline {\n\t\t\t\tlet area = ui::centered_rect(60, 20, f.area());\n\t\t\t\tui::rect_inside(\n\t\t\t\t\tSize::new(10, 3),\n\t\t\t\t\tf.area().into(),\n\t\t\t\t\tarea,\n\t\t\t\t)\n\t\t\t} else {\n\t\t\t\tlet area = ui::centered_rect(60, 1, f.area());\n\n\t\t\t\tui::rect_inside(\n\t\t\t\t\tSize::new(10, 3),\n\t\t\t\t\tf.area().into(),\n\t\t\t\t\tarea,\n\t\t\t\t)\n\t\t\t};\n\n\t\t\tf.render_widget(Clear, area);\n\n\t\t\tf.render_widget(ta, area);\n\n\t\t\tif self.show_char_count {\n\t\t\t\tself.draw_char_count(f, area);\n\t\t\t}\n\n\t\t\tself.current_area.set(area);\n\t\t}\n\t\tOk(())\n\t}\n}\n\nimpl Component for TextInputComponent {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\t_force_all: bool,\n\t) -> CommandBlocking {\n\t\tout.push(\n\t\t\tCommandInfo::new(\n\t\t\t\tstrings::commands::close_popup(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\tself.is_visible(),\n\t\t\t)\n\t\t\t.order(1),\n\t\t);\n\n\t\t//TODO: we might want to show the textarea specific commands here\n\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(&mut self, ev: &Event) -> Result<EventState> {\n\t\tlet input = Input::from(ev.clone());\n\t\tself.should_select(&input);\n\t\tif let Some(ta) = &mut self.textarea {\n\t\t\tlet modified = if let Event::Key(e) = ev {\n\t\t\t\tif key_match(e, self.key_config.keys.exit_popup) {\n\t\t\t\t\tself.hide();\n\t\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t\t}\n\n\t\t\t\tif key_match(e, self.key_config.keys.newline)\n\t\t\t\t\t&& self.input_type == InputType::Multiline\n\t\t\t\t{\n\t\t\t\t\tta.insert_newline();\n\t\t\t\t\ttrue\n\t\t\t\t} else {\n\t\t\t\t\tSelf::process_inputs(ta, &input)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfalse\n\t\t\t};\n\n\t\t\tif self.select_state\n\t\t\t\t== SelectionState::SelectionEndPending\n\t\t\t{\n\t\t\t\tta.cancel_selection();\n\t\t\t\tself.select_state = SelectionState::NotSelecting;\n\t\t\t}\n\n\t\t\tif modified {\n\t\t\t\tself.msg.take();\n\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t}\n\t\t}\n\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\t/*\n\t  visible maps to textarea Option\n\t  None = > not visible\n\t  Some => visible\n\t*/\n\tfn is_visible(&self) -> bool {\n\t\tself.textarea.is_some()\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.textarea = None;\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.show_inner_textarea();\n\t\tOk(())\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\n\t#[test]\n\tfn test_smoke() {\n\t\tlet env = Environment::test_env();\n\t\tlet mut comp = TextInputComponent::new(&env, \"\", \"\", false);\n\t\tcomp.show_inner_textarea();\n\t\tcomp.set_text(String::from(\"a\\nb\"));\n\t\tassert!(comp.is_visible());\n\t\tif let Some(ta) = &mut comp.textarea {\n\t\t\tassert_eq!(ta.cursor(), (0, 0));\n\n\t\t\tta.move_cursor(CursorMove::Forward);\n\t\t\tassert_eq!(ta.cursor(), (0, 1));\n\n\t\t\tta.move_cursor(CursorMove::Back);\n\t\t\tassert_eq!(ta.cursor(), (0, 0));\n\t\t}\n\t}\n\n\t#[test]\n\tfn text_cursor_initial_position() {\n\t\tlet env = Environment::test_env();\n\t\tlet mut comp = TextInputComponent::new(&env, \"\", \"\", false);\n\t\tcomp.show_inner_textarea();\n\t\tcomp.set_text(String::from(\"a\"));\n\t\tassert!(comp.is_visible());\n\t\tif let Some(ta) = &mut comp.textarea {\n\t\t\tlet txt = ta.lines();\n\t\t\tassert_eq!(txt[0].len(), 1);\n\t\t\tassert_eq!(txt[0].as_bytes()[0], b'a');\n\t\t}\n\t}\n\n\t#[test]\n\tfn test_multiline() {\n\t\tlet env = Environment::test_env();\n\t\tlet mut comp = TextInputComponent::new(&env, \"\", \"\", false);\n\t\tcomp.show_inner_textarea();\n\t\tcomp.set_text(String::from(\"a\\nb\\nc\"));\n\t\tassert!(comp.is_visible());\n\t\tif let Some(ta) = &mut comp.textarea {\n\t\t\tlet txt = ta.lines();\n\t\t\tassert_eq!(txt[0], \"a\");\n\t\t\tassert_eq!(txt[1], \"b\");\n\t\t\tassert_eq!(txt[2], \"c\");\n\t\t}\n\t}\n\n\t#[test]\n\tfn test_next_word_position() {\n\t\tlet env = Environment::test_env();\n\t\tlet mut comp = TextInputComponent::new(&env, \"\", \"\", false);\n\t\tcomp.show_inner_textarea();\n\t\tcomp.set_text(String::from(\"aa b;c\"));\n\t\tassert!(comp.is_visible());\n\t\tif let Some(ta) = &mut comp.textarea {\n\t\t\t// from word start\n\t\t\tta.move_cursor(CursorMove::Head);\n\t\t\tta.move_cursor(CursorMove::WordForward);\n\t\t\tassert_eq!(ta.cursor(), (0, 3));\n\t\t\t// from inside start\n\t\t\tta.move_cursor(CursorMove::Forward);\n\t\t\tta.move_cursor(CursorMove::WordForward);\n\t\t\tassert_eq!(ta.cursor(), (0, 5));\n\t\t\t// to string end\n\t\t\tta.move_cursor(CursorMove::Forward);\n\t\t\tta.move_cursor(CursorMove::WordForward);\n\t\t\tassert_eq!(ta.cursor(), (0, 6));\n\n\t\t\t// from string end\n\t\t\tta.move_cursor(CursorMove::Forward);\n\t\t\tlet save_cursor = ta.cursor();\n\t\t\tta.move_cursor(CursorMove::WordForward);\n\t\t\tassert_eq!(ta.cursor(), save_cursor);\n\t\t}\n\t}\n\n\t#[test]\n\tfn test_previous_word_position() {\n\t\tlet env = Environment::test_env();\n\t\tlet mut comp = TextInputComponent::new(&env, \"\", \"\", false);\n\t\tcomp.show_inner_textarea();\n\t\tcomp.set_text(String::from(\" a bb;c\"));\n\t\tassert!(comp.is_visible());\n\n\t\tif let Some(ta) = &mut comp.textarea {\n\t\t\t// from string end\n\t\t\tta.move_cursor(CursorMove::End);\n\t\t\tta.move_cursor(CursorMove::WordBack);\n\t\t\tassert_eq!(ta.cursor(), (0, 6));\n\t\t\t// from inside word\n\t\t\tta.move_cursor(CursorMove::Back);\n\t\t\tta.move_cursor(CursorMove::WordBack);\n\t\t\tassert_eq!(ta.cursor(), (0, 3));\n\t\t\t// from word start\n\t\t\tta.move_cursor(CursorMove::WordBack);\n\t\t\tassert_eq!(ta.cursor(), (0, 1));\n\t\t\t// to string start\n\t\t\tta.move_cursor(CursorMove::WordBack);\n\t\t\tassert_eq!(ta.cursor(), (0, 0));\n\t\t\t// from string start\n\t\t\tlet save_cursor = ta.cursor();\n\t\t\tta.move_cursor(CursorMove::WordBack);\n\n\t\t\tassert_eq!(ta.cursor(), save_cursor);\n\t\t}\n\t}\n\n\t#[test]\n\tfn test_next_word_multibyte() {\n\t\tlet env = Environment::test_env();\n\t\tlet mut comp = TextInputComponent::new(&env, \"\", \"\", false);\n\t\t// should emojis be word boundaries or not?\n\t\t// various editors (vs code, vim) do not agree with the\n\t\t// behavhior of the original textinput here.\n\t\t//\n\t\t// tui-textarea agrees with them.\n\t\t// So these tests are changed to match that behavior\n\t\t// FYI: this line is \"a à ❤ab🤯 a\"\n\n\t\t//              \"01245       89A        EFG\"\n\t\tlet text = dbg!(\"a à \\u{2764}ab\\u{1F92F} a\");\n\t\tcomp.show_inner_textarea();\n\t\tcomp.set_text(String::from(text));\n\t\tassert!(comp.is_visible());\n\n\t\tif let Some(ta) = &mut comp.textarea {\n\t\t\tta.move_cursor(CursorMove::Head);\n\t\t\tta.move_cursor(CursorMove::WordForward);\n\t\t\tassert_eq!(ta.cursor(), (0, 2));\n\t\t\tta.move_cursor(CursorMove::WordForward);\n\t\t\tassert_eq!(ta.cursor(), (0, 4));\n\t\t\tta.move_cursor(CursorMove::WordForward);\n\t\t\tassert_eq!(ta.cursor(), (0, 9));\n\t\t\tta.move_cursor(CursorMove::WordForward);\n\t\t\tassert_eq!(ta.cursor(), (0, 10));\n\t\t\tlet save_cursor = ta.cursor();\n\t\t\tta.move_cursor(CursorMove::WordForward);\n\t\t\tassert_eq!(ta.cursor(), save_cursor);\n\n\t\t\tta.move_cursor(CursorMove::End);\n\t\t\tta.move_cursor(CursorMove::WordBack);\n\t\t\tassert_eq!(ta.cursor(), (0, 9));\n\t\t\tta.move_cursor(CursorMove::WordBack);\n\t\t\tassert_eq!(ta.cursor(), (0, 4));\n\t\t\tta.move_cursor(CursorMove::WordBack);\n\t\t\tassert_eq!(ta.cursor(), (0, 2));\n\t\t\tta.move_cursor(CursorMove::WordBack);\n\t\t\tassert_eq!(ta.cursor(), (0, 0));\n\t\t\tlet save_cursor = ta.cursor();\n\t\t\tta.move_cursor(CursorMove::WordBack);\n\t\t\tassert_eq!(ta.cursor(), save_cursor);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/components/utils/emoji.rs",
    "content": "use once_cell::sync::Lazy;\nuse std::borrow::Cow;\n\nstatic EMOJI_REPLACER: Lazy<gh_emoji::Replacer> =\n\tLazy::new(gh_emoji::Replacer::new);\n\n// Replace markdown emojis with Unicode equivalent\n// :hammer: --> 🔨\n#[inline]\npub fn emojifi_string(s: String) -> String {\n\tif let Cow::Owned(altered_s) = EMOJI_REPLACER.replace_all(&s) {\n\t\taltered_s\n\t} else {\n\t\ts\n\t}\n}\n"
  },
  {
    "path": "src/components/utils/filetree.rs",
    "content": "//TODO: remove in favour of new `filetreelist` crate\n\nuse anyhow::{bail, Result};\nuse asyncgit::StatusItem;\nuse std::{\n\tcollections::BTreeSet,\n\tffi::OsStr,\n\tops::{Index, IndexMut},\n\tpath::Path,\n};\n\n/// holds the information shared among all `FileTreeItem` in a `FileTree`\n#[derive(Debug, Clone)]\npub struct TreeItemInfo {\n\t/// indent level\n\tpub indent: u8,\n\t/// currently visible depending on the folder collapse states\n\tpub visible: bool,\n\t/// just the last path element\n\tpub path: String,\n\t/// the full path\n\tpub full_path: String,\n}\n\nimpl TreeItemInfo {\n\tconst fn new(\n\t\tindent: u8,\n\t\tpath: String,\n\t\tfull_path: String,\n\t) -> Self {\n\t\tSelf {\n\t\t\tindent,\n\t\t\tvisible: true,\n\t\t\tpath,\n\t\t\tfull_path,\n\t\t}\n\t}\n}\n\n/// attribute used to indicate the collapse/expand state of a path item\n#[derive(PartialEq, Eq, Debug, Copy, Clone)]\npub struct PathCollapsed(pub bool);\n\n/// `FileTreeItem` can be of two kinds\n#[derive(PartialEq, Eq, Debug, Clone)]\npub enum FileTreeItemKind {\n\tPath(PathCollapsed),\n\tFile(StatusItem),\n}\n\n/// `FileTreeItem` can be of two kinds: see `FileTreeItem` but shares an info\n#[derive(Debug, Clone)]\npub struct FileTreeItem {\n\tpub info: TreeItemInfo,\n\tpub kind: FileTreeItemKind,\n}\n\nimpl FileTreeItem {\n\tfn new_file(item: &StatusItem) -> Result<Self> {\n\t\tlet item_path = Path::new(&item.path);\n\t\tlet indent = u8::try_from(\n\t\t\titem_path.ancestors().count().saturating_sub(2),\n\t\t)?;\n\n\t\tlet name = item_path\n\t\t\t.file_name()\n\t\t\t.map(OsStr::to_string_lossy)\n\t\t\t.map(|x| x.to_string());\n\n\t\tmatch name {\n\t\t\tSome(path) => Ok(Self {\n\t\t\t\tinfo: TreeItemInfo::new(\n\t\t\t\t\tindent,\n\t\t\t\t\tpath,\n\t\t\t\t\titem.path.clone(),\n\t\t\t\t),\n\t\t\t\tkind: FileTreeItemKind::File(item.clone()),\n\t\t\t}),\n\t\t\tNone => bail!(\"invalid file name {item:?}\"),\n\t\t}\n\t}\n\n\tfn new_path(\n\t\tpath: &Path,\n\t\tpath_string: String,\n\t\tcollapsed: bool,\n\t) -> Result<Self> {\n\t\tlet indent =\n\t\t\tu8::try_from(path.ancestors().count().saturating_sub(2))?;\n\n\t\tmatch path\n\t\t\t.components()\n\t\t\t.next_back()\n\t\t\t.map(std::path::Component::as_os_str)\n\t\t\t.map(OsStr::to_string_lossy)\n\t\t\t.map(String::from)\n\t\t{\n\t\t\tSome(path) => Ok(Self {\n\t\t\t\tinfo: TreeItemInfo::new(indent, path, path_string),\n\t\t\t\tkind: FileTreeItemKind::Path(PathCollapsed(\n\t\t\t\t\tcollapsed,\n\t\t\t\t)),\n\t\t\t}),\n\t\t\tNone => bail!(\"failed to create item from path\"),\n\t\t}\n\t}\n}\n\nimpl Eq for FileTreeItem {}\n\nimpl PartialEq for FileTreeItem {\n\tfn eq(&self, other: &Self) -> bool {\n\t\tself.info.full_path.eq(&other.info.full_path)\n\t}\n}\n\nimpl PartialOrd for FileTreeItem {\n\tfn partial_cmp(\n\t\t&self,\n\t\tother: &Self,\n\t) -> Option<std::cmp::Ordering> {\n\t\tSome(self.cmp(other))\n\t}\n}\n\nimpl Ord for FileTreeItem {\n\tfn cmp(&self, other: &Self) -> std::cmp::Ordering {\n\t\tself.info.path.cmp(&other.info.path)\n\t}\n}\n\n///\n#[derive(Default)]\npub struct FileTreeItems {\n\titems: Vec<FileTreeItem>,\n\tfile_count: usize,\n}\n\nimpl FileTreeItems {\n\t///\n\tpub(crate) fn new(\n\t\tlist: &[StatusItem],\n\t\tcollapsed: &BTreeSet<&String>,\n\t) -> Result<Self> {\n\t\tlet mut items = Vec::with_capacity(list.len());\n\t\tlet mut paths_added = BTreeSet::new();\n\n\t\tfor e in list {\n\t\t\t{\n\t\t\t\tlet item_path = Path::new(&e.path);\n\n\t\t\t\tSelf::push_dirs(\n\t\t\t\t\titem_path,\n\t\t\t\t\t&mut items,\n\t\t\t\t\t&mut paths_added,\n\t\t\t\t\tcollapsed,\n\t\t\t\t)?;\n\t\t\t}\n\n\t\t\titems.push(FileTreeItem::new_file(e)?);\n\t\t}\n\n\t\tOk(Self {\n\t\t\titems,\n\t\t\tfile_count: list.len(),\n\t\t})\n\t}\n\n\t///\n\tpub(crate) const fn items(&self) -> &Vec<FileTreeItem> {\n\t\t&self.items\n\t}\n\n\t///\n\tpub(crate) const fn len(&self) -> usize {\n\t\tself.items.len()\n\t}\n\n\t///\n\tpub const fn file_count(&self) -> usize {\n\t\tself.file_count\n\t}\n\n\t///\n\tpub(crate) fn find_parent_index(&self, index: usize) -> usize {\n\t\tlet item_indent = &self.items[index].info.indent;\n\t\tlet mut parent_index = index;\n\t\twhile item_indent <= &self.items[parent_index].info.indent {\n\t\t\tif parent_index == 0 {\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t\tparent_index -= 1;\n\t\t}\n\n\t\tparent_index\n\t}\n\n\tfn push_dirs<'a>(\n\t\titem_path: &'a Path,\n\t\tnodes: &mut Vec<FileTreeItem>,\n\t\tpaths_added: &mut BTreeSet<&'a Path>,\n\t\tcollapsed: &BTreeSet<&String>,\n\t) -> Result<()> {\n\t\tlet mut ancestors =\n\t\t\t{ item_path.ancestors().skip(1).collect::<Vec<_>>() };\n\t\tancestors.reverse();\n\n\t\tfor c in &ancestors {\n\t\t\tif c.parent().is_some() && !paths_added.contains(c) {\n\t\t\t\tpaths_added.insert(c);\n\t\t\t\t//TODO: get rid of expect\n\t\t\t\tlet path_string =\n\t\t\t\t\tString::from(c.to_str().expect(\"invalid path\"));\n\t\t\t\tlet is_collapsed = collapsed.contains(&path_string);\n\t\t\t\tnodes.push(FileTreeItem::new_path(\n\t\t\t\t\tc,\n\t\t\t\t\tpath_string,\n\t\t\t\t\tis_collapsed,\n\t\t\t\t)?);\n\t\t\t}\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tpub fn multiple_items_at_path(&self, index: usize) -> bool {\n\t\tlet tree_items = self.items();\n\t\tlet mut idx_temp_inner;\n\t\tif index + 2 < tree_items.len() {\n\t\t\tidx_temp_inner = index + 1;\n\t\t\twhile idx_temp_inner < tree_items.len().saturating_sub(1)\n\t\t\t\t&& tree_items[index].info.indent\n\t\t\t\t\t< tree_items[idx_temp_inner].info.indent\n\t\t\t{\n\t\t\t\tidx_temp_inner += 1;\n\t\t\t}\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\n\t\ttree_items[idx_temp_inner].info.indent\n\t\t\t== tree_items[index].info.indent\n\t}\n}\n\nimpl IndexMut<usize> for FileTreeItems {\n\tfn index_mut(&mut self, idx: usize) -> &mut Self::Output {\n\t\t&mut self.items[idx]\n\t}\n}\n\nimpl Index<usize> for FileTreeItems {\n\ttype Output = FileTreeItem;\n\n\tfn index(&self, idx: usize) -> &Self::Output {\n\t\t&self.items[idx]\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse asyncgit::StatusItemType;\n\n\tfn string_vec_to_status(items: &[&str]) -> Vec<StatusItem> {\n\t\titems\n\t\t\t.iter()\n\t\t\t.map(|a| StatusItem {\n\t\t\t\tpath: String::from(*a),\n\t\t\t\tstatus: StatusItemType::Modified,\n\t\t\t})\n\t\t\t.collect::<Vec<_>>()\n\t}\n\n\t#[test]\n\tfn test_simple() {\n\t\tlet items = string_vec_to_status(&[\n\t\t\t\"file.txt\", //\n\t\t]);\n\n\t\tlet res =\n\t\t\tFileTreeItems::new(&items, &BTreeSet::new()).unwrap();\n\n\t\tassert_eq!(\n\t\t\tres.items,\n\t\t\tvec![FileTreeItem {\n\t\t\t\tinfo: TreeItemInfo {\n\t\t\t\t\tpath: items[0].path.clone(),\n\t\t\t\t\tfull_path: items[0].path.clone(),\n\t\t\t\t\tindent: 0,\n\t\t\t\t\tvisible: true,\n\t\t\t\t},\n\t\t\t\tkind: FileTreeItemKind::File(items[0].clone())\n\t\t\t}]\n\t\t);\n\n\t\tlet items = string_vec_to_status(&[\n\t\t\t\"file.txt\",  //\n\t\t\t\"file2.txt\", //\n\t\t]);\n\n\t\tlet res =\n\t\t\tFileTreeItems::new(&items, &BTreeSet::new()).unwrap();\n\n\t\tassert_eq!(res.items.len(), 2);\n\t\tassert_eq!(res.items[1].info.path, items[1].path);\n\t}\n\n\t#[test]\n\tfn test_folder() {\n\t\tlet items = string_vec_to_status(&[\n\t\t\t\"a/file.txt\", //\n\t\t]);\n\n\t\tlet res = FileTreeItems::new(&items, &BTreeSet::new())\n\t\t\t.unwrap()\n\t\t\t.items\n\t\t\t.iter()\n\t\t\t.map(|i| i.info.full_path.clone())\n\t\t\t.collect::<Vec<_>>();\n\n\t\tassert_eq!(\n\t\t\tres,\n\t\t\tvec![String::from(\"a\"), items[0].path.clone(),]\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_indent() {\n\t\tlet items = string_vec_to_status(&[\n\t\t\t\"a/b/file.txt\", //\n\t\t]);\n\n\t\tlet list =\n\t\t\tFileTreeItems::new(&items, &BTreeSet::new()).unwrap();\n\t\tlet mut res = list\n\t\t\t.items\n\t\t\t.iter()\n\t\t\t.map(|i| (i.info.indent, i.info.path.as_str()));\n\n\t\tassert_eq!(res.next(), Some((0, \"a\")));\n\t\tassert_eq!(res.next(), Some((1, \"b\")));\n\t\tassert_eq!(res.next(), Some((2, \"file.txt\")));\n\t}\n\n\t#[test]\n\tfn test_indent_folder_file_name() {\n\t\tlet items = string_vec_to_status(&[\n\t\t\t\"a/b\",   //\n\t\t\t\"a.txt\", //\n\t\t]);\n\n\t\tlet list =\n\t\t\tFileTreeItems::new(&items, &BTreeSet::new()).unwrap();\n\t\tlet mut res = list\n\t\t\t.items\n\t\t\t.iter()\n\t\t\t.map(|i| (i.info.indent, i.info.path.as_str()));\n\n\t\tassert_eq!(res.next(), Some((0, \"a\")));\n\t\tassert_eq!(res.next(), Some((1, \"b\")));\n\t\tassert_eq!(res.next(), Some((0, \"a.txt\")));\n\t}\n\n\t#[test]\n\tfn test_folder_dup() {\n\t\tlet items = string_vec_to_status(&[\n\t\t\t\"a/file.txt\",  //\n\t\t\t\"a/file2.txt\", //\n\t\t]);\n\n\t\tlet res = FileTreeItems::new(&items, &BTreeSet::new())\n\t\t\t.unwrap()\n\t\t\t.items\n\t\t\t.iter()\n\t\t\t.map(|i| i.info.full_path.clone())\n\t\t\t.collect::<Vec<_>>();\n\n\t\tassert_eq!(\n\t\t\tres,\n\t\t\tvec![\n\t\t\t\tString::from(\"a\"),\n\t\t\t\titems[0].path.clone(),\n\t\t\t\titems[1].path.clone()\n\t\t\t]\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_multiple_items_at_path() {\n\t\t//0 a/\n\t\t//1   b/\n\t\t//2     c/\n\t\t//3       d\n\t\t//4     e/\n\t\t//5       f\n\n\t\tlet res = FileTreeItems::new(\n\t\t\t&string_vec_to_status(&[\n\t\t\t\t\"a/b/c/d\", //\n\t\t\t\t\"a/b/e/f\", //\n\t\t\t]),\n\t\t\t&BTreeSet::new(),\n\t\t)\n\t\t.unwrap();\n\n\t\tassert!(!res.multiple_items_at_path(0));\n\t\tassert!(!res.multiple_items_at_path(1));\n\t\tassert!(res.multiple_items_at_path(2));\n\t}\n\n\t#[test]\n\tfn test_find_parent() {\n\t\t//0 a/\n\t\t//1   b/\n\t\t//2     c\n\t\t//3     d\n\n\t\tlet res = FileTreeItems::new(\n\t\t\t&string_vec_to_status(&[\n\t\t\t\t\"a/b/c\", //\n\t\t\t\t\"a/b/d\", //\n\t\t\t]),\n\t\t\t&BTreeSet::new(),\n\t\t)\n\t\t.unwrap();\n\n\t\tassert_eq!(res.find_parent_index(3), 1);\n\t}\n}\n"
  },
  {
    "path": "src/components/utils/logitems.rs",
    "content": "use asyncgit::sync::{CommitId, CommitInfo};\nuse chrono::{DateTime, Duration, Local, Utc};\nuse indexmap::IndexSet;\nuse std::{rc::Rc, slice::Iter};\n\n#[cfg(feature = \"ghemoji\")]\nuse super::emoji::emojifi_string;\n\nstatic SLICE_OFFSET_RELOAD_THRESHOLD: usize = 100;\n\ntype BoxStr = Box<str>;\n\npub struct LogEntry {\n\t//TODO: cache string representation\n\tpub time: DateTime<Local>,\n\t//TODO: use tinyvec here\n\tpub author: BoxStr,\n\tpub msg: BoxStr,\n\t//TODO: use tinyvec here\n\tpub hash_short: BoxStr,\n\tpub id: CommitId,\n\tpub highlighted: bool,\n}\n\nimpl From<CommitInfo> for LogEntry {\n\tfn from(c: CommitInfo) -> Self {\n\t\tlet hash_short = c.id.get_short_string().into();\n\n\t\tlet time = {\n\t\t\tlet date = DateTime::from_timestamp(c.time, 0)\n\t\t\t\t.map(|d| d.naive_utc());\n\t\t\tif date.is_none() {\n\t\t\t\tlog::error!(\"error reading commit date: {hash_short} - timestamp: {}\",c.time);\n\t\t\t}\n\t\t\tDateTime::<Local>::from(\n\t\t\t\tDateTime::<Utc>::from_naive_utc_and_offset(\n\t\t\t\t\tdate.unwrap_or_default(),\n\t\t\t\t\tUtc,\n\t\t\t\t),\n\t\t\t)\n\t\t};\n\n\t\tlet author = c.author;\n\t\tlet msg = c.message;\n\n\t\t// Replace markdown emojis with Unicode equivalent\n\t\t#[cfg(feature = \"ghemoji\")]\n\t\tlet msg = emojifi_string(msg);\n\n\t\tSelf {\n\t\t\tauthor: author.into(),\n\t\t\tmsg: msg.into(),\n\t\t\ttime,\n\t\t\thash_short,\n\t\t\tid: c.id,\n\t\t\thighlighted: false,\n\t\t}\n\t}\n}\n\nimpl LogEntry {\n\tpub fn time_to_string(&self, now: DateTime<Local>) -> String {\n\t\tlet delta = now - self.time;\n\t\tif delta < Duration::try_minutes(30).unwrap_or_default() {\n\t\t\tlet delta_str = if delta\n\t\t\t\t< Duration::try_minutes(1).unwrap_or_default()\n\t\t\t{\n\t\t\t\t\"<1m ago\".to_string()\n\t\t\t} else {\n\t\t\t\tformat!(\"{:0>2}m ago\", delta.num_minutes())\n\t\t\t};\n\t\t\tformat!(\"{delta_str: <10}\")\n\t\t} else if self.time.date_naive() == now.date_naive() {\n\t\t\tself.time.format(\"%T  \").to_string()\n\t\t} else {\n\t\t\tself.time.format(\"%Y-%m-%d\").to_string()\n\t\t}\n\t}\n}\n\n///\n#[derive(Default)]\npub struct ItemBatch {\n\tindex_offset: Option<usize>,\n\titems: Vec<LogEntry>,\n\thighlighting: bool,\n}\n\nimpl ItemBatch {\n\tfn last_idx(&self) -> usize {\n\t\tself.index_offset() + self.items.len()\n\t}\n\n\t///\n\tpub fn index_offset(&self) -> usize {\n\t\tself.index_offset.unwrap_or_default()\n\t}\n\n\t///\n\tpub const fn index_offset_raw(&self) -> Option<usize> {\n\t\tself.index_offset\n\t}\n\n\t///\n\tpub const fn highlighting(&self) -> bool {\n\t\tself.highlighting\n\t}\n\n\t/// shortcut to get an `Iter` of our internal items\n\tpub fn iter(&self) -> Iter<'_, LogEntry> {\n\t\tself.items.iter()\n\t}\n\n\t/// clear current list of items\n\tpub fn clear(&mut self) {\n\t\tself.items.clear();\n\t\tself.index_offset = None;\n\t}\n\n\t/// insert new batch of items\n\tpub fn set_items(\n\t\t&mut self,\n\t\tstart_index: usize,\n\t\tcommits: Vec<CommitInfo>,\n\t\thighlighted: Option<&Rc<IndexSet<CommitId>>>,\n\t) {\n\t\tself.clear();\n\n\t\tif !commits.is_empty() {\n\t\t\tself.items.extend(commits.into_iter().map(|c| {\n\t\t\t\tlet id = c.id;\n\t\t\t\tlet mut entry = LogEntry::from(c);\n\t\t\t\tif highlighted.as_ref().is_some_and(|highlighted| {\n\t\t\t\t\thighlighted.contains(&id)\n\t\t\t\t}) {\n\t\t\t\t\tentry.highlighted = true;\n\t\t\t\t}\n\t\t\t\tentry\n\t\t\t}));\n\n\t\t\tself.index_offset = Some(start_index);\n\t\t\tself.highlighting = highlighted.is_some();\n\t\t}\n\t}\n\n\t/// returns `true` if we should fetch updated list of items\n\tpub fn needs_data(&self, idx: usize, idx_max: usize) -> bool {\n\t\tlet want_min =\n\t\t\tidx.saturating_sub(SLICE_OFFSET_RELOAD_THRESHOLD);\n\t\tlet want_max = idx\n\t\t\t.saturating_add(SLICE_OFFSET_RELOAD_THRESHOLD)\n\t\t\t.min(idx_max);\n\n\t\tlet needs_data_top = want_min < self.index_offset();\n\t\tlet needs_data_bottom = want_max >= self.last_idx();\n\t\tneeds_data_bottom || needs_data_top\n\t}\n}\n\nimpl<'a> IntoIterator for &'a ItemBatch {\n\ttype IntoIter = std::slice::Iter<\n\t\t'a,\n\t\tcrate::components::utils::logitems::LogEntry,\n\t>;\n\ttype Item = &'a crate::components::utils::logitems::LogEntry;\n\tfn into_iter(self) -> Self::IntoIter {\n\t\tself.iter()\n\t}\n}\n\n#[cfg(test)]\n#[cfg(feature = \"ghemoji\")]\nmod tests {\n\tuse super::*;\n\n\tfn test_conversion(s: &str) -> String {\n\t\temojifi_string(s.into())\n\t}\n\n\t#[test]\n\tfn test_emojifi_string_conversion_cases() {\n\t\tassert_eq!(\n\t\t\t&test_conversion(\"It's :hammer: time!\"),\n\t\t\t\"It's 🔨 time!\"\n\t\t);\n\t\tassert_eq!(\n\t\t\t&test_conversion(\":red_circle::orange_circle::yellow_circle::green_circle::large_blue_circle::purple_circle:\"),\n\t\t\t\"🔴🟠🟡🟢🔵🟣\"\n\t\t);\n\t\tassert_eq!(\n\t\t\t&test_conversion(\"It's raining :cat:s and :dog:s\"),\n\t\t\t\"It's raining 🐱s and 🐶s\"\n\t\t);\n\t\tassert_eq!(&test_conversion(\":crab: rules!\"), \"🦀 rules!\");\n\t}\n\n\t#[test]\n\tfn test_emojifi_string_no_conversion_cases() {\n\t\tassert_eq!(&test_conversion(\"123\"), \"123\");\n\t\tassert_eq!(\n\t\t\t&test_conversion(\"This :should_not_convert:\"),\n\t\t\t\"This :should_not_convert:\"\n\t\t);\n\t\tassert_eq!(&test_conversion(\":gopher:\"), \":gopher:\");\n\t}\n}\n"
  },
  {
    "path": "src/components/utils/mod.rs",
    "content": "use chrono::{DateTime, Local, Utc};\nuse unicode_width::UnicodeWidthStr;\n\n#[cfg(feature = \"ghemoji\")]\npub mod emoji;\npub mod filetree;\npub mod logitems;\npub mod scroll_horizontal;\npub mod scroll_vertical;\npub mod statustree;\n\n/// macro to simplify running code that might return Err.\n/// It will show a popup in that case\n#[macro_export]\nmacro_rules! try_or_popup {\n\t($self:ident, $msg:expr, $e:expr) => {\n\t\tif let Err(err) = $e {\n\t\t\t::log::error!(\"{} {}\", $msg, err);\n\t\t\t$self.queue.push(\n\t\t\t\t$crate::queue::InternalEvent::ShowErrorMsg(format!(\n\t\t\t\t\t\"{}\\n{}\",\n\t\t\t\t\t$msg, err\n\t\t\t\t)),\n\t\t\t);\n\t\t}\n\t};\n}\n\n/// helper func to convert unix time since epoch to formatted time string in local timezone\npub fn time_to_string(secs: i64, short: bool) -> String {\n\tlet time = DateTime::<Local>::from(\n\t\tDateTime::<Utc>::from_naive_utc_and_offset(\n\t\t\tDateTime::from_timestamp(secs, 0)\n\t\t\t\t.unwrap_or_default()\n\t\t\t\t.naive_utc(),\n\t\t\tUtc,\n\t\t),\n\t);\n\n\ttime.format(if short {\n\t\t\"%Y-%m-%d\"\n\t} else {\n\t\t\"%Y-%m-%d %H:%M:%S\"\n\t})\n\t.to_string()\n}\n\n#[inline]\npub fn string_width_align(s: &str, width: usize) -> String {\n\tstatic POSTFIX: &str = \"..\";\n\n\tlet len = UnicodeWidthStr::width(s);\n\tlet width_wo_postfix = width.saturating_sub(POSTFIX.len());\n\n\tif (len >= width_wo_postfix && len <= width)\n\t\t|| (len <= width_wo_postfix)\n\t{\n\t\tformat!(\"{s:width$}\")\n\t} else {\n\t\tlet mut s = s.to_string();\n\t\ts.truncate(find_truncate_point(&s, width_wo_postfix));\n\t\tformat!(\"{s}{POSTFIX}\")\n\t}\n}\n\n#[inline]\nfn find_truncate_point(s: &str, chars: usize) -> usize {\n\ts.chars().take(chars).map(char::len_utf8).sum()\n}\n"
  },
  {
    "path": "src/components/utils/scroll_horizontal.rs",
    "content": "use crate::{\n\tcomponents::HorizontalScrollType,\n\tui::{draw_scrollbar, style::SharedTheme, Orientation},\n};\nuse ratatui::{layout::Rect, Frame};\nuse std::cell::Cell;\n\npub struct HorizontalScroll {\n\tright: Cell<usize>,\n\tmax_right: Cell<usize>,\n}\n\nimpl HorizontalScroll {\n\tpub const fn new() -> Self {\n\t\tSelf {\n\t\t\tright: Cell::new(0),\n\t\t\tmax_right: Cell::new(0),\n\t\t}\n\t}\n\n\tpub const fn get_right(&self) -> usize {\n\t\tself.right.get()\n\t}\n\n\tpub fn reset(&self) {\n\t\tself.right.set(0);\n\t}\n\n\tpub fn move_right(\n\t\t&self,\n\t\tmove_type: HorizontalScrollType,\n\t) -> bool {\n\t\tlet old = self.right.get();\n\t\tlet max = self.max_right.get();\n\n\t\tlet new_scroll_right = match move_type {\n\t\t\tHorizontalScrollType::Left => old.saturating_sub(1),\n\t\t\tHorizontalScrollType::Right => old.saturating_add(1),\n\t\t};\n\n\t\tlet new_scroll_right = new_scroll_right.clamp(0, max);\n\n\t\tif new_scroll_right == old {\n\t\t\treturn false;\n\t\t}\n\n\t\tself.right.set(new_scroll_right);\n\n\t\ttrue\n\t}\n\n\tpub fn update(\n\t\t&self,\n\t\tselection: usize,\n\t\tmax_selection: usize,\n\t\tvisual_width: usize,\n\t) -> usize {\n\t\tlet new_right = calc_scroll_right(\n\t\t\tself.get_right(),\n\t\t\tvisual_width,\n\t\t\tselection,\n\t\t\tmax_selection,\n\t\t);\n\t\tself.right.set(new_right);\n\n\t\tif visual_width == 0 {\n\t\t\tself.max_right.set(0);\n\t\t} else {\n\t\t\tlet new_max_right =\n\t\t\t\tmax_selection.saturating_sub(visual_width);\n\t\t\tself.max_right.set(new_max_right);\n\t\t}\n\n\t\tnew_right\n\t}\n\n\tpub fn update_no_selection(\n\t\t&self,\n\t\tcolumn_count: usize,\n\t\tvisual_width: usize,\n\t) -> usize {\n\t\tself.update(self.get_right(), column_count, visual_width)\n\t}\n\n\tpub fn draw(&self, f: &mut Frame, r: Rect, theme: &SharedTheme) {\n\t\tdraw_scrollbar(\n\t\t\tf,\n\t\t\tr,\n\t\t\ttheme,\n\t\t\tself.max_right.get(),\n\t\t\tself.right.get(),\n\t\t\tOrientation::Horizontal,\n\t\t);\n\t}\n}\n\nconst fn calc_scroll_right(\n\tcurrent_right: usize,\n\twidth_in_lines: usize,\n\tselection: usize,\n\tselection_max: usize,\n) -> usize {\n\tif width_in_lines == 0 {\n\t\treturn 0;\n\t}\n\tif selection_max <= width_in_lines {\n\t\treturn 0;\n\t}\n\n\tif current_right + width_in_lines <= selection {\n\t\tselection.saturating_sub(width_in_lines) + 1\n\t} else if current_right > selection {\n\t\tselection\n\t} else {\n\t\tcurrent_right\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse pretty_assertions::assert_eq;\n\n\t#[test]\n\tfn test_scroll_no_scroll_to_right() {\n\t\tassert_eq!(calc_scroll_right(1, 10, 4, 4), 0);\n\t}\n\n\t#[test]\n\tfn test_scroll_zero_width() {\n\t\tassert_eq!(calc_scroll_right(4, 0, 4, 3), 0);\n\t}\n}\n"
  },
  {
    "path": "src/components/utils/scroll_vertical.rs",
    "content": "use crate::{\n\tcomponents::ScrollType,\n\tui::{draw_scrollbar, style::SharedTheme, Orientation},\n};\nuse ratatui::{layout::Rect, Frame};\nuse std::cell::Cell;\n\npub struct VerticalScroll {\n\ttop: Cell<usize>,\n\tmax_top: Cell<usize>,\n\tvisual_height: Cell<usize>,\n}\n\nimpl VerticalScroll {\n\tpub const fn new() -> Self {\n\t\tSelf {\n\t\t\ttop: Cell::new(0),\n\t\t\tmax_top: Cell::new(0),\n\t\t\tvisual_height: Cell::new(0),\n\t\t}\n\t}\n\n\tpub const fn get_top(&self) -> usize {\n\t\tself.top.get()\n\t}\n\n\tpub fn reset(&self) {\n\t\tself.top.set(0);\n\t}\n\n\tpub fn move_top(&self, move_type: ScrollType) -> bool {\n\t\tlet old = self.top.get();\n\t\tlet max = self.max_top.get();\n\n\t\tlet new_scroll_top = match move_type {\n\t\t\tScrollType::Down => old.saturating_add(1),\n\t\t\tScrollType::Up => old.saturating_sub(1),\n\t\t\tScrollType::PageDown => old\n\t\t\t\t.saturating_sub(1)\n\t\t\t\t.saturating_add(self.visual_height.get()),\n\t\t\tScrollType::PageUp => old\n\t\t\t\t.saturating_add(1)\n\t\t\t\t.saturating_sub(self.visual_height.get()),\n\t\t\tScrollType::Home => 0,\n\t\t\tScrollType::End => max,\n\t\t};\n\n\t\tlet new_scroll_top = new_scroll_top.clamp(0, max);\n\n\t\tif new_scroll_top == old {\n\t\t\treturn false;\n\t\t}\n\n\t\tself.top.set(new_scroll_top);\n\n\t\ttrue\n\t}\n\n\tpub fn move_area_to_visible(\n\t\t&self,\n\t\theight: usize,\n\t\tstart: usize,\n\t\tend: usize,\n\t) {\n\t\tlet top = self.top.get();\n\t\tlet bottom = top + height;\n\t\tlet max_top = self.max_top.get();\n\t\t// the top of some content is hidden\n\t\tif start < top {\n\t\t\tself.top.set(start);\n\t\t\treturn;\n\t\t}\n\t\t// the bottom of some content is hidden and there is visible space available\n\t\tif end > bottom && start > top {\n\t\t\tlet avail_space = start.saturating_sub(top);\n\t\t\tlet diff = std::cmp::min(\n\t\t\t\tavail_space,\n\t\t\t\tend.saturating_sub(bottom),\n\t\t\t);\n\t\t\tlet top = top.saturating_add(diff);\n\t\t\tself.top.set(std::cmp::min(max_top, top));\n\t\t}\n\t}\n\n\tpub fn update(\n\t\t&self,\n\t\tselection: usize,\n\t\tselection_max: usize,\n\t\tvisual_height: usize,\n\t) -> usize {\n\t\tself.visual_height.set(visual_height);\n\n\t\tlet new_top = calc_scroll_top(\n\t\t\tself.get_top(),\n\t\t\tvisual_height,\n\t\t\tselection,\n\t\t\tselection_max,\n\t\t);\n\t\tself.top.set(new_top);\n\n\t\tif visual_height == 0 {\n\t\t\tself.max_top.set(0);\n\t\t} else {\n\t\t\tlet new_max = selection_max.saturating_sub(visual_height);\n\t\t\tself.max_top.set(new_max);\n\t\t}\n\n\t\tnew_top\n\t}\n\n\tpub fn update_no_selection(\n\t\t&self,\n\t\tline_count: usize,\n\t\tvisual_height: usize,\n\t) -> usize {\n\t\tself.update(self.get_top(), line_count, visual_height)\n\t}\n\n\tpub fn draw(&self, f: &mut Frame, r: Rect, theme: &SharedTheme) {\n\t\tdraw_scrollbar(\n\t\t\tf,\n\t\t\tr,\n\t\t\ttheme,\n\t\t\tself.max_top.get(),\n\t\t\tself.top.get(),\n\t\t\tOrientation::Vertical,\n\t\t);\n\t}\n}\n\nconst fn calc_scroll_top(\n\tcurrent_top: usize,\n\theight_in_lines: usize,\n\tselection: usize,\n\tselection_max: usize,\n) -> usize {\n\tif height_in_lines == 0 {\n\t\treturn 0;\n\t}\n\tif selection_max <= height_in_lines {\n\t\treturn 0;\n\t}\n\n\tif current_top + height_in_lines <= selection {\n\t\tselection.saturating_sub(height_in_lines) + 1\n\t} else if current_top > selection {\n\t\tselection\n\t} else {\n\t\tcurrent_top\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse pretty_assertions::assert_eq;\n\n\t#[test]\n\tfn test_scroll_no_scroll_to_top() {\n\t\tassert_eq!(calc_scroll_top(1, 10, 4, 4), 0);\n\t}\n\n\t#[test]\n\tfn test_scroll_zero_height() {\n\t\tassert_eq!(calc_scroll_top(4, 0, 4, 3), 0);\n\t}\n\n\t#[test]\n\tfn test_scroll_bottom_into_view() {\n\t\tlet visual_height = 10;\n\t\tlet line_count = 20;\n\t\tlet scroll = VerticalScroll::new();\n\t\tscroll.max_top.set(line_count - visual_height);\n\n\t\t// intersecting with the bottom of the visible area\n\t\tscroll.move_area_to_visible(visual_height, 9, 11);\n\t\tassert_eq!(scroll.get_top(), 1);\n\n\t\t// completely below the visible area\n\t\tscroll.move_area_to_visible(visual_height, 15, 17);\n\t\tassert_eq!(scroll.get_top(), 7);\n\n\t\t// scrolling to the bottom overflow\n\t\tscroll.move_area_to_visible(visual_height, 30, 40);\n\t\tassert_eq!(scroll.get_top(), 10);\n\t}\n\n\t#[test]\n\tfn test_scroll_top_into_view() {\n\t\tlet visual_height = 10;\n\t\tlet line_count = 20;\n\t\tlet scroll = VerticalScroll::new();\n\t\tscroll.max_top.set(line_count - visual_height);\n\t\tscroll.top.set(4);\n\n\t\t// intersecting with the top of the visible area\n\t\tscroll.move_area_to_visible(visual_height, 2, 8);\n\t\tassert_eq!(scroll.get_top(), 2);\n\n\t\t// completely above the visible area\n\t\tscroll.move_area_to_visible(visual_height, 0, 2);\n\t\tassert_eq!(scroll.get_top(), 0);\n\t}\n\n\t#[test]\n\tfn test_scroll_with_pageup_pagedown() {\n\t\tlet scroll = VerticalScroll::new();\n\t\tscroll.max_top.set(10);\n\t\tscroll.visual_height.set(8);\n\n\t\tassert!(scroll.move_top(ScrollType::End));\n\t\tassert_eq!(scroll.get_top(), 10);\n\n\t\tassert!(!scroll.move_top(ScrollType::PageDown));\n\t\tassert_eq!(scroll.get_top(), 10);\n\n\t\tassert!(scroll.move_top(ScrollType::PageUp));\n\t\tassert_eq!(scroll.get_top(), 3);\n\n\t\tassert!(scroll.move_top(ScrollType::PageUp));\n\t\tassert_eq!(scroll.get_top(), 0);\n\n\t\tassert!(!scroll.move_top(ScrollType::PageUp));\n\t\tassert_eq!(scroll.get_top(), 0);\n\t}\n}\n"
  },
  {
    "path": "src/components/utils/statustree.rs",
    "content": "use super::filetree::{\n\tFileTreeItem, FileTreeItemKind, FileTreeItems, PathCollapsed,\n};\nuse anyhow::Result;\nuse asyncgit::StatusItem;\nuse std::{cell::Cell, cmp, collections::BTreeSet};\n\n//TODO: use new `filetreelist` crate\n\n///\n#[derive(Default)]\npub struct StatusTree {\n\tpub tree: FileTreeItems,\n\tpub selection: Option<usize>,\n\n\t// some folders may be folded up, this allows jumping\n\t// over folders which are folded into their parent\n\tpub available_selections: Vec<usize>,\n\n\tpub window_height: Cell<Option<usize>>,\n}\n\n///\n#[derive(Copy, Clone, Debug)]\npub enum MoveSelection {\n\tUp,\n\tDown,\n\tLeft,\n\tRight,\n\tHome,\n\tEnd,\n\tPageDown,\n\tPageUp,\n}\n\n#[derive(Copy, Clone, Debug)]\nstruct SelectionChange {\n\tnew_index: usize,\n\tchanges: bool,\n}\nimpl SelectionChange {\n\tconst fn new(new_index: usize, changes: bool) -> Self {\n\t\tSelf { new_index, changes }\n\t}\n}\n\nimpl StatusTree {\n\t/// update tree with a new list, try to retain selection and collapse states\n\tpub fn update(&mut self, list: &[StatusItem]) -> Result<()> {\n\t\tlet last_collapsed = self.all_collapsed();\n\n\t\tlet last_selection =\n\t\t\tself.selected_item().map(|e| e.info.full_path);\n\t\tlet last_selection_index = self.selection.unwrap_or(0);\n\n\t\tself.tree = FileTreeItems::new(list, &last_collapsed)?;\n\t\tself.selection = last_selection.as_ref().map_or_else(\n\t\t\t|| self.tree.items().first().map(|_| 0),\n\t\t\t|last_selection| {\n\t\t\t\tself.find_last_selection(\n\t\t\t\t\tlast_selection,\n\t\t\t\t\tlast_selection_index,\n\t\t\t\t)\n\t\t\t\t.or_else(|| self.tree.items().first().map(|_| 0))\n\t\t\t},\n\t\t);\n\n\t\tself.update_visibility(None, 0, true);\n\t\tself.available_selections = self.setup_available_selections();\n\n\t\t//NOTE: now that visibility is set we can make sure selection is visible\n\t\tif let Some(idx) = self.selection {\n\t\t\tself.selection = Some(self.find_visible_idx(idx));\n\t\t}\n\n\t\tOk(())\n\t}\n\n\t/// Return which indices can be selected, taking into account that\n\t/// some folders may be folded up into their parent\n\t///\n\t/// It should be impossible to select a folder which has been folded into its parent\n\tfn setup_available_selections(&self) -> Vec<usize> {\n\t\t// use the same algorithm as in filetree build_vec_text_for_drawing function\n\t\tlet mut should_skip_over: usize = 0;\n\t\tlet mut vec_available_selections: Vec<usize> = vec![];\n\t\tlet tree_items = self.tree.items();\n\t\tfor index in 0..tree_items.len() {\n\t\t\tif should_skip_over > 0 {\n\t\t\t\tshould_skip_over -= 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tlet mut idx_temp = index;\n\t\t\tvec_available_selections.push(index);\n\n\t\t\twhile idx_temp < tree_items.len().saturating_sub(2)\n\t\t\t\t&& tree_items[idx_temp].info.indent\n\t\t\t\t\t< tree_items[idx_temp + 1].info.indent\n\t\t\t{\n\t\t\t\t// fold up the folder/file\n\t\t\t\tidx_temp += 1;\n\t\t\t\tshould_skip_over += 1;\n\n\t\t\t\t// don't fold files up\n\t\t\t\tif let FileTreeItemKind::File(_) =\n\t\t\t\t\t&tree_items[idx_temp].kind\n\t\t\t\t{\n\t\t\t\t\tshould_skip_over -= 1;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\t// don't fold up if more than one folder in folder\n\t\t\t\tif self.tree.multiple_items_at_path(idx_temp) {\n\t\t\t\t\tshould_skip_over -= 1;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tvec_available_selections\n\t}\n\n\tfn find_visible_idx(&self, mut idx: usize) -> usize {\n\t\twhile idx > 0 {\n\t\t\tif self.is_visible_index(idx) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tidx -= 1;\n\t\t}\n\n\t\tidx\n\t}\n\n\t///\n\tpub fn move_selection(&mut self, dir: MoveSelection) -> bool {\n\t\tself.selection.is_some_and(|selection| {\n\t\t\tlet selection_change = match dir {\n\t\t\t\tMoveSelection::Up => {\n\t\t\t\t\tself.selection_updown(selection, true)\n\t\t\t\t}\n\t\t\t\tMoveSelection::Down => {\n\t\t\t\t\tself.selection_updown(selection, false)\n\t\t\t\t}\n\t\t\t\tMoveSelection::Left => self.selection_left(selection),\n\t\t\t\tMoveSelection::Right => {\n\t\t\t\t\tself.selection_right(selection)\n\t\t\t\t}\n\t\t\t\tMoveSelection::Home => SelectionChange::new(0, false),\n\t\t\t\tMoveSelection::End => self.selection_end(),\n\t\t\t\tMoveSelection::PageUp => self.selection_page_updown(\n\t\t\t\t\tselection,\n\t\t\t\t\t(0..=selection).rev(),\n\t\t\t\t),\n\t\t\t\tMoveSelection::PageDown => self\n\t\t\t\t\t.selection_page_updown(\n\t\t\t\t\t\tselection,\n\t\t\t\t\t\tselection..(self.tree.len()),\n\t\t\t\t\t),\n\t\t\t};\n\n\t\t\tlet changed_index =\n\t\t\t\tselection_change.new_index != selection;\n\n\t\t\tself.selection = Some(selection_change.new_index);\n\n\t\t\tchanged_index || selection_change.changes\n\t\t})\n\t}\n\n\t///\n\tpub fn selected_item(&self) -> Option<FileTreeItem> {\n\t\tself.selection.map(|i| self.tree[i].clone())\n\t}\n\n\t///\n\tpub const fn is_empty(&self) -> bool {\n\t\tself.tree.items().is_empty()\n\t}\n\n\tfn all_collapsed(&self) -> BTreeSet<&String> {\n\t\tlet mut res = BTreeSet::new();\n\n\t\tfor i in self.tree.items() {\n\t\t\tif let FileTreeItemKind::Path(PathCollapsed(collapsed)) =\n\t\t\t\ti.kind\n\t\t\t{\n\t\t\t\tif collapsed {\n\t\t\t\t\tres.insert(&i.info.full_path);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tres\n\t}\n\n\tfn find_last_selection(\n\t\t&self,\n\t\tlast_selection: &str,\n\t\tlast_index: usize,\n\t) -> Option<usize> {\n\t\tif self.is_empty() {\n\t\t\treturn None;\n\t\t}\n\n\t\tif let Ok(i) = self.tree.items().binary_search_by(|e| {\n\t\t\te.info.full_path.as_str().cmp(last_selection)\n\t\t}) {\n\t\t\treturn Some(i);\n\t\t}\n\n\t\tSome(cmp::min(last_index, self.tree.len() - 1))\n\t}\n\n\tfn selection_updown(\n\t\t&self,\n\t\tcurrent_index: usize,\n\t\tup: bool,\n\t) -> SelectionChange {\n\t\tlet mut current_index_in_available_selections;\n\t\tlet mut cur_index_find = current_index;\n\t\tif self.available_selections.is_empty() {\n\t\t\t// Go to top\n\t\t\tcurrent_index_in_available_selections = 0;\n\t\t} else {\n\t\t\tloop {\n\t\t\t\tif let Some(pos) = self\n\t\t\t\t\t.available_selections\n\t\t\t\t\t.iter()\n\t\t\t\t\t.position(|i| *i == cur_index_find)\n\t\t\t\t{\n\t\t\t\t\tcurrent_index_in_available_selections = pos;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\t// Find the closest to the index, usually this shouldn't happen\n\t\t\t\tif current_index == 0 {\n\t\t\t\t\t// This should never happen\n\t\t\t\t\tcurrent_index_in_available_selections = 0;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcur_index_find -= 1;\n\t\t\t}\n\t\t}\n\n\t\tlet mut new_index;\n\n\t\tloop {\n\t\t\t// Use available_selections to go to the correct selection as\n\t\t\t// some of the folders may be folded up\n\t\t\tnew_index = if up {\n\t\t\t\tcurrent_index_in_available_selections =\n\t\t\t\t\tcurrent_index_in_available_selections\n\t\t\t\t\t\t.saturating_sub(1);\n\t\t\t\tself.available_selections\n\t\t\t\t\t[current_index_in_available_selections]\n\t\t\t} else if current_index_in_available_selections\n\t\t\t\t.saturating_add(1)\n\t\t\t\t<= self.available_selections.len().saturating_sub(1)\n\t\t\t{\n\t\t\t\tcurrent_index_in_available_selections =\n\t\t\t\t\tcurrent_index_in_available_selections\n\t\t\t\t\t\t.saturating_add(1);\n\t\t\t\tself.available_selections\n\t\t\t\t\t[current_index_in_available_selections]\n\t\t\t} else {\n\t\t\t\t// can't move down anymore\n\t\t\t\tnew_index = current_index;\n\t\t\t\tbreak;\n\t\t\t};\n\n\t\t\tif self.is_visible_index(new_index) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tSelectionChange::new(new_index, false)\n\t}\n\n\tfn selection_end(&self) -> SelectionChange {\n\t\tlet items_max = self.tree.len().saturating_sub(1);\n\n\t\tlet mut new_index = items_max;\n\n\t\tloop {\n\t\t\tif self.is_visible_index(new_index) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tif new_index == 0 {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tnew_index = new_index.saturating_sub(1);\n\t\t\tnew_index = cmp::min(new_index, items_max);\n\t\t}\n\n\t\tSelectionChange::new(new_index, false)\n\t}\n\n\tfn selection_page_updown(\n\t\t&self,\n\t\tcurrent_index: usize,\n\t\trange: impl Iterator<Item = usize>,\n\t) -> SelectionChange {\n\t\tlet page_size = self.window_height.get().unwrap_or(0);\n\n\t\tlet new_index = range\n\t\t\t.filter(|index| {\n\t\t\t\tself.available_selections.contains(index)\n\t\t\t\t\t&& self.is_visible_index(*index)\n\t\t\t})\n\t\t\t.take(page_size)\n\t\t\t.last()\n\t\t\t.unwrap_or(current_index);\n\n\t\tSelectionChange::new(new_index, false)\n\t}\n\n\tfn is_visible_index(&self, idx: usize) -> bool {\n\t\tself.tree[idx].info.visible\n\t}\n\n\tfn selection_right(\n\t\t&mut self,\n\t\tcurrent_selection: usize,\n\t) -> SelectionChange {\n\t\tlet item_kind = self.tree[current_selection].kind.clone();\n\t\tlet item_path =\n\t\t\tself.tree[current_selection].info.full_path.clone();\n\n\t\tmatch item_kind {\n\t\t\tFileTreeItemKind::Path(PathCollapsed(collapsed))\n\t\t\t\tif collapsed =>\n\t\t\t{\n\t\t\t\tself.expand(&item_path, current_selection);\n\t\t\t\treturn SelectionChange::new(current_selection, true);\n\t\t\t}\n\t\t\tFileTreeItemKind::Path(PathCollapsed(collapsed))\n\t\t\t\tif !collapsed =>\n\t\t\t{\n\t\t\t\treturn self\n\t\t\t\t\t.selection_updown(current_selection, false);\n\t\t\t}\n\t\t\t_ => (),\n\t\t}\n\n\t\tSelectionChange::new(current_selection, false)\n\t}\n\n\tfn selection_left(\n\t\t&mut self,\n\t\tcurrent_selection: usize,\n\t) -> SelectionChange {\n\t\tlet item_kind = self.tree[current_selection].kind.clone();\n\t\tlet item_path =\n\t\t\tself.tree[current_selection].info.full_path.clone();\n\n\t\tif matches!(item_kind, FileTreeItemKind::File(_))\n\t\t\t|| matches!(item_kind,FileTreeItemKind::Path(PathCollapsed(collapsed))\n        if collapsed)\n\t\t{\n\t\t\tlet mut cur_parent =\n\t\t\t\tself.tree.find_parent_index(current_selection);\n\t\t\twhile !self.available_selections.contains(&cur_parent)\n\t\t\t\t&& cur_parent != 0\n\t\t\t{\n\t\t\t\tcur_parent = self.tree.find_parent_index(cur_parent);\n\t\t\t}\n\t\t\tSelectionChange::new(cur_parent, false)\n\t\t} else if matches!(item_kind,  FileTreeItemKind::Path(PathCollapsed(collapsed))\n        if !collapsed)\n\t\t{\n\t\t\tself.collapse(&item_path, current_selection);\n\t\t\tSelectionChange::new(current_selection, true)\n\t\t} else {\n\t\t\tSelectionChange::new(current_selection, false)\n\t\t}\n\t}\n\n\tfn collapse(&mut self, path: &str, index: usize) {\n\t\tif let FileTreeItemKind::Path(PathCollapsed(\n\t\t\tref mut collapsed,\n\t\t)) = self.tree[index].kind\n\t\t{\n\t\t\t*collapsed = true;\n\t\t}\n\n\t\tlet path = format!(\"{path}/\");\n\n\t\tfor i in index + 1..self.tree.len() {\n\t\t\tlet item = &mut self.tree[i];\n\t\t\tlet item_path = &item.info.full_path;\n\t\t\tif item_path.starts_with(&path) {\n\t\t\t\titem.info.visible = false;\n\t\t\t} else {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\tfn expand(&mut self, path: &str, current_index: usize) {\n\t\tif let FileTreeItemKind::Path(PathCollapsed(\n\t\t\tref mut collapsed,\n\t\t)) = self.tree[current_index].kind\n\t\t{\n\t\t\t*collapsed = false;\n\t\t}\n\n\t\tlet path = format!(\"{path}/\");\n\n\t\tself.update_visibility(\n\t\t\tSome(path.as_str()),\n\t\t\tcurrent_index + 1,\n\t\t\tfalse,\n\t\t);\n\t}\n\n\tfn update_visibility(\n\t\t&mut self,\n\t\tprefix: Option<&str>,\n\t\tstart_idx: usize,\n\t\tset_defaults: bool,\n\t) {\n\t\t// if we are in any subpath that is collapsed we keep skipping over it\n\t\tlet mut inner_collapsed: Option<String> = None;\n\n\t\tfor i in start_idx..self.tree.len() {\n\t\t\tif let Some(ref collapsed_path) = inner_collapsed {\n\t\t\t\tlet p: &String = &self.tree[i].info.full_path;\n\t\t\t\tif p.starts_with(collapsed_path) {\n\t\t\t\t\tif set_defaults {\n\t\t\t\t\t\tself.tree[i].info.visible = false;\n\t\t\t\t\t}\n\t\t\t\t\t// we are still in a collapsed inner path\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tinner_collapsed = None;\n\t\t\t}\n\n\t\t\tlet item_kind = self.tree[i].kind.clone();\n\t\t\tlet item_path = &self.tree[i].info.full_path;\n\n\t\t\tif matches!(item_kind, FileTreeItemKind::Path(PathCollapsed(collapsed)) if collapsed)\n\t\t\t{\n\t\t\t\t// we encountered an inner path that is still collapsed\n\t\t\t\tinner_collapsed = Some(format!(\"{}/\", &item_path));\n\t\t\t}\n\n\t\t\tif prefix\n\t\t\t\t.is_none_or(|prefix| item_path.starts_with(prefix))\n\t\t\t{\n\t\t\t\tself.tree[i].info.visible = true;\n\t\t\t} else {\n\t\t\t\t// if we do not set defaults we can early out\n\t\t\t\tif set_defaults {\n\t\t\t\t\tself.tree[i].info.visible = false;\n\t\t\t\t} else {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse asyncgit::StatusItemType;\n\n\tfn string_vec_to_status(items: &[&str]) -> Vec<StatusItem> {\n\t\titems\n\t\t\t.iter()\n\t\t\t.map(|a| StatusItem {\n\t\t\t\tpath: String::from(*a),\n\t\t\t\tstatus: StatusItemType::Modified,\n\t\t\t})\n\t\t\t.collect::<Vec<_>>()\n\t}\n\n\tfn get_visible(tree: &StatusTree) -> Vec<bool> {\n\t\ttree.tree\n\t\t\t.items()\n\t\t\t.iter()\n\t\t\t.map(|e| e.info.visible)\n\t\t\t.collect::<Vec<_>>()\n\t}\n\n\t#[test]\n\tfn test_selection() {\n\t\tlet items = string_vec_to_status(&[\n\t\t\t\"a/b\", //\n\t\t]);\n\n\t\tlet mut res = StatusTree::default();\n\t\tres.update(&items).unwrap();\n\n\t\tassert!(res.move_selection(MoveSelection::Down));\n\n\t\tassert_eq!(res.selection, Some(1));\n\n\t\tassert!(res.move_selection(MoveSelection::Left));\n\n\t\tassert_eq!(res.selection, Some(0));\n\t}\n\n\t#[test]\n\tfn test_keep_selected_item() {\n\t\tlet mut res = StatusTree::default();\n\t\tres.update(&string_vec_to_status(&[\"b\"])).unwrap();\n\n\t\tassert_eq!(res.selection, Some(0));\n\n\t\tres.update(&string_vec_to_status(&[\"a\", \"b\"])).unwrap();\n\n\t\tassert_eq!(res.selection, Some(1));\n\t}\n\n\t#[test]\n\tfn test_keep_selected_index() {\n\t\tlet mut res = StatusTree::default();\n\t\tres.update(&string_vec_to_status(&[\"a\", \"b\"])).unwrap();\n\t\tres.selection = Some(1);\n\n\t\tres.update(&string_vec_to_status(&[\"d\", \"c\", \"a\"])).unwrap();\n\t\tassert_eq!(res.selection, Some(1));\n\t}\n\n\t#[test]\n\tfn test_keep_selected_index_if_not_collapsed() {\n\t\tlet mut res = StatusTree::default();\n\t\tres.update(&string_vec_to_status(&[\"a/b\", \"c\"])).unwrap();\n\n\t\tres.collapse(\"a/b\", 0);\n\n\t\tres.selection = Some(2);\n\n\t\tres.update(&string_vec_to_status(&[\"a/b\"])).unwrap();\n\t\tassert_eq!(\n\t\t\tget_visible(&res),\n\t\t\tvec![\n\t\t\t\ttrue,  //\n\t\t\t\tfalse, //\n\t\t\t]\n\t\t);\n\t\tassert!(res.is_visible_index(res.selection.unwrap()));\n\t\tassert_eq!(res.selection, Some(0));\n\t}\n\n\t#[test]\n\tfn test_keep_collapsed_states() {\n\t\tlet mut res = StatusTree::default();\n\t\tres.update(&string_vec_to_status(&[\n\t\t\t\"a/b\", //\n\t\t\t\"c\",\n\t\t]))\n\t\t.unwrap();\n\n\t\tres.collapse(\"a\", 0);\n\n\t\tassert_eq!(\n\t\t\tres.all_collapsed().iter().collect::<Vec<_>>(),\n\t\t\tvec![&&String::from(\"a\")]\n\t\t);\n\n\t\tassert_eq!(\n\t\t\tget_visible(&res),\n\t\t\tvec![\n\t\t\t\ttrue,  //\n\t\t\t\tfalse, //\n\t\t\t\ttrue,  //\n\t\t\t]\n\t\t);\n\n\t\tres.update(&string_vec_to_status(&[\n\t\t\t\"a/b\", //\n\t\t\t\"c\",   //\n\t\t\t\"d\",\n\t\t]))\n\t\t.unwrap();\n\n\t\tassert_eq!(\n\t\t\tres.all_collapsed().iter().collect::<Vec<_>>(),\n\t\t\tvec![&&String::from(\"a\")]\n\t\t);\n\n\t\tassert_eq!(\n\t\t\tget_visible(&res),\n\t\t\tvec![\n\t\t\t\ttrue,  //\n\t\t\t\tfalse, //\n\t\t\t\ttrue,  //\n\t\t\t\ttrue\n\t\t\t]\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_expand() {\n\t\tlet items = string_vec_to_status(&[\n\t\t\t\"a/b/c\", //\n\t\t\t\"a/d\",   //\n\t\t]);\n\n\t\t//0 a/\n\t\t//1   b/\n\t\t//2     c\n\t\t//3   d\n\n\t\tlet mut res = StatusTree::default();\n\t\tres.update(&items).unwrap();\n\n\t\tres.collapse(&String::from(\"a/b\"), 1);\n\n\t\tlet visible = get_visible(&res);\n\n\t\tassert_eq!(\n\t\t\tvisible,\n\t\t\tvec![\n\t\t\t\ttrue,  //\n\t\t\t\ttrue,  //\n\t\t\t\tfalse, //\n\t\t\t\ttrue,\n\t\t\t]\n\t\t);\n\n\t\tres.expand(&String::from(\"a/b\"), 1);\n\n\t\tlet visible = get_visible(&res);\n\n\t\tassert_eq!(\n\t\t\tvisible,\n\t\t\tvec![\n\t\t\t\ttrue, //\n\t\t\t\ttrue, //\n\t\t\t\ttrue, //\n\t\t\t\ttrue,\n\t\t\t]\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_expand_bug() {\n\t\tlet items = string_vec_to_status(&[\n\t\t\t\"a/b/c\",  //\n\t\t\t\"a/b2/d\", //\n\t\t]);\n\n\t\t//0 a/\n\t\t//1   b/\n\t\t//2     c\n\t\t//3   b2/\n\t\t//4     d\n\n\t\tlet mut res = StatusTree::default();\n\t\tres.update(&items).unwrap();\n\n\t\tres.collapse(&String::from(\"b\"), 1);\n\t\tres.collapse(&String::from(\"a\"), 0);\n\n\t\tassert_eq!(\n\t\t\tget_visible(&res),\n\t\t\tvec![\n\t\t\t\ttrue,  //\n\t\t\t\tfalse, //\n\t\t\t\tfalse, //\n\t\t\t\tfalse, //\n\t\t\t\tfalse,\n\t\t\t]\n\t\t);\n\n\t\tres.expand(&String::from(\"a\"), 0);\n\n\t\tassert_eq!(\n\t\t\tget_visible(&res),\n\t\t\tvec![\n\t\t\t\ttrue,  //\n\t\t\t\ttrue,  //\n\t\t\t\tfalse, //\n\t\t\t\ttrue,  //\n\t\t\t\ttrue,\n\t\t\t]\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_collapse_too_much() {\n\t\tlet items = string_vec_to_status(&[\n\t\t\t\"a/b\",  //\n\t\t\t\"a2/c\", //\n\t\t]);\n\n\t\t//0 a/\n\t\t//1   b\n\t\t//2 a2/\n\t\t//3   c\n\n\t\tlet mut res = StatusTree::default();\n\t\tres.update(&items).unwrap();\n\n\t\tres.collapse(&String::from(\"a\"), 0);\n\n\t\tlet visible = get_visible(&res);\n\n\t\tassert_eq!(\n\t\t\tvisible,\n\t\t\tvec![\n\t\t\t\ttrue,  //\n\t\t\t\tfalse, //\n\t\t\t\ttrue,  //\n\t\t\t\ttrue,\n\t\t\t]\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_expand_with_collapsed_sub_parts() {\n\t\tlet items = string_vec_to_status(&[\n\t\t\t\"a/b/c\", //\n\t\t\t\"a/d\",   //\n\t\t]);\n\n\t\t//0 a/\n\t\t//1   b/\n\t\t//2     c\n\t\t//3   d\n\n\t\tlet mut res = StatusTree::default();\n\t\tres.update(&items).unwrap();\n\n\t\tres.collapse(&String::from(\"a/b\"), 1);\n\n\t\tlet visible = get_visible(&res);\n\n\t\tassert_eq!(\n\t\t\tvisible,\n\t\t\tvec![\n\t\t\t\ttrue,  //\n\t\t\t\ttrue,  //\n\t\t\t\tfalse, //\n\t\t\t\ttrue,\n\t\t\t]\n\t\t);\n\n\t\tres.collapse(&String::from(\"a\"), 0);\n\n\t\tlet visible = get_visible(&res);\n\n\t\tassert_eq!(\n\t\t\tvisible,\n\t\t\tvec![\n\t\t\t\ttrue,  //\n\t\t\t\tfalse, //\n\t\t\t\tfalse, //\n\t\t\t\tfalse,\n\t\t\t]\n\t\t);\n\n\t\tres.expand(&String::from(\"a\"), 0);\n\n\t\tlet visible = get_visible(&res);\n\n\t\tassert_eq!(\n\t\t\tvisible,\n\t\t\tvec![\n\t\t\t\ttrue,  //\n\t\t\t\ttrue,  //\n\t\t\t\tfalse, //\n\t\t\t\ttrue,\n\t\t\t]\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_selection_skips_collapsed() {\n\t\tlet items = string_vec_to_status(&[\n\t\t\t\"a/b/c\", //\n\t\t\t\"a/d\",   //\n\t\t]);\n\n\t\t//0 a/\n\t\t//1   b/\n\t\t//2     c\n\t\t//3   d\n\n\t\tlet mut res = StatusTree::default();\n\t\tres.update(&items).unwrap();\n\t\tres.collapse(&String::from(\"a/b\"), 1);\n\t\tres.selection = Some(1);\n\n\t\tassert!(res.move_selection(MoveSelection::Down));\n\n\t\tassert_eq!(res.selection, Some(3));\n\t}\n\n\t#[test]\n\tfn test_folders_fold_up_if_alone_in_directory() {\n\t\tlet items = string_vec_to_status(&[\n\t\t\t\"a/b/c/d\", //\n\t\t\t\"a/e/f/g\", //\n\t\t\t\"a/h/i/j\", //\n\t\t]);\n\n\t\t//0 a/\n\t\t//1   b/\n\t\t//2     c/\n\t\t//3       d\n\t\t//4   e/\n\t\t//5     f/\n\t\t//6       g\n\t\t//7   h/\n\t\t//8     i/\n\t\t//9       j\n\n\t\t//0 a/\n\t\t//1   b/c/\n\t\t//3       d\n\t\t//4   e/f/\n\t\t//6       g\n\t\t//7   h/i/\n\t\t//9       j\n\n\t\tlet mut res = StatusTree::default();\n\t\tres.update(&items).unwrap();\n\t\tres.selection = Some(0);\n\n\t\tassert!(res.move_selection(MoveSelection::Down));\n\t\tassert_eq!(res.selection, Some(1));\n\n\t\tassert!(res.move_selection(MoveSelection::Down));\n\t\tassert_eq!(res.selection, Some(3));\n\n\t\tassert!(res.move_selection(MoveSelection::Down));\n\t\tassert_eq!(res.selection, Some(4));\n\n\t\tassert!(res.move_selection(MoveSelection::Down));\n\t\tassert_eq!(res.selection, Some(6));\n\n\t\tassert!(res.move_selection(MoveSelection::Down));\n\t\tassert_eq!(res.selection, Some(7));\n\n\t\tassert!(res.move_selection(MoveSelection::Down));\n\t\tassert_eq!(res.selection, Some(9));\n\t}\n\n\t#[test]\n\tfn test_folders_fold_up_if_alone_in_directory_2() {\n\t\tlet items = string_vec_to_status(&[\"a/b/c/d/e/f/g/h\"]);\n\n\t\t//0 a/\n\t\t//1   b/\n\t\t//2     c/\n\t\t//3       d/\n\t\t//4         e/\n\t\t//5           f/\n\t\t//6             g/\n\t\t//7               h\n\n\t\t//0 a/b/c/d/e/f/g/\n\t\t//7               h\n\n\t\tlet mut res = StatusTree::default();\n\t\tres.update(&items).unwrap();\n\t\tres.selection = Some(0);\n\n\t\tassert!(res.move_selection(MoveSelection::Down));\n\t\tassert_eq!(res.selection, Some(7));\n\t}\n\n\t#[test]\n\tfn test_folders_fold_up_down_with_selection_left_right() {\n\t\tlet items = string_vec_to_status(&[\n\t\t\t\"a/b/c/d\", //\n\t\t\t\"a/e/f/g\", //\n\t\t\t\"a/h/i/j\", //\n\t\t]);\n\n\t\t//0 a/\n\t\t//1   b/\n\t\t//2     c/\n\t\t//3       d\n\t\t//4   e/\n\t\t//5     f/\n\t\t//6       g\n\t\t//7   h/\n\t\t//8     i/\n\t\t//9       j\n\n\t\t//0 a/\n\t\t//1   b/c/\n\t\t//3       d\n\t\t//4   e/f/\n\t\t//6       g\n\t\t//7   h/i/\n\t\t//9       j\n\n\t\tlet mut res = StatusTree::default();\n\t\tres.update(&items).unwrap();\n\t\tres.selection = Some(0);\n\n\t\tassert!(res.move_selection(MoveSelection::Left));\n\t\tassert_eq!(res.selection, Some(0));\n\n\t\t// These should do nothing\n\t\tres.move_selection(MoveSelection::Left);\n\t\tres.move_selection(MoveSelection::Left);\n\t\tassert_eq!(res.selection, Some(0));\n\t\t//\n\t\tassert!(res.move_selection(MoveSelection::Right)); // unfold 0\n\t\tassert_eq!(res.selection, Some(0));\n\n\t\tassert!(res.move_selection(MoveSelection::Right)); // move to 1\n\t\tassert_eq!(res.selection, Some(1));\n\n\t\tassert!(res.move_selection(MoveSelection::Left)); // fold 1\n\t\tassert!(res.move_selection(MoveSelection::Down)); // move to 4\n\t\tassert_eq!(res.selection, Some(4));\n\n\t\tassert!(res.move_selection(MoveSelection::Left)); // fold 4\n\t\tassert!(res.move_selection(MoveSelection::Down)); // move to 7\n\t\tassert_eq!(res.selection, Some(7));\n\n\t\tassert!(res.move_selection(MoveSelection::Right)); // move to 9\n\t\tassert_eq!(res.selection, Some(9));\n\n\t\tassert!(res.move_selection(MoveSelection::Left)); // move to 7\n\t\tassert_eq!(res.selection, Some(7));\n\n\t\tassert!(res.move_selection(MoveSelection::Left)); // folds 7\n\t\tassert_eq!(res.selection, Some(7));\n\n\t\tassert!(res.move_selection(MoveSelection::Left)); // jump to 0\n\t\tassert_eq!(res.selection, Some(0));\n\t}\n}\n"
  },
  {
    "path": "src/input.rs",
    "content": "use crate::notify_mutex::NotifiableMutex;\nuse anyhow::Result;\nuse crossbeam_channel::{unbounded, Receiver, Sender};\nuse crossterm::event::{self, Event, Event::Key, KeyEventKind};\nuse std::{\n\tsync::{\n\t\tatomic::{AtomicBool, Ordering},\n\t\tArc,\n\t},\n\tthread,\n\ttime::Duration,\n};\n\nstatic FAST_POLL_DURATION: Duration = Duration::from_millis(100);\nstatic SLOW_POLL_DURATION: Duration = Duration::from_secs(10);\n\n///\n#[derive(Clone, Copy, Debug)]\npub enum InputState {\n\tPaused,\n\tPolling,\n}\n\n///\n#[derive(Clone, Debug)]\npub enum InputEvent {\n\tInput(Event),\n\tState(InputState),\n}\n\n///\n#[derive(Clone)]\npub struct Input {\n\tdesired_state: Arc<NotifiableMutex<bool>>,\n\tcurrent_state: Arc<AtomicBool>,\n\treceiver: Receiver<InputEvent>,\n\taborted: Arc<AtomicBool>,\n}\n\nimpl Input {\n\t///\n\tpub fn new() -> Self {\n\t\tlet (tx, rx) = unbounded();\n\n\t\tlet desired_state = Arc::new(NotifiableMutex::new(true));\n\t\tlet current_state = Arc::new(AtomicBool::new(true));\n\t\tlet aborted = Arc::new(AtomicBool::new(false));\n\n\t\tlet arc_desired = Arc::clone(&desired_state);\n\t\tlet arc_current = Arc::clone(&current_state);\n\t\tlet arc_aborted = Arc::clone(&aborted);\n\n\t\tthread::spawn(move || {\n\t\t\tif let Err(e) =\n\t\t\t\tSelf::input_loop(&arc_desired, &arc_current, &tx)\n\t\t\t{\n\t\t\t\tlog::error!(\"input thread error: {e}\");\n\t\t\t\tarc_aborted.store(true, Ordering::SeqCst);\n\t\t\t}\n\t\t});\n\n\t\tSelf {\n\t\t\treceiver: rx,\n\t\t\tdesired_state,\n\t\t\tcurrent_state,\n\t\t\taborted,\n\t\t}\n\t}\n\n\t///\n\tpub fn receiver(&self) -> Receiver<InputEvent> {\n\t\tself.receiver.clone()\n\t}\n\n\t///\n\tpub fn set_polling(&self, enabled: bool) {\n\t\tself.desired_state.set_and_notify(enabled);\n\t}\n\n\tfn shall_poll(&self) -> bool {\n\t\tself.desired_state.get()\n\t}\n\n\t///\n\tpub fn is_state_changing(&self) -> bool {\n\t\tself.shall_poll()\n\t\t\t!= self.current_state.load(Ordering::Relaxed)\n\t}\n\n\tpub fn is_aborted(&self) -> bool {\n\t\tself.aborted.load(Ordering::SeqCst)\n\t}\n\n\tfn poll(dur: Duration) -> anyhow::Result<Option<Event>> {\n\t\tif event::poll(dur)? {\n\t\t\tOk(Some(event::read()?))\n\t\t} else {\n\t\t\tOk(None)\n\t\t}\n\t}\n\n\tfn input_loop(\n\t\tarc_desired: &Arc<NotifiableMutex<bool>>,\n\t\tarc_current: &Arc<AtomicBool>,\n\t\ttx: &Sender<InputEvent>,\n\t) -> Result<()> {\n\t\tlet mut poll_duration = SLOW_POLL_DURATION;\n\t\tloop {\n\t\t\tif arc_desired.get() {\n\t\t\t\tif !arc_current.load(Ordering::Relaxed) {\n\t\t\t\t\tlog::info!(\"input polling resumed\");\n\n\t\t\t\t\ttx.send(InputEvent::State(InputState::Polling))?;\n\t\t\t\t}\n\t\t\t\tarc_current.store(true, Ordering::Relaxed);\n\n\t\t\t\tif let Some(e) = Self::poll(poll_duration)? {\n\t\t\t\t\t// windows send key release too, only process key press\n\t\t\t\t\tif let Key(key) = e {\n\t\t\t\t\t\tif key.kind != KeyEventKind::Press {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\ttx.send(InputEvent::Input(e))?;\n\t\t\t\t\t//Note: right after an input event we might have a reason to stop\n\t\t\t\t\t// polling (external editor opening) so lets do a quick poll until the next input\n\t\t\t\t\t// this fixes https://github.com/gitui-org/gitui/issues/1506\n\t\t\t\t\tpoll_duration = FAST_POLL_DURATION;\n\t\t\t\t} else {\n\t\t\t\t\tpoll_duration = SLOW_POLL_DURATION;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif arc_current.load(Ordering::Relaxed) {\n\t\t\t\t\tlog::info!(\"input polling suspended\");\n\n\t\t\t\t\ttx.send(InputEvent::State(InputState::Paused))?;\n\t\t\t\t}\n\n\t\t\t\tarc_current.store(false, Ordering::Relaxed);\n\n\t\t\t\tarc_desired.wait(true);\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/keys/key_config.rs",
    "content": "use anyhow::Result;\nuse crossterm::event::{KeyCode, KeyModifiers};\nuse std::{fs::canonicalize, path::PathBuf, rc::Rc};\n\nuse crate::{args::get_app_config_path, strings::symbol};\n\nuse super::{\n\tkey_list::{GituiKeyEvent, KeysList},\n\tsymbols::KeySymbols,\n};\n\npub type SharedKeyConfig = Rc<KeyConfig>;\nconst KEY_LIST_FILENAME: &str = \"key_bindings.ron\";\nconst KEY_SYMBOLS_FILENAME: &str = \"key_symbols.ron\";\n\n#[derive(Default, Clone)]\npub struct KeyConfig {\n\tpub keys: KeysList,\n\tsymbols: KeySymbols,\n}\n\nimpl KeyConfig {\n\tfn get_config_file() -> Result<PathBuf> {\n\t\tlet app_home = get_app_config_path()?;\n\t\tlet config_file = app_home.join(KEY_LIST_FILENAME);\n\t\tcanonicalize(&config_file)\n\t\t\t.map_or_else(|_| Ok(config_file), Ok)\n\t}\n\n\tfn get_symbols_file() -> Result<PathBuf> {\n\t\tlet app_home = get_app_config_path()?;\n\t\tlet symbols_file = app_home.join(KEY_SYMBOLS_FILENAME);\n\t\tcanonicalize(&symbols_file)\n\t\t\t.map_or_else(|_| Ok(symbols_file), Ok)\n\t}\n\n\tpub fn init(\n\t\tkey_bindings_path: Option<&PathBuf>,\n\t\tkey_symbols_path: Option<&PathBuf>,\n\t) -> Result<Self> {\n\t\tlet keys = KeysList::init(\n\t\t\tkey_bindings_path\n\t\t\t\t.unwrap_or(&Self::get_config_file()?)\n\t\t\t\t.clone(),\n\t\t);\n\t\tlet symbols = KeySymbols::init(\n\t\t\tkey_symbols_path\n\t\t\t\t.unwrap_or(&Self::get_symbols_file()?)\n\t\t\t\t.clone(),\n\t\t);\n\n\t\tOk(Self { keys, symbols })\n\t}\n\n\tfn get_key_symbol(&self, k: KeyCode) -> &str {\n\t\tmatch k {\n\t\t\tKeyCode::Enter => &self.symbols.enter,\n\t\t\tKeyCode::Left => &self.symbols.left,\n\t\t\tKeyCode::Right => &self.symbols.right,\n\t\t\tKeyCode::Up => &self.symbols.up,\n\t\t\tKeyCode::Down => &self.symbols.down,\n\t\t\tKeyCode::Backspace => &self.symbols.backspace,\n\t\t\tKeyCode::Home => &self.symbols.home,\n\t\t\tKeyCode::End => &self.symbols.end,\n\t\t\tKeyCode::PageUp => &self.symbols.page_up,\n\t\t\tKeyCode::PageDown => &self.symbols.page_down,\n\t\t\tKeyCode::Tab => &self.symbols.tab,\n\t\t\tKeyCode::BackTab => &self.symbols.back_tab,\n\t\t\tKeyCode::Delete => &self.symbols.delete,\n\t\t\tKeyCode::Insert => &self.symbols.insert,\n\t\t\tKeyCode::Esc => &self.symbols.esc,\n\t\t\t_ => \"?\",\n\t\t}\n\t}\n\n\tpub fn get_hint(&self, ev: GituiKeyEvent) -> String {\n\t\tmatch ev.code {\n\t\t\tKeyCode::Down\n\t\t\t| KeyCode::Up\n\t\t\t| KeyCode::Right\n\t\t\t| KeyCode::Left\n\t\t\t| KeyCode::Enter\n\t\t\t| KeyCode::Backspace\n\t\t\t| KeyCode::Home\n\t\t\t| KeyCode::End\n\t\t\t| KeyCode::PageUp\n\t\t\t| KeyCode::PageDown\n\t\t\t| KeyCode::Tab\n\t\t\t| KeyCode::BackTab\n\t\t\t| KeyCode::Delete\n\t\t\t| KeyCode::Insert\n\t\t\t| KeyCode::Esc => {\n\t\t\t\tformat!(\n\t\t\t\t\t\"{}{}\",\n\t\t\t\t\tself.get_modifier_hint(ev.modifiers),\n\t\t\t\t\tself.get_key_symbol(ev.code)\n\t\t\t\t)\n\t\t\t}\n\t\t\tKeyCode::Char(' ') => String::from(symbol::SPACE),\n\t\t\tKeyCode::Char(c) => {\n\t\t\t\tformat!(\n\t\t\t\t\t\"{}{}\",\n\t\t\t\t\tself.get_modifier_hint(ev.modifiers),\n\t\t\t\t\tc\n\t\t\t\t)\n\t\t\t}\n\t\t\tKeyCode::F(u) => {\n\t\t\t\tformat!(\n\t\t\t\t\t\"{}F{}\",\n\t\t\t\t\tself.get_modifier_hint(ev.modifiers),\n\t\t\t\t\tu\n\t\t\t\t)\n\t\t\t}\n\t\t\tKeyCode::Null => {\n\t\t\t\tself.get_modifier_hint(ev.modifiers).into()\n\t\t\t}\n\t\t\t_ => String::new(),\n\t\t}\n\t}\n\n\tfn get_modifier_hint(&self, modifier: KeyModifiers) -> &str {\n\t\tmatch modifier {\n\t\t\tKeyModifiers::CONTROL => &self.symbols.control,\n\t\t\tKeyModifiers::SHIFT => &self.symbols.shift,\n\t\t\tKeyModifiers::ALT => &self.symbols.alt,\n\t\t\t_ => \"\",\n\t\t}\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse std::fs;\n\tuse std::io::Write;\n\tuse tempfile::NamedTempFile;\n\n\t#[test]\n\tfn test_get_hint() {\n\t\tlet config = KeyConfig::default();\n\t\tlet h = config.get_hint(GituiKeyEvent::new(\n\t\t\tKeyCode::Char('c'),\n\t\t\tKeyModifiers::CONTROL,\n\t\t));\n\t\tassert_eq!(h, \"^c\");\n\t}\n\n\t#[test]\n\tfn test_symbolic_links() {\n\t\tlet app_home = get_app_config_path().unwrap();\n\t\tfs::create_dir_all(&app_home).unwrap();\n\t\t// save current config\n\t\tlet original_key_list_path = app_home.join(KEY_LIST_FILENAME);\n\t\tlet renamed_key_list = if original_key_list_path.exists() {\n\t\t\tlet temp = NamedTempFile::new_in(&app_home).unwrap();\n\t\t\tfs::rename(&original_key_list_path, &temp).unwrap();\n\t\t\tSome(temp)\n\t\t} else {\n\t\t\tNone\n\t\t};\n\t\tlet original_key_symbols_path =\n\t\t\tapp_home.join(KEY_SYMBOLS_FILENAME);\n\t\tlet renamed_key_symbols = if original_key_symbols_path\n\t\t\t.exists()\n\t\t{\n\t\t\tlet temp = NamedTempFile::new_in(&app_home).unwrap();\n\t\t\tfs::rename(&original_key_symbols_path, &temp).unwrap();\n\t\t\tSome(temp)\n\t\t} else {\n\t\t\tNone\n\t\t};\n\n\t\t// create temporary config files\n\t\tlet mut temporary_key_list =\n\t\t\tNamedTempFile::new_in(&app_home).unwrap();\n\t\twriteln!(\n\t\t\ttemporary_key_list,\n\t\t\tr#\"\n(\n\tmove_down: Some(( code: Char('j'), modifiers: \"CONTROL\")),\n)\n\"#\n\t\t)\n\t\t.unwrap();\n\n\t\tlet mut temporary_key_symbols =\n\t\t\tNamedTempFile::new_in(&app_home).unwrap();\n\t\twriteln!(\n\t\t\ttemporary_key_symbols,\n\t\t\tr#\"\n(\n\tesc: Some(\"Esc\"),\n)\n\"#\n\t\t)\n\t\t.unwrap();\n\n\t\t// testing\n\t\tlet result = std::panic::catch_unwind(|| {\n\t\t\tlet loaded_config = KeyConfig::init(None, None).unwrap();\n\t\t\tassert_eq!(\n\t\t\t\tloaded_config.keys.move_down,\n\t\t\t\tKeysList::default().move_down\n\t\t\t);\n\t\t\tassert_eq!(\n\t\t\t\tloaded_config.symbols.esc,\n\t\t\t\tKeySymbols::default().esc\n\t\t\t);\n\n\t\t\tcreate_symlink(\n\t\t\t\t&temporary_key_symbols,\n\t\t\t\t&original_key_symbols_path,\n\t\t\t)\n\t\t\t.unwrap();\n\t\t\tlet loaded_config = KeyConfig::init(None, None).unwrap();\n\t\t\tassert_eq!(\n\t\t\t\tloaded_config.keys.move_down,\n\t\t\t\tKeysList::default().move_down\n\t\t\t);\n\t\t\tassert_eq!(loaded_config.symbols.esc, \"Esc\");\n\n\t\t\tcreate_symlink(\n\t\t\t\t&temporary_key_list,\n\t\t\t\t&original_key_list_path,\n\t\t\t)\n\t\t\t.unwrap();\n\t\t\tlet loaded_config = KeyConfig::init(None, None).unwrap();\n\t\t\tassert_eq!(\n\t\t\t\tloaded_config.keys.move_down,\n\t\t\t\tGituiKeyEvent::new(\n\t\t\t\t\tKeyCode::Char('j'),\n\t\t\t\t\tKeyModifiers::CONTROL\n\t\t\t\t)\n\t\t\t);\n\t\t\tassert_eq!(loaded_config.symbols.esc, \"Esc\");\n\n\t\t\tfs::remove_file(&original_key_symbols_path).unwrap();\n\t\t\tlet loaded_config = KeyConfig::init(None, None).unwrap();\n\t\t\tassert_eq!(\n\t\t\t\tloaded_config.keys.move_down,\n\t\t\t\tGituiKeyEvent::new(\n\t\t\t\t\tKeyCode::Char('j'),\n\t\t\t\t\tKeyModifiers::CONTROL\n\t\t\t\t)\n\t\t\t);\n\t\t\tassert_eq!(\n\t\t\t\tloaded_config.symbols.esc,\n\t\t\t\tKeySymbols::default().esc\n\t\t\t);\n\n\t\t\tfs::remove_file(&original_key_list_path).unwrap();\n\t\t});\n\n\t\t// remove symlinks from testing if they still exist\n\t\tlet _ = fs::remove_file(&original_key_list_path);\n\t\tlet _ = fs::remove_file(&original_key_symbols_path);\n\n\t\t// restore original config files\n\t\tif let Some(temp) = renamed_key_list {\n\t\t\tlet _ = fs::rename(&temp, &original_key_list_path);\n\t\t}\n\n\t\tif let Some(temp) = renamed_key_symbols {\n\t\t\tlet _ = fs::rename(&temp, &original_key_symbols_path);\n\t\t}\n\n\t\tassert!(result.is_ok());\n\t}\n\n\t#[cfg(not(target_os = \"windows\"))]\n\tfn create_symlink<\n\t\tP: AsRef<std::path::Path>,\n\t\tQ: AsRef<std::path::Path>,\n\t>(\n\t\toriginal: P,\n\t\tlink: Q,\n\t) -> Result<(), std::io::Error> {\n\t\tstd::os::unix::fs::symlink(original, link)\n\t}\n\n\t#[cfg(target_os = \"windows\")]\n\tfn create_symlink<\n\t\tP: AsRef<std::path::Path>,\n\t\tQ: AsRef<std::path::Path>,\n\t>(\n\t\toriginal: P,\n\t\tlink: Q,\n\t) -> Result<(), std::io::Error> {\n\t\tstd::os::windows::fs::symlink_file(original, link)\n\t}\n}\n"
  },
  {
    "path": "src/keys/key_list.rs",
    "content": "use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};\nuse serde::{Deserialize, Serialize};\nuse std::{fs::File, path::PathBuf};\nuse struct_patch::traits::Patch as PatchTrait;\nuse struct_patch::Patch;\n\n#[derive(Debug, PartialOrd, Clone, Copy, Serialize, Deserialize)]\npub struct GituiKeyEvent {\n\tpub code: KeyCode,\n\tpub modifiers: KeyModifiers,\n}\n\nimpl GituiKeyEvent {\n\tpub const fn new(code: KeyCode, modifiers: KeyModifiers) -> Self {\n\t\tSelf { code, modifiers }\n\t}\n}\n\npub fn key_match(ev: &KeyEvent, binding: GituiKeyEvent) -> bool {\n\tev.code == binding.code && ev.modifiers == binding.modifiers\n}\n\nimpl PartialEq for GituiKeyEvent {\n\tfn eq(&self, other: &Self) -> bool {\n\t\tlet ev: KeyEvent = self.into();\n\t\tlet other: KeyEvent = other.into();\n\t\tev == other\n\t}\n}\n\nimpl From<&GituiKeyEvent> for KeyEvent {\n\tfn from(other: &GituiKeyEvent) -> Self {\n\t\tSelf::new(other.code, other.modifiers)\n\t}\n}\n\n#[derive(Debug, Clone, Patch)]\n#[patch(attribute(derive(Deserialize, Debug)))]\npub struct KeysList {\n\tpub tab_status: GituiKeyEvent,\n\tpub tab_log: GituiKeyEvent,\n\tpub tab_files: GituiKeyEvent,\n\tpub tab_stashing: GituiKeyEvent,\n\tpub tab_stashes: GituiKeyEvent,\n\tpub tab_toggle: GituiKeyEvent,\n\tpub tab_toggle_reverse: GituiKeyEvent,\n\tpub toggle_workarea: GituiKeyEvent,\n\tpub exit: GituiKeyEvent,\n\tpub quit: GituiKeyEvent,\n\tpub exit_popup: GituiKeyEvent,\n\tpub open_commit: GituiKeyEvent,\n\tpub open_commit_editor: GituiKeyEvent,\n\tpub open_help: GituiKeyEvent,\n\tpub open_options: GituiKeyEvent,\n\tpub move_left: GituiKeyEvent,\n\tpub move_right: GituiKeyEvent,\n\tpub move_up: GituiKeyEvent,\n\tpub move_down: GituiKeyEvent,\n\tpub tree_collapse_recursive: GituiKeyEvent,\n\tpub tree_expand_recursive: GituiKeyEvent,\n\tpub home: GituiKeyEvent,\n\tpub end: GituiKeyEvent,\n\tpub popup_up: GituiKeyEvent,\n\tpub popup_down: GituiKeyEvent,\n\tpub page_down: GituiKeyEvent,\n\tpub page_up: GituiKeyEvent,\n\tpub shift_up: GituiKeyEvent,\n\tpub shift_down: GituiKeyEvent,\n\tpub enter: GituiKeyEvent,\n\tpub blame: GituiKeyEvent,\n\tpub file_history: GituiKeyEvent,\n\tpub edit_file: GituiKeyEvent,\n\tpub status_stage_all: GituiKeyEvent,\n\tpub status_reset_item: GituiKeyEvent,\n\tpub status_ignore_file: GituiKeyEvent,\n\tpub diff_stage_lines: GituiKeyEvent,\n\tpub diff_reset_lines: GituiKeyEvent,\n\tpub stashing_save: GituiKeyEvent,\n\tpub stashing_toggle_untracked: GituiKeyEvent,\n\tpub stashing_toggle_index: GituiKeyEvent,\n\tpub stash_apply: GituiKeyEvent,\n\tpub stash_open: GituiKeyEvent,\n\tpub stash_drop: GituiKeyEvent,\n\tpub cmd_bar_toggle: GituiKeyEvent,\n\tpub log_tag_commit: GituiKeyEvent,\n\tpub log_mark_commit: GituiKeyEvent,\n\tpub log_checkout_commit: GituiKeyEvent,\n\tpub log_reset_commit: GituiKeyEvent,\n\tpub log_reword_commit: GituiKeyEvent,\n\tpub log_find: GituiKeyEvent,\n\tpub find_commit_sha: GituiKeyEvent,\n\tpub commit_amend: GituiKeyEvent,\n\tpub toggle_signoff: GituiKeyEvent,\n\tpub toggle_verify: GituiKeyEvent,\n\tpub copy: GituiKeyEvent,\n\tpub create_branch: GituiKeyEvent,\n\tpub rename_branch: GituiKeyEvent,\n\tpub select_branch: GituiKeyEvent,\n\tpub delete_branch: GituiKeyEvent,\n\tpub merge_branch: GituiKeyEvent,\n\tpub rebase_branch: GituiKeyEvent,\n\tpub reset_branch: GituiKeyEvent,\n\tpub compare_commits: GituiKeyEvent,\n\tpub tags: GituiKeyEvent,\n\tpub delete_tag: GituiKeyEvent,\n\tpub select_tag: GituiKeyEvent,\n\tpub push: GituiKeyEvent,\n\tpub open_file_tree: GituiKeyEvent,\n\tpub file_find: GituiKeyEvent,\n\tpub branch_find: GituiKeyEvent,\n\tpub force_push: GituiKeyEvent,\n\tpub fetch: GituiKeyEvent,\n\tpub pull: GituiKeyEvent,\n\tpub abort_merge: GituiKeyEvent,\n\tpub undo_commit: GituiKeyEvent,\n\tpub diff_hunk_next: GituiKeyEvent,\n\tpub diff_hunk_prev: GituiKeyEvent,\n\tpub stage_unstage_item: GituiKeyEvent,\n\tpub tag_annotate: GituiKeyEvent,\n\tpub view_submodules: GituiKeyEvent,\n\tpub view_remotes: GituiKeyEvent,\n\tpub update_remote_name: GituiKeyEvent,\n\tpub update_remote_url: GituiKeyEvent,\n\tpub add_remote: GituiKeyEvent,\n\tpub delete_remote: GituiKeyEvent,\n\tpub view_submodule_parent: GituiKeyEvent,\n\tpub update_submodule: GituiKeyEvent,\n\tpub commit_history_next: GituiKeyEvent,\n\tpub commit: GituiKeyEvent,\n\tpub newline: GituiKeyEvent,\n\tpub goto_line: GituiKeyEvent,\n}\n\n#[rustfmt::skip]\nimpl Default for KeysList {\n\tfn default() -> Self {\n\t\tSelf {\n\t\t\ttab_status: GituiKeyEvent::new(KeyCode::Char('1'), KeyModifiers::empty()),\n\t\t\ttab_log: GituiKeyEvent::new(KeyCode::Char('2'),  KeyModifiers::empty()),\n\t\t\ttab_files: GituiKeyEvent::new(KeyCode::Char('3'),  KeyModifiers::empty()),\n\t\t\ttab_stashing: GituiKeyEvent::new(KeyCode::Char('4'),  KeyModifiers::empty()),\n\t\t\ttab_stashes: GituiKeyEvent::new(KeyCode::Char('5'),  KeyModifiers::empty()),\n\t\t\ttab_toggle: GituiKeyEvent::new(KeyCode::Tab,  KeyModifiers::empty()),\n\t\t\ttab_toggle_reverse: GituiKeyEvent::new(KeyCode::BackTab,  KeyModifiers::SHIFT),\n\t\t\ttoggle_workarea: GituiKeyEvent::new(KeyCode::Char('w'),  KeyModifiers::empty()),\n\t\t\texit: GituiKeyEvent::new(KeyCode::Char('c'),  KeyModifiers::CONTROL),\n\t\t\tquit: GituiKeyEvent::new(KeyCode::Char('q'),  KeyModifiers::empty()),\n\t\t\texit_popup: GituiKeyEvent::new(KeyCode::Esc,  KeyModifiers::empty()),\n\t\t\topen_commit: GituiKeyEvent::new(KeyCode::Char('c'),  KeyModifiers::empty()),\n\t\t\topen_commit_editor: GituiKeyEvent::new(KeyCode::Char('e'), KeyModifiers::CONTROL),\n\t\t\topen_help: GituiKeyEvent::new(KeyCode::Char('h'),  KeyModifiers::empty()),\n\t\t\topen_options: GituiKeyEvent::new(KeyCode::Char('o'),  KeyModifiers::empty()),\n\t\t\tmove_left: GituiKeyEvent::new(KeyCode::Left,  KeyModifiers::empty()),\n\t\t\tmove_right: GituiKeyEvent::new(KeyCode::Right,  KeyModifiers::empty()),\n\t\t\ttree_collapse_recursive: GituiKeyEvent::new(KeyCode::Left,  KeyModifiers::SHIFT),\n\t\t\ttree_expand_recursive: GituiKeyEvent::new(KeyCode::Right,  KeyModifiers::SHIFT),\n\t\t\thome: GituiKeyEvent::new(KeyCode::Home,  KeyModifiers::empty()),\n\t\t\tend: GituiKeyEvent::new(KeyCode::End,  KeyModifiers::empty()),\n\t\t\tmove_up: GituiKeyEvent::new(KeyCode::Up,  KeyModifiers::empty()),\n\t\t\tmove_down: GituiKeyEvent::new(KeyCode::Down,  KeyModifiers::empty()),\n\t\t\tpopup_up: GituiKeyEvent::new(KeyCode::Up,  KeyModifiers::empty()),\n\t\t\tpopup_down: GituiKeyEvent::new(KeyCode::Down,  KeyModifiers::empty()),\n\t\t\tpage_down: GituiKeyEvent::new(KeyCode::PageDown,  KeyModifiers::empty()),\n\t\t\tpage_up: GituiKeyEvent::new(KeyCode::PageUp,  KeyModifiers::empty()),\n\t\t\tshift_up: GituiKeyEvent::new(KeyCode::Up,  KeyModifiers::SHIFT),\n\t\t\tshift_down: GituiKeyEvent::new(KeyCode::Down,  KeyModifiers::SHIFT),\n\t\t\tenter: GituiKeyEvent::new(KeyCode::Enter,  KeyModifiers::empty()),\n\t\t\tblame: GituiKeyEvent::new(KeyCode::Char('B'),  KeyModifiers::SHIFT),\n\t\t\tfile_history: GituiKeyEvent::new(KeyCode::Char('H'),  KeyModifiers::SHIFT),\n\t\t\tedit_file: GituiKeyEvent::new(KeyCode::Char('e'),  KeyModifiers::empty()),\n\t\t\tstatus_stage_all: GituiKeyEvent::new(KeyCode::Char('a'),  KeyModifiers::empty()),\n\t\t\tstatus_reset_item: GituiKeyEvent::new(KeyCode::Char('D'),  KeyModifiers::SHIFT),\n\t\t\tdiff_reset_lines: GituiKeyEvent::new(KeyCode::Char('d'),  KeyModifiers::empty()),\n\t\t\tstatus_ignore_file: GituiKeyEvent::new(KeyCode::Char('i'),  KeyModifiers::empty()),\n\t\t\tdiff_stage_lines: GituiKeyEvent::new(KeyCode::Char('s'),  KeyModifiers::empty()),\n\t\t\tstashing_save: GituiKeyEvent::new(KeyCode::Char('s'),  KeyModifiers::empty()),\n\t\t\tstashing_toggle_untracked: GituiKeyEvent::new(KeyCode::Char('u'),  KeyModifiers::empty()),\n\t\t\tstashing_toggle_index: GituiKeyEvent::new(KeyCode::Char('i'),  KeyModifiers::empty()),\n\t\t\tstash_apply: GituiKeyEvent::new(KeyCode::Char('a'),  KeyModifiers::empty()),\n\t\t\tstash_open: GituiKeyEvent::new(KeyCode::Right,  KeyModifiers::empty()),\n\t\t\tstash_drop: GituiKeyEvent::new(KeyCode::Char('D'),  KeyModifiers::SHIFT),\n\t\t\tcmd_bar_toggle: GituiKeyEvent::new(KeyCode::Char('.'),  KeyModifiers::empty()),\n\t\t\tlog_tag_commit: GituiKeyEvent::new(KeyCode::Char('t'),  KeyModifiers::empty()),\n\t\t\tlog_mark_commit: GituiKeyEvent::new(KeyCode::Char(' '),  KeyModifiers::empty()),\n\t\t\tlog_checkout_commit: GituiKeyEvent { code: KeyCode::Char('S'), modifiers: KeyModifiers::SHIFT },\n\t\t\tlog_reset_commit: GituiKeyEvent { code: KeyCode::Char('R'), modifiers: KeyModifiers::SHIFT },\n\t\t\tlog_reword_commit: GituiKeyEvent { code: KeyCode::Char('r'), modifiers: KeyModifiers::empty() },\n\t\t\tlog_find: GituiKeyEvent { code: KeyCode::Char('f'), modifiers: KeyModifiers::empty() },\n\t\t\tfind_commit_sha: GituiKeyEvent::new(KeyCode::Char('j'), KeyModifiers::CONTROL),\n\t\t\tcommit_amend: GituiKeyEvent::new(KeyCode::Char('a'),  KeyModifiers::CONTROL),\n\t\t\ttoggle_signoff: GituiKeyEvent::new(KeyCode::Char('s'),  KeyModifiers::CONTROL),\n\t\t\ttoggle_verify: GituiKeyEvent::new(KeyCode::Char('f'),  KeyModifiers::CONTROL),\n\t\t\tcopy: GituiKeyEvent::new(KeyCode::Char('y'),  KeyModifiers::empty()),\n\t\t\tcreate_branch: GituiKeyEvent::new(KeyCode::Char('c'),  KeyModifiers::empty()),\n\t\t\trename_branch: GituiKeyEvent::new(KeyCode::Char('r'),  KeyModifiers::empty()),\n\t\t\tselect_branch: GituiKeyEvent::new(KeyCode::Char('b'),  KeyModifiers::empty()),\n\t\t\tdelete_branch: GituiKeyEvent::new(KeyCode::Char('D'),  KeyModifiers::SHIFT),\n\t\t\tmerge_branch: GituiKeyEvent::new(KeyCode::Char('m'),  KeyModifiers::empty()),\n\t\t\trebase_branch: GituiKeyEvent::new(KeyCode::Char('R'),  KeyModifiers::SHIFT),\n\t\t\treset_branch: GituiKeyEvent::new(KeyCode::Char('s'),  KeyModifiers::empty()),\n\t\t\tcompare_commits: GituiKeyEvent::new(KeyCode::Char('C'),  KeyModifiers::SHIFT),\n\t\t\ttags: GituiKeyEvent::new(KeyCode::Char('T'),  KeyModifiers::SHIFT),\n\t\t\tdelete_tag: GituiKeyEvent::new(KeyCode::Char('D'),  KeyModifiers::SHIFT),\n\t\t\tselect_tag: GituiKeyEvent::new(KeyCode::Enter,  KeyModifiers::empty()),\n\t\t\tpush: GituiKeyEvent::new(KeyCode::Char('p'),  KeyModifiers::empty()),\n\t\t\tforce_push: GituiKeyEvent::new(KeyCode::Char('P'),  KeyModifiers::SHIFT),\n\t\t\tundo_commit: GituiKeyEvent::new(KeyCode::Char('U'),  KeyModifiers::SHIFT),\n\t\t\tfetch: GituiKeyEvent::new(KeyCode::Char('F'),  KeyModifiers::SHIFT),\n\t\t\tpull: GituiKeyEvent::new(KeyCode::Char('f'),  KeyModifiers::empty()),\n\t\t\tabort_merge: GituiKeyEvent::new(KeyCode::Char('A'),  KeyModifiers::SHIFT),\n\t\t\topen_file_tree: GituiKeyEvent::new(KeyCode::Char('F'),  KeyModifiers::SHIFT),\n\t\t\tfile_find: GituiKeyEvent::new(KeyCode::Char('f'),  KeyModifiers::empty()),\n\t\t\tbranch_find: GituiKeyEvent::new(KeyCode::Char('f'),  KeyModifiers::empty()),\n\t\t\tdiff_hunk_next: GituiKeyEvent::new(KeyCode::Char('n'),  KeyModifiers::empty()),\n\t\t\tdiff_hunk_prev: GituiKeyEvent::new(KeyCode::Char('p'),  KeyModifiers::empty()),\n\t\t\tstage_unstage_item: GituiKeyEvent::new(KeyCode::Enter,  KeyModifiers::empty()),\n\t\t\ttag_annotate: GituiKeyEvent::new(KeyCode::Char('a'),  KeyModifiers::CONTROL),\n\t\t\tview_submodules: GituiKeyEvent::new(KeyCode::Char('S'),  KeyModifiers::SHIFT),\n\t\t\tview_remotes: GituiKeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL),\n\t\t\tupdate_remote_name: GituiKeyEvent::new(KeyCode::Char('n'),KeyModifiers::NONE),\n\t\t\tupdate_remote_url: GituiKeyEvent::new(KeyCode::Char('u'),KeyModifiers::NONE),\n\t\t\tadd_remote: GituiKeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE),\n\t\t\tdelete_remote: GituiKeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE),\n\t\t\tview_submodule_parent: GituiKeyEvent::new(KeyCode::Char('p'),  KeyModifiers::empty()),\n\t\t\tupdate_submodule: GituiKeyEvent::new(KeyCode::Char('u'),  KeyModifiers::empty()),\n\t\t\tcommit_history_next: GituiKeyEvent::new(KeyCode::Char('n'),  KeyModifiers::CONTROL),\n\t\t\tcommit: GituiKeyEvent::new(KeyCode::Char('d'),  KeyModifiers::CONTROL),\n\t\t\tnewline: GituiKeyEvent::new(KeyCode::Enter,  KeyModifiers::empty()),\n\t\t\tgoto_line: GituiKeyEvent::new(KeyCode::Char('L'),  KeyModifiers::SHIFT),\n\t\t}\n\t}\n}\n\nimpl KeysList {\n\tpub fn init(file: PathBuf) -> Self {\n\t\tlet mut keys_list = Self::default();\n\t\tif let Ok(f) = File::open(file) {\n\t\t\tmatch ron::de::from_reader(f) {\n\t\t\t\tOk(patch) => keys_list.apply(patch),\n\t\t\t\tErr(e) => {\n\t\t\t\t\tlog::error!(\"KeysList parse error: {e}\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tkeys_list\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse pretty_assertions::assert_eq;\n\tuse std::io::Write;\n\tuse tempfile::NamedTempFile;\n\n\t#[test]\n\tfn test_apply_vim_style_example() {\n\t\tlet mut keys_list = KeysList::default();\n\t\tlet f = File::open(\"vim_style_key_config.ron\")\n\t\t\t.expect(\"vim style config should exist\");\n\t\tlet patch = ron::de::from_reader(f)\n\t\t\t.expect(\"vim style config format incorrect\");\n\t\tkeys_list.apply(patch);\n\t}\n\n\t#[test]\n\tfn test_smoke() {\n\t\tlet mut file = NamedTempFile::new().unwrap();\n\n\t\twriteln!(\n\t\t\tfile,\n\t\t\tr#\"\n(\n\tmove_down: Some(( code: Char('j'), modifiers: \"CONTROL\")),\n\tmove_up: Some((code: Char('h'), modifiers: \"\"))\n)\n\"#\n\t\t)\n\t\t.unwrap();\n\n\t\tlet keys = KeysList::init(file.path().to_path_buf());\n\n\t\tassert_eq!(keys.move_right, KeysList::default().move_right);\n\t\tassert_eq!(\n\t\t\tkeys.move_down,\n\t\t\tGituiKeyEvent::new(\n\t\t\t\tKeyCode::Char('j'),\n\t\t\t\tKeyModifiers::CONTROL\n\t\t\t)\n\t\t);\n\t\tassert_eq!(\n\t\t\tkeys.move_up,\n\t\t\tGituiKeyEvent::new(\n\t\t\t\tKeyCode::Char('h'),\n\t\t\t\tKeyModifiers::NONE\n\t\t\t)\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "src/keys/mod.rs",
    "content": "mod key_config;\nmod key_list;\nmod symbols;\n\npub use key_config::{KeyConfig, SharedKeyConfig};\npub use key_list::key_match;\n"
  },
  {
    "path": "src/keys/symbols.rs",
    "content": "use std::{fs::File, io::Read, path::PathBuf};\n\nuse anyhow::Result;\nuse serde::{Deserialize, Serialize};\n\n#[derive(Debug, Clone)]\npub struct KeySymbols {\n\tpub enter: String,\n\tpub left: String,\n\tpub right: String,\n\tpub up: String,\n\tpub down: String,\n\tpub backspace: String,\n\tpub home: String,\n\tpub end: String,\n\tpub page_up: String,\n\tpub page_down: String,\n\tpub tab: String,\n\tpub back_tab: String,\n\tpub delete: String,\n\tpub insert: String,\n\tpub esc: String,\n\tpub control: String,\n\tpub shift: String,\n\tpub alt: String,\n}\n\n#[rustfmt::skip]\nimpl Default for KeySymbols {\n\tfn default() -> Self {\n\t\tSelf {\n\t\t\tenter: \"\\u{23ce}\".into(),     //⏎\n\t\t\tleft: \"\\u{2190}\".into(),      //←\n\t\t\tright: \"\\u{2192}\".into(),     //→\n\t\t\tup: \"\\u{2191}\".into(),        //↑\n\t\t\tdown: \"\\u{2193}\".into(),      //↓\n\t\t\tbackspace: \"\\u{232b}\".into(), //⌫\n\t\t\thome: \"\\u{2912}\".into(),      //⤒\n\t\t\tend: \"\\u{2913}\".into(),       //⤓\n\t\t\tpage_up: \"\\u{21de}\".into(),   //⇞\n\t\t\tpage_down: \"\\u{21df}\".into(), //⇟\n\t\t\ttab: \"\\u{21e5}\".into(),       //⇥\n\t\t\tback_tab: \"\\u{21e4}\".into(),  //⇤\n\t\t\tdelete: \"\\u{2326}\".into(),    //⌦\n\t\t\tinsert: \"\\u{2380}\".into(),    //⎀\n\t\t\tesc: \"\\u{238b}\".into(),       //⎋\n\t\t\tcontrol: \"^\".into(),\n\t\t\tshift: \"\\u{21e7}\".into(),     //⇧\n\t\t\talt: \"\\u{2325}\".into(),       //⌥\n\t\t}\n\t}\n}\n\nimpl KeySymbols {\n\tpub fn init(file: PathBuf) -> Self {\n\t\tif file.exists() {\n\t\t\tlet file =\n\t\t\t\tKeySymbolsFile::read_file(file).unwrap_or_default();\n\t\t\tfile.get_symbols()\n\t\t} else {\n\t\t\tSelf::default()\n\t\t}\n\t}\n}\n\n//TODO: this could auto generated in a proc macro\n#[derive(Serialize, Deserialize, Default)]\npub struct KeySymbolsFile {\n\tpub enter: Option<String>,\n\tpub left: Option<String>,\n\tpub right: Option<String>,\n\tpub up: Option<String>,\n\tpub down: Option<String>,\n\tpub backspace: Option<String>,\n\tpub home: Option<String>,\n\tpub end: Option<String>,\n\tpub page_up: Option<String>,\n\tpub page_down: Option<String>,\n\tpub tab: Option<String>,\n\tpub back_tab: Option<String>,\n\tpub delete: Option<String>,\n\tpub insert: Option<String>,\n\tpub esc: Option<String>,\n\tpub control: Option<String>,\n\tpub shift: Option<String>,\n\tpub alt: Option<String>,\n}\n\nimpl KeySymbolsFile {\n\tfn read_file(config_file: PathBuf) -> Result<Self> {\n\t\tlet mut f = File::open(config_file)?;\n\t\tlet mut buffer = Vec::new();\n\t\tf.read_to_end(&mut buffer)?;\n\t\tOk(ron::de::from_bytes(&buffer)?)\n\t}\n\n\tpub fn get_symbols(self) -> KeySymbols {\n\t\tlet default = KeySymbols::default();\n\n\t\tKeySymbols {\n\t\t\tenter: self.enter.unwrap_or(default.enter),\n\t\t\tleft: self.left.unwrap_or(default.left),\n\t\t\tright: self.right.unwrap_or(default.right),\n\t\t\tup: self.up.unwrap_or(default.up),\n\t\t\tdown: self.down.unwrap_or(default.down),\n\t\t\tbackspace: self.backspace.unwrap_or(default.backspace),\n\t\t\thome: self.home.unwrap_or(default.home),\n\t\t\tend: self.end.unwrap_or(default.end),\n\t\t\tpage_up: self.page_up.unwrap_or(default.page_up),\n\t\t\tpage_down: self.page_down.unwrap_or(default.page_down),\n\t\t\ttab: self.tab.unwrap_or(default.tab),\n\t\t\tback_tab: self.back_tab.unwrap_or(default.back_tab),\n\t\t\tdelete: self.delete.unwrap_or(default.delete),\n\t\t\tinsert: self.insert.unwrap_or(default.insert),\n\t\t\tesc: self.esc.unwrap_or(default.esc),\n\t\t\tcontrol: self.control.unwrap_or(default.control),\n\t\t\tshift: self.shift.unwrap_or(default.shift),\n\t\t\talt: self.alt.unwrap_or(default.alt),\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/main.rs",
    "content": "//!\n//! The gitui program is a text-based UI for working with a Git repository.\n//! The main navigation occurs between a number of tabs.\n//! When you execute commands, the program may use popups to communicate\n//! with the user. It is possible to customize the keybindings.\n//!\n//!\n//! ## Internal Modules\n//! The top-level modules of gitui can be grouped as follows:\n//!\n//! - User Interface\n//!   - [tabs] for main navigation\n//!   - [components] for visual elements used on tabs\n//!   - [popups] for temporary dialogs\n//!   - [ui] for tooling like scrollbars\n//! - Git Interface\n//!   - [asyncgit] (crate) for async operations on repository\n//! - Distribution and Documentation\n//!   - Project files\n//!   - Github CI\n//!   - Installation files\n//!   - Usage guides\n//!\n//! ## Included Crates\n//! Some crates are part of the gitui repository:\n//! - [asyncgit] for Git operations in the background.\n//!   - git2-hooks (used by asyncgit).\n//!     - git2-testing (used by git2-hooks).\n//!   - invalidstring used by asyncgit for testing with invalid strings.\n//! - [filetreelist] for a tree view of files.\n//! - [scopetime] for measuring execution time.\n//!\n\n#![forbid(unsafe_code)]\n#![deny(\n\tmismatched_lifetime_syntaxes,\n\tunused_imports,\n\tunused_must_use,\n\tdead_code,\n\tunstable_name_collisions,\n\tunused_assignments\n)]\n#![deny(clippy::all, clippy::perf, clippy::nursery, clippy::pedantic)]\n#![deny(\n\tclippy::unwrap_used,\n\tclippy::filetype_is_file,\n\tclippy::cargo,\n\tclippy::panic,\n\tclippy::match_like_matches_macro\n)]\n#![allow(\n\tclippy::multiple_crate_versions,\n\tclippy::bool_to_int_with_if,\n\tclippy::module_name_repetitions,\n\tclippy::empty_docs,\n\tclippy::unnecessary_debug_formatting\n)]\n\n//TODO:\n// #![deny(clippy::expect_used)]\n\nmod app;\nmod args;\nmod bug_report;\nmod clipboard;\nmod cmdbar;\nmod components;\nmod input;\nmod keys;\nmod notify_mutex;\nmod options;\nmod popup_stack;\nmod popups;\nmod queue;\nmod spinner;\nmod string_utils;\nmod strings;\nmod tabs;\nmod ui;\nmod watcher;\n\nuse crate::{\n\tapp::App,\n\targs::{process_cmdline, CliArgs},\n};\nuse anyhow::{anyhow, bail, Result};\nuse app::QuitState;\nuse asyncgit::{\n\tsync::{utils::repo_work_dir, RepoPath},\n\tAsyncGitNotification,\n};\nuse backtrace::Backtrace;\nuse crossbeam_channel::{never, tick, unbounded, Receiver, Select};\nuse crossterm::{\n\tterminal::{\n\t\tdisable_raw_mode, enable_raw_mode, EnterAlternateScreen,\n\t\tLeaveAlternateScreen,\n\t},\n\tExecutableCommand,\n};\nuse input::{Input, InputEvent, InputState};\nuse keys::KeyConfig;\nuse ratatui::backend::CrosstermBackend;\nuse scopeguard::defer;\nuse scopetime::scope_time;\nuse spinner::Spinner;\nuse std::{\n\tio::{self, Stdout},\n\tpanic,\n\tpath::Path,\n\ttime::{Duration, Instant},\n};\nuse ui::style::Theme;\nuse watcher::RepoWatcher;\n\ntype Terminal = ratatui::Terminal<CrosstermBackend<io::Stdout>>;\n\nstatic TICK_INTERVAL: Duration = Duration::from_secs(5);\nstatic SPINNER_INTERVAL: Duration = Duration::from_millis(80);\n\n///\n#[derive(Clone)]\npub enum QueueEvent {\n\tTick,\n\tNotify,\n\tSpinnerUpdate,\n\tAsyncEvent(AsyncNotification),\n\tInputEvent(InputEvent),\n}\n\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\npub enum SyntaxHighlightProgress {\n\tProgress,\n\tDone,\n}\n\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\npub enum AsyncAppNotification {\n\t///\n\tSyntaxHighlighting(SyntaxHighlightProgress),\n}\n\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\npub enum AsyncNotification {\n\t///\n\tApp(AsyncAppNotification),\n\t///\n\tGit(AsyncGitNotification),\n}\n\n#[derive(Clone, Copy, PartialEq)]\nenum Updater {\n\tTicker,\n\tNotifyWatcher,\n}\n\n/// Do `log::error!` and `eprintln!` in one line.\nmacro_rules! log_eprintln {\n\t( $($arg:tt)* ) => {{\n\t\tlog::error!($($arg)*);\n\t\teprintln!($($arg)*);\n\t}};\n}\n\nfn main() -> Result<()> {\n\tlet app_start = Instant::now();\n\n\tlet cliargs = process_cmdline()?;\n\n\tasyncgit::register_tracing_logging();\n\tensure_valid_path(&cliargs.repo_path)?;\n\n\tlet key_config = KeyConfig::init(\n\t\tcliargs.key_bindings_path.as_ref(),\n\t\tcliargs.key_symbols_path.as_ref(),\n\t)\n\t.map_err(|e| log_eprintln!(\"KeyConfig loading error: {e}\"))\n\t.unwrap_or_default();\n\tlet theme = Theme::init(&cliargs.theme);\n\n\tsetup_terminal()?;\n\tdefer! {\n\t\tshutdown_terminal();\n\t}\n\n\tset_panic_handler()?;\n\n\tlet mut terminal =\n\t\tstart_terminal(io::stdout(), &cliargs.repo_path)?;\n\tlet input = Input::new();\n\n\tlet updater = if cliargs.notify_watcher {\n\t\tUpdater::NotifyWatcher\n\t} else {\n\t\tUpdater::Ticker\n\t};\n\n\tlet mut args = cliargs;\n\n\tloop {\n\t\tlet quit_state = run_app(\n\t\t\tapp_start,\n\t\t\targs.clone(),\n\t\t\ttheme.clone(),\n\t\t\tkey_config.clone(),\n\t\t\t&input,\n\t\t\tupdater,\n\t\t\t&mut terminal,\n\t\t)?;\n\n\t\tmatch quit_state {\n\t\t\tQuitState::OpenSubmodule(p) => {\n\t\t\t\targs = CliArgs {\n\t\t\t\t\trepo_path: p,\n\t\t\t\t\tselect_file: None,\n\t\t\t\t\ttheme: args.theme,\n\t\t\t\t\tnotify_watcher: args.notify_watcher,\n\t\t\t\t\tkey_bindings_path: args.key_bindings_path,\n\t\t\t\t\tkey_symbols_path: args.key_symbols_path,\n\t\t\t\t}\n\t\t\t}\n\t\t\t_ => break,\n\t\t}\n\t}\n\n\tOk(())\n}\n\nfn run_app(\n\tapp_start: Instant,\n\tcliargs: CliArgs,\n\ttheme: Theme,\n\tkey_config: KeyConfig,\n\tinput: &Input,\n\tupdater: Updater,\n\tterminal: &mut Terminal,\n) -> Result<QuitState, anyhow::Error> {\n\tlet (tx_git, rx_git) = unbounded();\n\tlet (tx_app, rx_app) = unbounded();\n\n\tlet rx_input = input.receiver();\n\n\tlet (rx_ticker, rx_watcher) = match updater {\n\t\tUpdater::NotifyWatcher => {\n\t\t\tlet repo_watcher = RepoWatcher::new(\n\t\t\t\trepo_work_dir(&cliargs.repo_path)?.as_str(),\n\t\t\t);\n\n\t\t\t(never(), repo_watcher.receiver())\n\t\t}\n\t\tUpdater::Ticker => (tick(TICK_INTERVAL), never()),\n\t};\n\n\tlet spinner_ticker = tick(SPINNER_INTERVAL);\n\n\tlet mut app = App::new(\n\t\tcliargs,\n\t\ttx_git,\n\t\ttx_app,\n\t\tinput.clone(),\n\t\ttheme,\n\t\tkey_config,\n\t)?;\n\n\tlet mut spinner = Spinner::default();\n\tlet mut first_update = true;\n\n\tlog::trace!(\"app start: {} ms\", app_start.elapsed().as_millis());\n\n\tloop {\n\t\tlet event = if first_update {\n\t\t\tfirst_update = false;\n\t\t\tQueueEvent::Notify\n\t\t} else {\n\t\t\tselect_event(\n\t\t\t\t&rx_input,\n\t\t\t\t&rx_git,\n\t\t\t\t&rx_app,\n\t\t\t\t&rx_ticker,\n\t\t\t\t&rx_watcher,\n\t\t\t\t&spinner_ticker,\n\t\t\t)?\n\t\t};\n\n\t\t{\n\t\t\tif matches!(event, QueueEvent::SpinnerUpdate) {\n\t\t\t\tspinner.update();\n\t\t\t\tspinner.draw(terminal)?;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tscope_time!(\"loop\");\n\n\t\t\tmatch event {\n\t\t\t\tQueueEvent::InputEvent(ev) => {\n\t\t\t\t\tif matches!(\n\t\t\t\t\t\tev,\n\t\t\t\t\t\tInputEvent::State(InputState::Polling)\n\t\t\t\t\t) {\n\t\t\t\t\t\t//Note: external ed closed, we need to re-hide cursor\n\t\t\t\t\t\tterminal.hide_cursor()?;\n\t\t\t\t\t}\n\t\t\t\t\tapp.event(ev)?;\n\t\t\t\t}\n\t\t\t\tQueueEvent::Tick | QueueEvent::Notify => {\n\t\t\t\t\tapp.update()?;\n\t\t\t\t}\n\t\t\t\tQueueEvent::AsyncEvent(ev) => {\n\t\t\t\t\tif !matches!(\n\t\t\t\t\t\tev,\n\t\t\t\t\t\tAsyncNotification::Git(\n\t\t\t\t\t\t\tAsyncGitNotification::FinishUnchanged\n\t\t\t\t\t\t)\n\t\t\t\t\t) {\n\t\t\t\t\t\tapp.update_async(ev)?;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tQueueEvent::SpinnerUpdate => unreachable!(),\n\t\t\t}\n\n\t\t\tdraw(terminal, &app)?;\n\n\t\t\tspinner.set_state(app.any_work_pending());\n\t\t\tspinner.draw(terminal)?;\n\n\t\t\tif app.is_quit() {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\tOk(app.quit_state())\n}\n\nfn setup_terminal() -> Result<()> {\n\tenable_raw_mode()?;\n\tio::stdout().execute(EnterAlternateScreen)?;\n\tOk(())\n}\n\nfn shutdown_terminal() {\n\tlet leave_screen =\n\t\tio::stdout().execute(LeaveAlternateScreen).map(|_f| ());\n\n\tif let Err(e) = leave_screen {\n\t\tlog::error!(\"leave_screen failed:\\n{e}\");\n\t}\n\n\tlet leave_raw_mode = disable_raw_mode();\n\n\tif let Err(e) = leave_raw_mode {\n\t\tlog::error!(\"leave_raw_mode failed:\\n{e}\");\n\t}\n}\n\nfn draw(terminal: &mut Terminal, app: &App) -> io::Result<()> {\n\tif app.requires_redraw() {\n\t\tterminal.clear()?;\n\t}\n\n\tterminal.draw(|f| {\n\t\tif let Err(e) = app.draw(f) {\n\t\t\tlog::error!(\"failed to draw: {e:?}\");\n\t\t}\n\t})?;\n\n\tOk(())\n}\n\nfn ensure_valid_path(repo_path: &RepoPath) -> Result<()> {\n\tmatch asyncgit::sync::repo_open_error(repo_path) {\n\t\tSome(e) => {\n\t\t\tlog::error!(\"invalid repo path: {e}\");\n\t\t\tbail!(\"invalid repo path: {e}\")\n\t\t}\n\t\tNone => Ok(()),\n\t}\n}\n\nfn select_event(\n\trx_input: &Receiver<InputEvent>,\n\trx_git: &Receiver<AsyncGitNotification>,\n\trx_app: &Receiver<AsyncAppNotification>,\n\trx_ticker: &Receiver<Instant>,\n\trx_notify: &Receiver<()>,\n\trx_spinner: &Receiver<Instant>,\n) -> Result<QueueEvent> {\n\tlet mut sel = Select::new();\n\n\tsel.recv(rx_input);\n\tsel.recv(rx_git);\n\tsel.recv(rx_app);\n\tsel.recv(rx_ticker);\n\tsel.recv(rx_notify);\n\tsel.recv(rx_spinner);\n\n\tlet oper = sel.select();\n\tlet index = oper.index();\n\n\tlet ev = match index {\n\t\t0 => oper.recv(rx_input).map(QueueEvent::InputEvent),\n\t\t1 => oper.recv(rx_git).map(|e| {\n\t\t\tQueueEvent::AsyncEvent(AsyncNotification::Git(e))\n\t\t}),\n\t\t2 => oper.recv(rx_app).map(|e| {\n\t\t\tQueueEvent::AsyncEvent(AsyncNotification::App(e))\n\t\t}),\n\t\t3 => oper.recv(rx_ticker).map(|_| QueueEvent::Notify),\n\t\t4 => oper.recv(rx_notify).map(|()| QueueEvent::Notify),\n\t\t5 => oper.recv(rx_spinner).map(|_| QueueEvent::SpinnerUpdate),\n\t\t_ => bail!(\"unknown select source\"),\n\t}?;\n\n\tOk(ev)\n}\n\nfn start_terminal(\n\tbuf: Stdout,\n\trepo_path: &RepoPath,\n) -> Result<Terminal> {\n\tlet mut path = repo_path.gitpath().canonicalize()?;\n\tlet home = dirs::home_dir().ok_or_else(|| {\n\t\tanyhow!(\"failed to find the home directory\")\n\t})?;\n\tif path.starts_with(&home) {\n\t\tlet relative_part = path\n\t\t\t.strip_prefix(&home)\n\t\t\t.expect(\"can't fail because of the if statement\");\n\t\tpath = Path::new(\"~\").join(relative_part);\n\t}\n\n\tlet mut backend = CrosstermBackend::new(buf);\n\tbackend.execute(crossterm::terminal::SetTitle(format!(\n\t\t\"gitui ({})\",\n\t\tpath.display()\n\t)))?;\n\n\tlet mut terminal = Terminal::new(backend)?;\n\tterminal.hide_cursor()?;\n\tterminal.clear()?;\n\n\tOk(terminal)\n}\n\nfn set_panic_handler() -> Result<()> {\n\tpanic::set_hook(Box::new(|e| {\n\t\tlet backtrace = Backtrace::new();\n\t\tshutdown_terminal();\n\t\tlog_eprintln!(\"\\nGitUI was closed due to an unexpected panic.\\nPlease file an issue on https://github.com/gitui-org/gitui/issues with the following info:\\n\\n{e}\\n\\ntrace:\\n{backtrace:?}\");\n\t}));\n\n\t// global threadpool\n\trayon_core::ThreadPoolBuilder::new()\n\t\t.num_threads(4)\n\t\t.build_global()?;\n\n\tOk(())\n}\n"
  },
  {
    "path": "src/notify_mutex.rs",
    "content": "use std::sync::{Arc, Condvar, Mutex};\n\n/// combines a `Mutex` and `Condvar` to allow waiting for a change in the variable protected by the `Mutex`\n#[derive(Clone, Debug)]\npub struct NotifiableMutex<T>\nwhere\n\tT: Send + Sync,\n{\n\tdata: Arc<(Mutex<T>, Condvar)>,\n}\n\nimpl<T> NotifiableMutex<T>\nwhere\n\tT: Send + Sync,\n{\n\t///\n\tpub fn new(start_value: T) -> Self {\n\t\tSelf {\n\t\t\tdata: Arc::new((Mutex::new(start_value), Condvar::new())),\n\t\t}\n\t}\n\n\t///\n\tpub fn wait(&self, condition: T)\n\twhere\n\t\tT: PartialEq + Copy,\n\t{\n\t\tlet mut data = self.data.0.lock().expect(\"lock err\");\n\t\twhile *data != condition {\n\t\t\tdata = self.data.1.wait(data).expect(\"wait err\");\n\t\t}\n\t\tdrop(data);\n\t}\n\n\t///\n\tpub fn set_and_notify(&self, value: T) {\n\t\t*self.data.0.lock().expect(\"set err\") = value;\n\t\tself.data.1.notify_one();\n\t}\n\n\t///\n\tpub fn get(&self) -> T\n\twhere\n\t\tT: Copy,\n\t{\n\t\t*self.data.0.lock().expect(\"get err\")\n\t}\n}\n"
  },
  {
    "path": "src/options.rs",
    "content": "use anyhow::Result;\nuse asyncgit::sync::{\n\tdiff::DiffOptions, repo_dir, RepoPathRef,\n\tShowUntrackedFilesConfig,\n};\nuse ron::{\n\tde::from_bytes,\n\tser::{to_string_pretty, PrettyConfig},\n};\nuse serde::{Deserialize, Serialize};\nuse std::{\n\tcell::RefCell,\n\tfs::File,\n\tio::{Read, Write},\n\tpath::PathBuf,\n\trc::Rc,\n};\n\n#[derive(Default, Clone, Serialize, Deserialize)]\nstruct OptionsData {\n\tpub tab: usize,\n\tpub diff: DiffOptions,\n\tpub status_show_untracked: Option<ShowUntrackedFilesConfig>,\n\tpub commit_msgs: Vec<String>,\n}\n\nconst COMMIT_MSG_HISTORY_LENGTH: usize = 20;\n\n#[derive(Clone)]\npub struct Options {\n\trepo: RepoPathRef,\n\tdata: OptionsData,\n}\n\n#[cfg(test)]\nimpl Options {\n\tpub fn test_env() -> Self {\n\t\tuse asyncgit::sync::RepoPath;\n\t\tSelf {\n\t\t\trepo: RefCell::new(RepoPath::Path(Default::default())),\n\t\t\tdata: Default::default(),\n\t\t}\n\t}\n}\n\npub type SharedOptions = Rc<RefCell<Options>>;\n\nimpl Options {\n\tpub fn new(repo: RepoPathRef) -> SharedOptions {\n\t\tRc::new(RefCell::new(Self {\n\t\t\tdata: Self::read(&repo).unwrap_or_default(),\n\t\t\trepo,\n\t\t}))\n\t}\n\n\tpub fn set_current_tab(&mut self, tab: usize) {\n\t\tself.data.tab = tab;\n\t\tself.save();\n\t}\n\n\tpub const fn current_tab(&self) -> usize {\n\t\tself.data.tab\n\t}\n\n\tpub const fn diff_options(&self) -> DiffOptions {\n\t\tself.data.diff\n\t}\n\n\tpub const fn status_show_untracked(\n\t\t&self,\n\t) -> Option<ShowUntrackedFilesConfig> {\n\t\tself.data.status_show_untracked\n\t}\n\n\tpub fn set_status_show_untracked(\n\t\t&mut self,\n\t\tvalue: Option<ShowUntrackedFilesConfig>,\n\t) {\n\t\tself.data.status_show_untracked = value;\n\t\tself.save();\n\t}\n\n\tpub fn diff_context_change(&mut self, increase: bool) {\n\t\tself.data.diff.context = if increase {\n\t\t\tself.data.diff.context.saturating_add(1)\n\t\t} else {\n\t\t\tself.data.diff.context.saturating_sub(1)\n\t\t};\n\n\t\tself.save();\n\t}\n\n\tpub fn diff_hunk_lines_change(&mut self, increase: bool) {\n\t\tself.data.diff.interhunk_lines = if increase {\n\t\t\tself.data.diff.interhunk_lines.saturating_add(1)\n\t\t} else {\n\t\t\tself.data.diff.interhunk_lines.saturating_sub(1)\n\t\t};\n\n\t\tself.save();\n\t}\n\n\tpub fn diff_toggle_whitespace(&mut self) {\n\t\tself.data.diff.ignore_whitespace =\n\t\t\t!self.data.diff.ignore_whitespace;\n\n\t\tself.save();\n\t}\n\n\tpub fn add_commit_msg(&mut self, msg: &str) {\n\t\tself.data.commit_msgs.push(msg.to_owned());\n\t\twhile self.data.commit_msgs.len() > COMMIT_MSG_HISTORY_LENGTH\n\t\t{\n\t\t\tself.data.commit_msgs.remove(0);\n\t\t}\n\t\tself.save();\n\t}\n\n\tpub const fn has_commit_msg_history(&self) -> bool {\n\t\t!self.data.commit_msgs.is_empty()\n\t}\n\n\tpub fn commit_msg(&self, idx: usize) -> Option<String> {\n\t\tif self.data.commit_msgs.is_empty() {\n\t\t\tNone\n\t\t} else {\n\t\t\tlet entries = self.data.commit_msgs.len();\n\t\t\tlet mut index = idx;\n\n\t\t\twhile index >= entries {\n\t\t\t\tindex -= entries;\n\t\t\t}\n\n\t\t\tindex = entries.saturating_sub(1) - index;\n\n\t\t\tSome(self.data.commit_msgs[index].clone())\n\t\t}\n\t}\n\n\tfn save(&self) {\n\t\tif let Err(e) = self.save_failable() {\n\t\t\tlog::error!(\"options save error: {e}\");\n\t\t}\n\t}\n\n\tfn read(repo: &RepoPathRef) -> Result<OptionsData> {\n\t\tlet dir = Self::options_file(repo)?;\n\n\t\tlet mut f = File::open(dir)?;\n\t\tlet mut buffer = Vec::new();\n\t\tf.read_to_end(&mut buffer)?;\n\t\tOk(from_bytes(&buffer)?)\n\t}\n\n\tfn save_failable(&self) -> Result<()> {\n\t\tlet dir = Self::options_file(&self.repo)?;\n\n\t\tlet mut file = File::create(dir)?;\n\t\tlet data =\n\t\t\tto_string_pretty(&self.data, PrettyConfig::default())?;\n\t\tfile.write_all(data.as_bytes())?;\n\n\t\tOk(())\n\t}\n\n\tfn options_file(repo: &RepoPathRef) -> Result<PathBuf> {\n\t\tlet dir = repo_dir(&repo.borrow())?;\n\t\tlet dir = dir.join(\"gitui\");\n\t\tOk(dir)\n\t}\n}\n"
  },
  {
    "path": "src/popup_stack.rs",
    "content": "use crate::queue::StackablePopupOpen;\n\n#[derive(Default)]\npub struct PopupStack {\n\tstack: Vec<StackablePopupOpen>,\n}\n\nimpl PopupStack {\n\tpub fn push(&mut self, popup: StackablePopupOpen) {\n\t\tself.stack.push(popup);\n\t}\n\n\tpub fn pop(&mut self) -> Option<StackablePopupOpen> {\n\t\tself.stack.pop()\n\t}\n}\n"
  },
  {
    "path": "src/popups/blame_file.rs",
    "content": "use crate::{\n\tapp::Environment,\n\tcomponents::{\n\t\tstring_width_align, time_to_string, visibility_blocking,\n\t\tCommandBlocking, CommandInfo, Component, DrawableComponent,\n\t\tEventState, ScrollType,\n\t},\n\tkeys::{key_match, SharedKeyConfig},\n\tpopups::{FileRevOpen, InspectCommitOpen},\n\tqueue::{InternalEvent, Queue, StackablePopupOpen},\n\tstring_utils::tabs_to_spaces,\n\tstrings,\n\tui::{self, style::SharedTheme, AsyncSyntaxJob, SyntaxText},\n\tAsyncAppNotification, AsyncNotification, SyntaxHighlightProgress,\n};\nuse anyhow::Result;\nuse asyncgit::{\n\tasyncjob::AsyncSingleJob,\n\tsync::{BlameHunk, CommitId, FileBlame, RepoPathRef},\n\tAsyncBlame, AsyncGitNotification, BlameParams,\n};\nuse crossbeam_channel::Sender;\nuse crossterm::event::Event;\nuse ratatui::{\n\tlayout::{Constraint, Rect},\n\tsymbols::line::VERTICAL,\n\ttext::{Span, Text},\n\twidgets::{Block, Borders, Cell, Clear, Row, Table, TableState},\n\tFrame,\n};\nuse std::path::Path;\n\nstatic NO_COMMIT_ID: &str = \"0000000\";\nstatic NO_AUTHOR: &str = \"<no author>\";\nstatic MIN_AUTHOR_WIDTH: usize = 3;\nstatic MAX_AUTHOR_WIDTH: usize = 20;\n\nstruct SyntaxFileBlame {\n\tpub file_blame: FileBlame,\n\tpub styled_text: Option<SyntaxText>,\n}\n\nimpl SyntaxFileBlame {\n\tfn path(&self) -> &str {\n\t\t&self.file_blame.path\n\t}\n\n\tconst fn commit_id(&self) -> &CommitId {\n\t\t&self.file_blame.commit_id\n\t}\n\n\tconst fn lines(&self) -> &Vec<(Option<BlameHunk>, String)> {\n\t\t&self.file_blame.lines\n\t}\n}\n\nenum BlameProcess {\n\tGettingBlame(AsyncBlame),\n\tSyntaxHighlighting {\n\t\tunstyled_file_blame: SyntaxFileBlame,\n\t\tjob: AsyncSingleJob<AsyncSyntaxJob>,\n\t},\n\tResult(SyntaxFileBlame),\n}\n\nimpl BlameProcess {\n\tconst fn result(&self) -> Option<&SyntaxFileBlame> {\n\t\tmatch self {\n\t\t\tSelf::GettingBlame(_) => None,\n\t\t\tSelf::SyntaxHighlighting {\n\t\t\t\tunstyled_file_blame,\n\t\t\t\t..\n\t\t\t} => Some(unstyled_file_blame),\n\t\t\tSelf::Result(ref file_blame) => Some(file_blame),\n\t\t}\n\t}\n}\n\n#[derive(Clone, Debug)]\npub struct BlameFileOpen {\n\tpub file_path: String,\n\tpub commit_id: Option<CommitId>,\n\tpub selection: Option<usize>,\n}\n\npub struct BlameFilePopup {\n\ttitle: String,\n\ttheme: SharedTheme,\n\tqueue: Queue,\n\tvisible: bool,\n\topen_request: Option<BlameFileOpen>,\n\tparams: Option<BlameParams>,\n\ttable_state: std::cell::Cell<TableState>,\n\tkey_config: SharedKeyConfig,\n\tcurrent_height: std::cell::Cell<usize>,\n\tblame: Option<BlameProcess>,\n\tapp_sender: Sender<AsyncAppNotification>,\n\tgit_sender: Sender<AsyncGitNotification>,\n\trepo: RepoPathRef,\n}\n\nimpl DrawableComponent for BlameFilePopup {\n\tfn draw(&self, f: &mut Frame, area: Rect) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tlet title = self.get_title();\n\n\t\t\tlet rows = self.get_rows(area.width.into());\n\t\t\tlet author_width = get_author_width(area.width.into());\n\t\t\tlet constraints = [\n\t\t\t\t// commit id\n\t\t\t\tConstraint::Length(7),\n\t\t\t\t// commit date\n\t\t\t\tConstraint::Length(10),\n\t\t\t\t// commit author\n\t\t\t\tConstraint::Length(author_width.try_into()?),\n\t\t\t\t// line number and vertical bar\n\t\t\t\tConstraint::Length(\n\t\t\t\t\t(self.get_line_number_width().saturating_add(1))\n\t\t\t\t\t\t.try_into()?,\n\t\t\t\t),\n\t\t\t\t// the source code line\n\t\t\t\tConstraint::Percentage(100),\n\t\t\t];\n\n\t\t\tlet number_of_rows: usize = rows.len();\n\t\t\tlet syntax_highlight_progress = match self.blame {\n\t\t\t\tSome(BlameProcess::SyntaxHighlighting {\n\t\t\t\t\tref job,\n\t\t\t\t\t..\n\t\t\t\t}) => job\n\t\t\t\t\t.progress()\n\t\t\t\t\t.map(|p| format!(\" ({}%)\", p.progress))\n\t\t\t\t\t.unwrap_or_default(),\n\t\t\t\t_ => String::new(),\n\t\t\t};\n\t\t\tlet title_with_highlight_progress =\n\t\t\t\tformat!(\"{title}{syntax_highlight_progress}\");\n\n\t\t\tlet table = Table::new(rows, constraints)\n\t\t\t\t.column_spacing(1)\n\t\t\t\t.row_highlight_style(self.theme.text(true, true))\n\t\t\t\t.block(\n\t\t\t\t\tBlock::default()\n\t\t\t\t\t\t.borders(Borders::ALL)\n\t\t\t\t\t\t.title(Span::styled(\n\t\t\t\t\t\t\ttitle_with_highlight_progress,\n\t\t\t\t\t\t\tself.theme.title(true),\n\t\t\t\t\t\t))\n\t\t\t\t\t\t.border_style(self.theme.block(true)),\n\t\t\t\t);\n\n\t\t\tlet mut table_state = self.table_state.take();\n\n\t\t\tf.render_widget(Clear, area);\n\t\t\tf.render_stateful_widget(table, area, &mut table_state);\n\n\t\t\tui::draw_scrollbar(\n\t\t\t\tf,\n\t\t\t\tarea,\n\t\t\t\t&self.theme,\n\t\t\t\t// April 2021: `draw_scrollbar` assumes that the last parameter\n\t\t\t\t// is `scroll_top`.  Therefore, it subtracts the area’s height\n\t\t\t\t// before calculating the position of the scrollbar. To account\n\t\t\t\t// for that, we add the current height.\n\t\t\t\tnumber_of_rows + (area.height as usize),\n\t\t\t\t// April 2021: we don’t have access to `table_state.offset`\n\t\t\t\t// (it’s private), so we use `table_state.selected()` as a\n\t\t\t\t// replacement.\n\t\t\t\t//\n\t\t\t\t// Other widgets, for example `BranchListComponent`, manage\n\t\t\t\t// scroll state themselves and use `self.scroll_top` in this\n\t\t\t\t// situation.\n\t\t\t\t//\n\t\t\t\t// There are plans to change `render_stateful_widgets`, so this\n\t\t\t\t// might be acceptable as an interim solution.\n\t\t\t\t//\n\t\t\t\t// https://github.com/fdehau/tui-rs/issues/448\n\t\t\t\ttable_state.selected().unwrap_or(0),\n\t\t\t\tui::Orientation::Vertical,\n\t\t\t);\n\n\t\t\tself.table_state.set(table_state);\n\t\t\tself.current_height.set(area.height.into());\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for BlameFilePopup {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tlet has_result = self\n\t\t\t.blame\n\t\t\t.as_ref()\n\t\t\t.is_some_and(|blame| blame.result().is_some());\n\t\tif self.is_visible() || force_all {\n\t\t\tout.push(\n\t\t\t\tCommandInfo::new(\n\t\t\t\t\tstrings::commands::close_popup(&self.key_config),\n\t\t\t\t\ttrue,\n\t\t\t\t\ttrue,\n\t\t\t\t)\n\t\t\t\t.order(1),\n\t\t\t);\n\t\t\tout.push(\n\t\t\t\tCommandInfo::new(\n\t\t\t\t\tstrings::commands::scroll(&self.key_config),\n\t\t\t\t\ttrue,\n\t\t\t\t\thas_result,\n\t\t\t\t)\n\t\t\t\t.order(1),\n\t\t\t);\n\t\t\tout.push(\n\t\t\t\tCommandInfo::new(\n\t\t\t\t\tstrings::commands::commit_details_open(\n\t\t\t\t\t\t&self.key_config,\n\t\t\t\t\t),\n\t\t\t\t\ttrue,\n\t\t\t\t\thas_result,\n\t\t\t\t)\n\t\t\t\t.order(1),\n\t\t\t);\n\t\t\tout.push(\n\t\t\t\tCommandInfo::new(\n\t\t\t\t\tstrings::commands::open_file_history(\n\t\t\t\t\t\t&self.key_config,\n\t\t\t\t\t),\n\t\t\t\t\ttrue,\n\t\t\t\t\thas_result,\n\t\t\t\t)\n\t\t\t\t.order(1),\n\t\t\t);\n\t\t\tout.push(\n\t\t\t\tCommandInfo::new(\n\t\t\t\t\tstrings::commands::open_line_number_popup(\n\t\t\t\t\t\t&self.key_config,\n\t\t\t\t\t),\n\t\t\t\t\ttrue,\n\t\t\t\t\thas_result,\n\t\t\t\t)\n\t\t\t\t.order(1),\n\t\t\t);\n\t\t}\n\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(\n\t\t&mut self,\n\t\tevent: &crossterm::event::Event,\n\t) -> Result<EventState> {\n\t\tif self.is_visible() {\n\t\t\tif let Event::Key(key) = event {\n\t\t\t\tif key_match(key, self.key_config.keys.exit_popup) {\n\t\t\t\t\tself.hide_stacked(false);\n\t\t\t\t} else if key_match(key, self.key_config.keys.move_up)\n\t\t\t\t{\n\t\t\t\t\tself.move_selection(ScrollType::Up);\n\t\t\t\t} else if key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.move_down,\n\t\t\t\t) {\n\t\t\t\t\tself.move_selection(ScrollType::Down);\n\t\t\t\t} else if key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.shift_up,\n\t\t\t\t) || key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.home,\n\t\t\t\t) {\n\t\t\t\t\tself.move_selection(ScrollType::Home);\n\t\t\t\t} else if key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.shift_down,\n\t\t\t\t) || key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.end,\n\t\t\t\t) {\n\t\t\t\t\tself.move_selection(ScrollType::End);\n\t\t\t\t} else if key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.page_down,\n\t\t\t\t) {\n\t\t\t\t\tself.move_selection(ScrollType::PageDown);\n\t\t\t\t} else if key_match(key, self.key_config.keys.page_up)\n\t\t\t\t{\n\t\t\t\t\tself.move_selection(ScrollType::PageUp);\n\t\t\t\t} else if key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.move_right,\n\t\t\t\t) {\n\t\t\t\t\tif let Some(commit_id) = self.selected_commit() {\n\t\t\t\t\t\tself.hide_stacked(true);\n\t\t\t\t\t\tself.queue.push(InternalEvent::OpenPopup(\n\t\t\t\t\t\t\tStackablePopupOpen::InspectCommit(\n\t\t\t\t\t\t\t\tInspectCommitOpen::new(commit_id),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t));\n\t\t\t\t\t}\n\t\t\t\t} else if key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.file_history,\n\t\t\t\t) {\n\t\t\t\t\tif let Some(filepath) = self\n\t\t\t\t\t\t.params\n\t\t\t\t\t\t.as_ref()\n\t\t\t\t\t\t.map(|p| p.file_path.clone())\n\t\t\t\t\t{\n\t\t\t\t\t\tself.hide_stacked(true);\n\t\t\t\t\t\tself.queue.push(InternalEvent::OpenPopup(\n\t\t\t\t\t\t\tStackablePopupOpen::FileRevlog(\n\t\t\t\t\t\t\t\tFileRevOpen::new(filepath),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t));\n\t\t\t\t\t}\n\t\t\t\t} else if key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.goto_line,\n\t\t\t\t) {\n\t\t\t\t\tlet maybe_blame_result = &self\n\t\t\t\t\t\t.blame\n\t\t\t\t\t\t.as_ref()\n\t\t\t\t\t\t.and_then(|blame| blame.result());\n\t\t\t\t\tif let Some(blame_result) = maybe_blame_result {\n\t\t\t\t\t\tlet max_line = blame_result.lines().len() - 1;\n\t\t\t\t\t\tself.queue.push(\n\t\t\t\t\t\t\tInternalEvent::OpenGotoLinePopup(\n\t\t\t\t\t\t\t\tmax_line,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t}\n\t\t}\n\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\n\t\tOk(())\n\t}\n}\n\nimpl BlameFilePopup {\n\t///\n\tpub fn new(env: &Environment, title: &str) -> Self {\n\t\tSelf {\n\t\t\ttitle: String::from(title),\n\t\t\ttheme: env.theme.clone(),\n\t\t\tqueue: env.queue.clone(),\n\t\t\tvisible: false,\n\t\t\tparams: None,\n\t\t\topen_request: None,\n\t\t\ttable_state: std::cell::Cell::new(TableState::default()),\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\tcurrent_height: std::cell::Cell::new(0),\n\t\t\tapp_sender: env.sender_app.clone(),\n\t\t\tgit_sender: env.sender_git.clone(),\n\t\t\tblame: None,\n\t\t\trepo: env.repo.clone(),\n\t\t}\n\t}\n\n\tfn hide_stacked(&mut self, stack: bool) {\n\t\tself.visible = false;\n\t\tif stack {\n\t\t\tif let Some(request) = self.open_request.clone() {\n\t\t\t\tself.queue.push(InternalEvent::PopupStackPush(\n\t\t\t\t\tStackablePopupOpen::BlameFile(BlameFileOpen {\n\t\t\t\t\t\tfile_path: request.file_path,\n\t\t\t\t\t\tcommit_id: request.commit_id,\n\t\t\t\t\t\tselection: self.get_selection(),\n\t\t\t\t\t}),\n\t\t\t\t));\n\t\t\t}\n\t\t} else {\n\t\t\tself.queue.push(InternalEvent::PopupStackPop);\n\t\t}\n\t}\n\n\t///\n\tpub fn open(&mut self, open: BlameFileOpen) -> Result<()> {\n\t\tself.open_request = Some(open.clone());\n\t\tself.params = Some(BlameParams {\n\t\t\tfile_path: open.file_path,\n\t\t\tcommit_id: open.commit_id,\n\t\t});\n\t\tself.blame =\n\t\t\tSome(BlameProcess::GettingBlame(AsyncBlame::new(\n\t\t\t\tself.repo.borrow().clone(),\n\t\t\t\t&self.git_sender,\n\t\t\t)));\n\t\tself.table_state.get_mut().select(Some(0));\n\t\tself.visible = true;\n\t\tself.update()?;\n\n\t\tOk(())\n\t}\n\n\t///\n\tpub const fn any_work_pending(&self) -> bool {\n\t\tself.blame.is_some()\n\t\t\t&& !matches!(self.blame, Some(BlameProcess::Result(_)))\n\t}\n\n\tpub fn update_async(\n\t\t&mut self,\n\t\tev: AsyncNotification,\n\t) -> Result<()> {\n\t\tif let AsyncNotification::Git(ev) = ev {\n\t\t\treturn self.update_git(ev);\n\t\t}\n\n\t\tself.update_syntax(ev);\n\t\tOk(())\n\t}\n\n\tfn update_git(\n\t\t&mut self,\n\t\tevent: AsyncGitNotification,\n\t) -> Result<()> {\n\t\tif self.is_visible() && event == AsyncGitNotification::Blame {\n\t\t\tself.update()?;\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn update(&mut self) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tif let Some(BlameProcess::GettingBlame(\n\t\t\t\tref mut async_blame,\n\t\t\t)) = self.blame\n\t\t\t{\n\t\t\t\tif let Some(params) = &self.params {\n\t\t\t\t\tif let Some((\n\t\t\t\t\t\tprevious_blame_params,\n\t\t\t\t\t\tlast_file_blame,\n\t\t\t\t\t)) = async_blame.last()?\n\t\t\t\t\t{\n\t\t\t\t\t\tif previous_blame_params == *params {\n\t\t\t\t\t\t\tself.blame = Some(\n\t\t\t\t\t\t\t\tBlameProcess::SyntaxHighlighting {\n\t\t\t\t\t\t\t\t\tunstyled_file_blame:\n\t\t\t\t\t\t\t\t\t\tSyntaxFileBlame {\n\t\t\t\t\t\t\t\t\t\t\tfile_blame:\n\t\t\t\t\t\t\t\t\t\t\t\tlast_file_blame,\n\t\t\t\t\t\t\t\t\t\t\tstyled_text: None,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tjob: AsyncSingleJob::new(\n\t\t\t\t\t\t\t\t\t\tself.app_sender.clone(),\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tself.set_open_selection();\n\t\t\t\t\t\t\tself.highlight_blame_lines();\n\n\t\t\t\t\t\t\treturn Ok(());\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tasync_blame.request(params.clone())?;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn update_syntax(&mut self, ev: AsyncNotification) {\n\t\tlet Some(BlameProcess::SyntaxHighlighting {\n\t\t\tref unstyled_file_blame,\n\t\t\tref job,\n\t\t}) = self.blame\n\t\telse {\n\t\t\treturn;\n\t\t};\n\n\t\tif let AsyncNotification::App(\n\t\t\tAsyncAppNotification::SyntaxHighlighting(progress),\n\t\t) = ev\n\t\t{\n\t\t\tmatch progress {\n\t\t\t\tSyntaxHighlightProgress::Done => {\n\t\t\t\t\tif let Some(job) = job.take_last() {\n\t\t\t\t\t\tif let Some(syntax) = job.result() {\n\t\t\t\t\t\t\tif syntax.path()\n\t\t\t\t\t\t\t\t== Path::new(\n\t\t\t\t\t\t\t\t\tunstyled_file_blame.path(),\n\t\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\tself.blame =\n\t\t\t\t\t\t\t\t\tSome(BlameProcess::Result(\n\t\t\t\t\t\t\t\t\t\tSyntaxFileBlame {\n\t\t\t\t\t\t\t\t\t\t\tfile_blame:\n\t\t\t\t\t\t\t\t\t\t\t\tunstyled_file_blame\n\t\t\t\t\t\t\t\t\t\t\t\t\t.file_blame\n\t\t\t\t\t\t\t\t\t\t\t\t\t.clone(),\n\t\t\t\t\t\t\t\t\t\t\tstyled_text: Some(syntax),\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tSyntaxHighlightProgress::Progress => {}\n\t\t\t}\n\t\t}\n\t}\n\n\t///\n\tfn get_title(&self) -> String {\n\t\tmatch (\n\t\t\tself.any_work_pending(),\n\t\t\tself.params.as_ref(),\n\t\t\tself.blame.as_ref().and_then(|blame| blame.result()),\n\t\t) {\n\t\t\t(true, Some(params), _) => {\n\t\t\t\tformat!(\n\t\t\t\t\t\"{} -- {} -- <calculating.. (who is to blame?)>\",\n\t\t\t\t\tself.title, params.file_path\n\t\t\t\t)\n\t\t\t}\n\t\t\t(false, Some(params), Some(file_blame)) => {\n\t\t\t\tformat!(\n\t\t\t\t\t\"{} -- {} -- {}\",\n\t\t\t\t\tself.title,\n\t\t\t\t\tparams.file_path,\n\t\t\t\t\tfile_blame.commit_id().get_short_string()\n\t\t\t\t)\n\t\t\t}\n\t\t\t(false, Some(params), None) => {\n\t\t\t\tformat!(\n\t\t\t\t\t\"{} -- {} -- <no blame available>\",\n\t\t\t\t\tself.title, params.file_path\n\t\t\t\t)\n\t\t\t}\n\t\t\t_ => format!(\"{} -- <no blame available>\", self.title),\n\t\t}\n\t}\n\n\t///\n\tfn get_rows(&self, width: usize) -> Vec<Row<'_>> {\n\t\tself.blame\n\t\t\t.as_ref()\n\t\t\t.and_then(|blame| blame.result())\n\t\t\t.map(|file_blame| {\n\t\t\t\tlet styled_text: Option<Text<'_>> = file_blame\n\t\t\t\t\t.styled_text\n\t\t\t\t\t.as_ref()\n\t\t\t\t\t.map(std::convert::Into::into);\n\t\t\t\tfile_blame\n\t\t\t\t\t.lines()\n\t\t\t\t\t.iter()\n\t\t\t\t\t.enumerate()\n\t\t\t\t\t.map(|(i, (blame_hunk, line))| {\n\t\t\t\t\t\tself.get_line_blame(\n\t\t\t\t\t\t\twidth,\n\t\t\t\t\t\t\ti,\n\t\t\t\t\t\t\t(blame_hunk.as_ref(), line.as_ref()),\n\t\t\t\t\t\t\tfile_blame,\n\t\t\t\t\t\t\tstyled_text.as_ref(),\n\t\t\t\t\t\t)\n\t\t\t\t\t})\n\t\t\t\t\t.collect()\n\t\t\t})\n\t\t\t.unwrap_or_default()\n\t}\n\n\tfn highlight_blame_lines(&mut self) {\n\t\tlet Some(BlameProcess::SyntaxHighlighting {\n\t\t\tref unstyled_file_blame,\n\t\t\tref mut job,\n\t\t}) = self.blame\n\t\telse {\n\t\t\treturn;\n\t\t};\n\n\t\tlet Some(params) = &self.params else {\n\t\t\treturn;\n\t\t};\n\n\t\tlet raw_lines = unstyled_file_blame\n\t\t\t.lines()\n\t\t\t.iter()\n\t\t\t.map(|l| l.1.clone())\n\t\t\t.collect::<Vec<_>>();\n\t\tlet mut text = tabs_to_spaces(raw_lines.join(\"\\n\"));\n\t\ttext.push('\\n');\n\n\t\tjob.spawn(AsyncSyntaxJob::new(\n\t\t\ttext,\n\t\t\tparams.file_path.clone(),\n\t\t\tself.theme.get_syntax(),\n\t\t));\n\t}\n\n\tfn get_line_blame<'a>(\n\t\t&'a self,\n\t\twidth: usize,\n\t\tline_number: usize,\n\t\thunk_and_line: (Option<&BlameHunk>, &str),\n\t\tfile_blame: &'a SyntaxFileBlame,\n\t\tstyled_text: Option<&Text<'a>>,\n\t) -> Row<'a> {\n\t\tlet (hunk_for_line, line) = hunk_and_line;\n\n\t\tlet show_metadata = if line_number == 0 {\n\t\t\ttrue\n\t\t} else {\n\t\t\tlet hunk_for_previous_line =\n\t\t\t\t&file_blame.lines()[line_number - 1];\n\n\t\t\tmatch (hunk_for_previous_line, hunk_for_line) {\n\t\t\t\t((Some(previous), _), Some(current)) => {\n\t\t\t\t\tprevious.commit_id != current.commit_id\n\t\t\t\t}\n\t\t\t\t_ => true,\n\t\t\t}\n\t\t};\n\n\t\tlet mut cells = if show_metadata {\n\t\t\tself.get_metadata_for_line_blame(width, hunk_for_line)\n\t\t} else {\n\t\t\tvec![Cell::from(\"\"), Cell::from(\"\"), Cell::from(\"\")]\n\t\t};\n\n\t\tlet line_number_width = self.get_line_number_width();\n\n\t\tlet text_cell = styled_text.as_ref().map_or_else(\n\t\t\t|| {\n\t\t\t\tCell::from(tabs_to_spaces(String::from(line)))\n\t\t\t\t\t.style(self.theme.text(true, false))\n\t\t\t},\n\t\t\t|styled_text| {\n\t\t\t\tlet styled_text =\n\t\t\t\t\tstyled_text.lines[line_number].clone();\n\t\t\t\tCell::from(styled_text)\n\t\t\t},\n\t\t);\n\n\t\tcells.push(\n\t\t\tCell::from(format!(\n\t\t\t\t\"{line_number:>line_number_width$}{VERTICAL}\",\n\t\t\t))\n\t\t\t.style(self.theme.text(true, false)),\n\t\t);\n\t\tcells.push(text_cell);\n\n\t\tRow::new(cells)\n\t}\n\n\tfn get_metadata_for_line_blame(\n\t\t&self,\n\t\twidth: usize,\n\t\tblame_hunk: Option<&BlameHunk>,\n\t) -> Vec<Cell<'_>> {\n\t\tlet commit_hash = blame_hunk.map_or_else(\n\t\t\t|| NO_COMMIT_ID.into(),\n\t\t\t|hunk| hunk.commit_id.get_short_string(),\n\t\t);\n\t\tlet author_width = get_author_width(width);\n\t\tlet truncated_author: String = blame_hunk.map_or_else(\n\t\t\t|| NO_AUTHOR.into(),\n\t\t\t|hunk| string_width_align(&hunk.author, author_width),\n\t\t);\n\t\tlet author = format!(\"{truncated_author:MAX_AUTHOR_WIDTH$}\");\n\t\tlet time = blame_hunk.map_or_else(String::new, |hunk| {\n\t\t\ttime_to_string(hunk.time, true)\n\t\t});\n\n\t\tlet file_blame =\n\t\t\tself.blame.as_ref().and_then(|blame| blame.result());\n\t\tlet is_blamed_commit = file_blame\n\t\t\t.and_then(|file_blame| {\n\t\t\t\tblame_hunk.map(|hunk| {\n\t\t\t\t\tfile_blame.commit_id() == &hunk.commit_id\n\t\t\t\t})\n\t\t\t})\n\t\t\t.unwrap_or(false);\n\n\t\tvec![\n\t\t\tCell::from(commit_hash).style(\n\t\t\t\tself.theme.commit_hash_in_blame(is_blamed_commit),\n\t\t\t),\n\t\t\tCell::from(time).style(self.theme.commit_time(false)),\n\t\t\tCell::from(author).style(self.theme.commit_author(false)),\n\t\t]\n\t}\n\n\tfn get_max_line_number(&self) -> usize {\n\t\tself.blame\n\t\t\t.as_ref()\n\t\t\t.and_then(|blame| blame.result())\n\t\t\t.map_or(0, |file_blame| file_blame.lines().len() - 1)\n\t}\n\n\tfn get_line_number_width(&self) -> usize {\n\t\tlet max_line_number = self.get_max_line_number();\n\n\t\tnumber_of_digits(max_line_number)\n\t}\n\n\tfn move_selection(&self, scroll_type: ScrollType) -> bool {\n\t\tlet mut table_state = self.table_state.take();\n\n\t\tlet old_selection = table_state.selected().unwrap_or(0);\n\t\tlet max_selection = self.get_max_line_number();\n\n\t\tlet new_selection = match scroll_type {\n\t\t\tScrollType::Up => old_selection.saturating_sub(1),\n\t\t\tScrollType::Down => {\n\t\t\t\told_selection.saturating_add(1).min(max_selection)\n\t\t\t}\n\t\t\tScrollType::Home => 0,\n\t\t\tScrollType::End => max_selection,\n\t\t\tScrollType::PageUp => old_selection.saturating_sub(\n\t\t\t\tself.current_height.get().saturating_sub(2),\n\t\t\t),\n\t\t\tScrollType::PageDown => old_selection\n\t\t\t\t.saturating_add(\n\t\t\t\t\tself.current_height.get().saturating_sub(2),\n\t\t\t\t)\n\t\t\t\t.min(max_selection),\n\t\t};\n\n\t\tlet needs_update = new_selection != old_selection;\n\n\t\ttable_state.select(Some(new_selection));\n\t\tself.table_state.set(table_state);\n\n\t\tneeds_update\n\t}\n\n\tfn set_open_selection(&self) {\n\t\tif let Some(selection) =\n\t\t\tself.open_request.as_ref().and_then(|req| req.selection)\n\t\t{\n\t\t\tlet mut table_state = self.table_state.take();\n\t\t\ttable_state.select(Some(selection));\n\t\t\tself.table_state.set(table_state);\n\t\t}\n\t}\n\n\tfn get_selection(&self) -> Option<usize> {\n\t\tself.blame\n\t\t\t.as_ref()\n\t\t\t.and_then(|blame| blame.result())\n\t\t\t.and_then(|_| {\n\t\t\t\tlet table_state = self.table_state.take();\n\n\t\t\t\tlet selection = table_state.selected();\n\n\t\t\t\tself.table_state.set(table_state);\n\n\t\t\t\tselection\n\t\t\t})\n\t}\n\n\tpub fn goto_line(&mut self, line: usize) {\n\t\tself.visible = true;\n\t\tlet mut table_state = self.table_state.take();\n\t\ttable_state\n\t\t\t.select(Some(line.clamp(0, self.get_max_line_number())));\n\t\tself.table_state.set(table_state);\n\t}\n\n\tfn selected_commit(&self) -> Option<CommitId> {\n\t\tself.blame\n\t\t\t.as_ref()\n\t\t\t.and_then(|blame| blame.result())\n\t\t\t.and_then(|file_blame| {\n\t\t\t\tlet table_state = self.table_state.take();\n\n\t\t\t\tlet commit_id =\n\t\t\t\t\ttable_state.selected().and_then(|selected| {\n\t\t\t\t\t\tfile_blame.lines()[selected]\n\t\t\t\t\t\t\t.0\n\t\t\t\t\t\t\t.as_ref()\n\t\t\t\t\t\t\t.map(|hunk| hunk.commit_id)\n\t\t\t\t\t});\n\n\t\t\t\tself.table_state.set(table_state);\n\n\t\t\t\tcommit_id\n\t\t\t})\n\t}\n}\n\nfn get_author_width(width: usize) -> usize {\n\t(width.saturating_sub(19) / 3)\n\t\t.clamp(MIN_AUTHOR_WIDTH, MAX_AUTHOR_WIDTH)\n}\n\nconst fn number_of_digits(number: usize) -> usize {\n\tlet mut rest = number;\n\tlet mut result = 0;\n\n\twhile rest > 0 {\n\t\trest /= 10;\n\t\tresult += 1;\n\t}\n\n\tresult\n}\n"
  },
  {
    "path": "src/popups/branchlist.rs",
    "content": "use crate::components::{\n\tvisibility_blocking, CommandBlocking, CommandInfo, Component,\n\tDrawableComponent, EventState, FuzzyFinderTarget, VerticalScroll,\n};\nuse crate::{\n\tapp::Environment,\n\tcomponents::ScrollType,\n\tkeys::{key_match, SharedKeyConfig},\n\tqueue::{\n\t\tAction, InternalEvent, NeedsUpdate, Queue, StackablePopupOpen,\n\t},\n\tstrings, try_or_popup,\n\tui::{self, Size},\n};\nuse anyhow::Result;\nuse asyncgit::{\n\tsync::{\n\t\tself,\n\t\tbranch::{\n\t\t\tcheckout_remote_branch, BranchDetails, LocalBranch,\n\t\t\tRemoteBranch,\n\t\t},\n\t\tcheckout_branch, get_branches_info,\n\t\tstatus::StatusType,\n\t\tBranchInfo, BranchType, CommitId, RepoPathRef, RepoState,\n\t},\n\tAsyncGitNotification,\n};\nuse crossterm::event::{Event, KeyEvent};\nuse ratatui::{\n\tlayout::{\n\t\tAlignment, Constraint, Direction, Layout, Margin, Rect,\n\t},\n\ttext::{Line, Span, Text},\n\twidgets::{Block, BorderType, Borders, Clear, Paragraph, Tabs},\n\tFrame,\n};\nuse std::cell::Cell;\nuse ui::style::SharedTheme;\nuse unicode_truncate::UnicodeTruncateStr;\n\nuse super::InspectCommitOpen;\n\n///\npub struct BranchListPopup {\n\trepo: RepoPathRef,\n\tbranches: Vec<BranchInfo>,\n\tlocal: bool,\n\thas_remotes: bool,\n\tvisible: bool,\n\tselection: u16,\n\tscroll: VerticalScroll,\n\tcurrent_height: Cell<u16>,\n\tqueue: Queue,\n\ttheme: SharedTheme,\n\tkey_config: SharedKeyConfig,\n}\n\nimpl DrawableComponent for BranchListPopup {\n\tfn draw(&self, f: &mut Frame, rect: Rect) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tconst PERCENT_SIZE: Size = Size::new(80, 50);\n\t\t\tconst MIN_SIZE: Size = Size::new(60, 20);\n\n\t\t\tlet area = ui::centered_rect(\n\t\t\t\tPERCENT_SIZE.width,\n\t\t\t\tPERCENT_SIZE.height,\n\t\t\t\tf.area(),\n\t\t\t);\n\t\t\tlet area =\n\t\t\t\tui::rect_inside(MIN_SIZE, f.area().into(), area);\n\t\t\tlet area = area.intersection(rect);\n\n\t\t\tf.render_widget(Clear, area);\n\n\t\t\tf.render_widget(\n\t\t\t\tBlock::default()\n\t\t\t\t\t.title(strings::title_branches())\n\t\t\t\t\t.border_type(BorderType::Thick)\n\t\t\t\t\t.borders(Borders::ALL),\n\t\t\t\tarea,\n\t\t\t);\n\n\t\t\tlet area = area.inner(Margin {\n\t\t\t\tvertical: 1,\n\t\t\t\thorizontal: 1,\n\t\t\t});\n\n\t\t\tlet chunks = Layout::default()\n\t\t\t\t.direction(Direction::Vertical)\n\t\t\t\t.constraints(\n\t\t\t\t\t[Constraint::Length(2), Constraint::Min(1)]\n\t\t\t\t\t\t.as_ref(),\n\t\t\t\t)\n\t\t\t\t.split(area);\n\n\t\t\tself.draw_tabs(f, chunks[0]);\n\t\t\tself.draw_list(f, chunks[1])?;\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for BranchListPopup {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.visible || force_all {\n\t\t\tif !force_all {\n\t\t\t\tout.clear();\n\t\t\t}\n\n\t\t\tself.add_commands_internal(out);\n\t\t}\n\t\tvisibility_blocking(self)\n\t}\n\n\t//TODO: cleanup\n\t#[allow(clippy::cognitive_complexity)]\n\tfn event(&mut self, ev: &Event) -> Result<EventState> {\n\t\tif !self.visible {\n\t\t\treturn Ok(EventState::NotConsumed);\n\t\t}\n\n\t\tif let Event::Key(e) = ev {\n\t\t\tif self.move_event(e)?.is_consumed() {\n\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t}\n\n\t\t\tlet selection_is_cur_branch =\n\t\t\t\tself.selection_is_cur_branch();\n\n\t\t\tif key_match(e, self.key_config.keys.enter) {\n\t\t\t\ttry_or_popup!(\n\t\t\t\t\tself,\n\t\t\t\t\t\"switch branch error:\",\n\t\t\t\t\tself.switch_to_selected_branch()\n\t\t\t\t);\n\t\t\t} else if key_match(e, self.key_config.keys.create_branch)\n\t\t\t\t&& self.local\n\t\t\t{\n\t\t\t\tself.queue.push(InternalEvent::CreateBranch);\n\t\t\t} else if key_match(e, self.key_config.keys.rename_branch)\n\t\t\t\t&& self.valid_selection()\n\t\t\t{\n\t\t\t\tself.rename_branch();\n\t\t\t} else if key_match(e, self.key_config.keys.delete_branch)\n\t\t\t\t&& !selection_is_cur_branch\n\t\t\t\t&& self.valid_selection()\n\t\t\t{\n\t\t\t\tself.delete_branch();\n\t\t\t} else if key_match(e, self.key_config.keys.merge_branch)\n\t\t\t\t&& !selection_is_cur_branch\n\t\t\t\t&& self.valid_selection()\n\t\t\t{\n\t\t\t\ttry_or_popup!(\n\t\t\t\t\tself,\n\t\t\t\t\t\"merge branch error:\",\n\t\t\t\t\tself.merge_branch()\n\t\t\t\t);\n\t\t\t} else if key_match(e, self.key_config.keys.rebase_branch)\n\t\t\t\t&& !selection_is_cur_branch\n\t\t\t\t&& self.valid_selection()\n\t\t\t{\n\t\t\t\ttry_or_popup!(\n\t\t\t\t\tself,\n\t\t\t\t\t\"rebase error:\",\n\t\t\t\t\tself.rebase_branch()\n\t\t\t\t);\n\t\t\t} else if key_match(e, self.key_config.keys.move_right)\n\t\t\t\t&& self.valid_selection()\n\t\t\t{\n\t\t\t\tself.inspect_head_of_branch();\n\t\t\t} else if key_match(\n\t\t\t\te,\n\t\t\t\tself.key_config.keys.compare_commits,\n\t\t\t) && self.valid_selection()\n\t\t\t{\n\t\t\t\tself.hide();\n\t\t\t\tif let Some(commit_id) = self.get_selected_commit() {\n\t\t\t\t\tself.queue.push(InternalEvent::OpenPopup(\n\t\t\t\t\t\tStackablePopupOpen::CompareCommits(\n\t\t\t\t\t\t\tInspectCommitOpen::new(commit_id),\n\t\t\t\t\t\t),\n\t\t\t\t\t));\n\t\t\t\t}\n\t\t\t} else if key_match(e, self.key_config.keys.fetch)\n\t\t\t\t&& self.has_remotes\n\t\t\t{\n\t\t\t\tself.queue.push(InternalEvent::FetchRemotes);\n\t\t\t} else if key_match(e, self.key_config.keys.view_remotes)\n\t\t\t{\n\t\t\t\tself.queue.push(InternalEvent::ViewRemotes);\n\t\t\t} else if key_match(e, self.key_config.keys.reset_branch)\n\t\t\t{\n\t\t\t\tif let Some(commit_id) = self.get_selected_commit() {\n\t\t\t\t\tself.queue.push(InternalEvent::OpenResetPopup(\n\t\t\t\t\t\tcommit_id,\n\t\t\t\t\t));\n\t\t\t\t}\n\t\t\t} else if key_match(\n\t\t\t\te,\n\t\t\t\tself.key_config.keys.cmd_bar_toggle,\n\t\t\t) {\n\t\t\t\t//do not consume if its the more key\n\t\t\t\treturn Ok(EventState::NotConsumed);\n\t\t\t} else if key_match(e, self.key_config.keys.branch_find) {\n\t\t\t\tlet branches = self\n\t\t\t\t\t.branches\n\t\t\t\t\t.iter()\n\t\t\t\t\t.map(|b| b.name.clone())\n\t\t\t\t\t.collect();\n\t\t\t\tself.queue.push(InternalEvent::OpenFuzzyFinder(\n\t\t\t\t\tbranches,\n\t\t\t\t\tFuzzyFinderTarget::Branches,\n\t\t\t\t));\n\t\t\t}\n\t\t}\n\n\t\tOk(EventState::Consumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.visible = false;\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\n\t\tOk(())\n\t}\n}\n\nimpl BranchListPopup {\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\tbranches: Vec::new(),\n\t\t\tlocal: true,\n\t\t\thas_remotes: false,\n\t\t\tvisible: false,\n\t\t\tselection: 0,\n\t\t\tscroll: VerticalScroll::new(),\n\t\t\tqueue: env.queue.clone(),\n\t\t\ttheme: env.theme.clone(),\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\tcurrent_height: Cell::new(0),\n\t\t\trepo: env.repo.clone(),\n\t\t}\n\t}\n\n\tfn move_event(&mut self, e: &KeyEvent) -> Result<EventState> {\n\t\tif key_match(e, self.key_config.keys.exit_popup) {\n\t\t\tself.hide();\n\t\t} else if key_match(e, self.key_config.keys.move_down) {\n\t\t\treturn self\n\t\t\t\t.move_selection(ScrollType::Up)\n\t\t\t\t.map(Into::into);\n\t\t} else if key_match(e, self.key_config.keys.move_up) {\n\t\t\treturn self\n\t\t\t\t.move_selection(ScrollType::Down)\n\t\t\t\t.map(Into::into);\n\t\t} else if key_match(e, self.key_config.keys.page_down) {\n\t\t\treturn self\n\t\t\t\t.move_selection(ScrollType::PageDown)\n\t\t\t\t.map(Into::into);\n\t\t} else if key_match(e, self.key_config.keys.page_up) {\n\t\t\treturn self\n\t\t\t\t.move_selection(ScrollType::PageUp)\n\t\t\t\t.map(Into::into);\n\t\t} else if key_match(e, self.key_config.keys.home) {\n\t\t\treturn self\n\t\t\t\t.move_selection(ScrollType::Home)\n\t\t\t\t.map(Into::into);\n\t\t} else if key_match(e, self.key_config.keys.end) {\n\t\t\treturn self\n\t\t\t\t.move_selection(ScrollType::End)\n\t\t\t\t.map(Into::into);\n\t\t} else if key_match(e, self.key_config.keys.tab_toggle) {\n\t\t\tself.local = !self.local;\n\t\t\tself.check_remotes();\n\t\t\tself.update_branches()?;\n\t\t}\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\t///\n\tpub fn open(&mut self) -> Result<()> {\n\t\tself.show()?;\n\t\tself.update_branches()?;\n\n\t\tOk(())\n\t}\n\n\tpub fn branch_finder_update(&mut self, idx: usize) -> Result<()> {\n\t\tself.set_selection(idx.try_into()?)?;\n\t\tOk(())\n\t}\n\n\tfn check_remotes(&mut self) {\n\t\tif self.visible {\n\t\t\tself.has_remotes =\n\t\t\t\tget_branches_info(&self.repo.borrow(), false)\n\t\t\t\t\t.is_ok_and(|branches| !branches.is_empty());\n\t\t}\n\t}\n\n\t/// fetch list of branches\n\tpub fn update_branches(&mut self) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tself.check_remotes();\n\t\t\tself.branches =\n\t\t\t\tget_branches_info(&self.repo.borrow(), self.local)?;\n\t\t\t//remove remote branch called `HEAD`\n\t\t\tif !self.local {\n\t\t\t\tself.branches\n\t\t\t\t\t.iter()\n\t\t\t\t\t.position(|b| b.name.ends_with(\"/HEAD\"))\n\t\t\t\t\t.map(|idx| self.branches.remove(idx));\n\t\t\t}\n\t\t\tself.set_selection(self.selection)?;\n\t\t}\n\t\tOk(())\n\t}\n\n\t///\n\tpub fn update_git(\n\t\t&mut self,\n\t\tev: AsyncGitNotification,\n\t) -> Result<()> {\n\t\tif self.is_visible() && ev == AsyncGitNotification::Push {\n\t\t\tself.update_branches()?;\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tconst fn valid_selection(&self) -> bool {\n\t\t!self.branches.is_empty()\n\t}\n\n\tfn merge_branch(&mut self) -> Result<()> {\n\t\tif let Some(branch) =\n\t\t\tself.branches.get(usize::from(self.selection))\n\t\t{\n\t\t\tsync::merge_branch(\n\t\t\t\t&self.repo.borrow(),\n\t\t\t\t&branch.name,\n\t\t\t\tself.get_branch_type(),\n\t\t\t)?;\n\n\t\t\tself.hide_and_switch_tab()?;\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn rebase_branch(&mut self) -> Result<()> {\n\t\tif let Some(branch) =\n\t\t\tself.branches.get(usize::from(self.selection))\n\t\t{\n\t\t\tsync::rebase_branch(\n\t\t\t\t&self.repo.borrow(),\n\t\t\t\t&branch.name,\n\t\t\t\tself.get_branch_type(),\n\t\t\t)?;\n\n\t\t\tself.hide_and_switch_tab()?;\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn inspect_head_of_branch(&mut self) {\n\t\tif let Some(commit_id) = self.get_selected_commit() {\n\t\t\tself.hide();\n\t\t\tself.queue.push(InternalEvent::OpenPopup(\n\t\t\t\tStackablePopupOpen::InspectCommit(\n\t\t\t\t\tInspectCommitOpen::new(commit_id),\n\t\t\t\t),\n\t\t\t));\n\t\t}\n\t}\n\n\tconst fn get_branch_type(&self) -> BranchType {\n\t\tif self.local {\n\t\t\tBranchType::Local\n\t\t} else {\n\t\t\tBranchType::Remote\n\t\t}\n\t}\n\n\tfn hide_and_switch_tab(&mut self) -> Result<()> {\n\t\tself.hide();\n\t\tself.queue.push(InternalEvent::Update(NeedsUpdate::ALL));\n\n\t\tif sync::repo_state(&self.repo.borrow())? != RepoState::Clean\n\t\t{\n\t\t\tself.queue.push(InternalEvent::TabSwitchStatus);\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn selection_is_cur_branch(&self) -> bool {\n\t\tself.branches\n\t\t\t.iter()\n\t\t\t.enumerate()\n\t\t\t.filter(|(index, b)| {\n\t\t\t\tb.local_details().is_some_and(|details| {\n\t\t\t\t\tdetails.is_head\n\t\t\t\t\t\t&& *index == self.selection as usize\n\t\t\t\t})\n\t\t\t})\n\t\t\t.count() > 0\n\t}\n\n\t// top commit of selected branch\n\tfn get_selected_commit(&self) -> Option<CommitId> {\n\t\tself.branches\n\t\t\t.get(usize::from(self.selection))\n\t\t\t.map(|b| b.top_commit)\n\t}\n\n\t///\n\tfn move_selection(&mut self, scroll: ScrollType) -> Result<bool> {\n\t\tlet new_selection = match scroll {\n\t\t\tScrollType::Up => self.selection.saturating_add(1),\n\t\t\tScrollType::Down => self.selection.saturating_sub(1),\n\t\t\tScrollType::PageDown => self\n\t\t\t\t.selection\n\t\t\t\t.saturating_add(self.current_height.get()),\n\t\t\tScrollType::PageUp => self\n\t\t\t\t.selection\n\t\t\t\t.saturating_sub(self.current_height.get()),\n\t\t\tScrollType::Home => 0,\n\t\t\tScrollType::End => {\n\t\t\t\tlet num_branches: u16 =\n\t\t\t\t\tself.branches.len().try_into()?;\n\t\t\t\tnum_branches.saturating_sub(1)\n\t\t\t}\n\t\t};\n\n\t\tself.set_selection(new_selection)?;\n\n\t\tOk(true)\n\t}\n\n\tfn set_selection(&mut self, selection: u16) -> Result<()> {\n\t\tlet num_branches: u16 = self.branches.len().try_into()?;\n\t\tlet num_branches = num_branches.saturating_sub(1);\n\n\t\tlet selection = if selection > num_branches {\n\t\t\tnum_branches\n\t\t} else {\n\t\t\tselection\n\t\t};\n\n\t\tself.selection = selection;\n\n\t\tOk(())\n\t}\n\n\t/// Get branches to display\n\tfn get_text(\n\t\t&self,\n\t\ttheme: &SharedTheme,\n\t\twidth_available: u16,\n\t\theight: usize,\n\t) -> Text<'_> {\n\t\tconst UPSTREAM_SYMBOL: char = '\\u{2191}';\n\t\tconst TRACKING_SYMBOL: char = '\\u{2193}';\n\t\tconst HEAD_SYMBOL: char = '*';\n\t\tconst EMPTY_SYMBOL: char = ' ';\n\t\tconst THREE_DOTS: &str = \"...\";\n\t\tconst THREE_DOTS_LENGTH: usize = THREE_DOTS.len(); // \"...\"\n\t\tconst COMMIT_HASH_LENGTH: usize = 8;\n\t\tconst IS_HEAD_STAR_LENGTH: usize = 3; // \"*  \"\n\n\t\tlet branch_name_length: usize =\n\t\t\twidth_available as usize * 40 / 100;\n\t\t// commit message takes up the remaining width\n\t\tlet commit_message_length: usize = (width_available as usize)\n\t\t\t.saturating_sub(COMMIT_HASH_LENGTH)\n\t\t\t.saturating_sub(branch_name_length)\n\t\t\t.saturating_sub(IS_HEAD_STAR_LENGTH)\n\t\t\t.saturating_sub(THREE_DOTS_LENGTH);\n\t\tlet mut txt = Vec::new();\n\n\t\tfor (i, displaybranch) in self\n\t\t\t.branches\n\t\t\t.iter()\n\t\t\t.skip(self.scroll.get_top())\n\t\t\t.take(height)\n\t\t\t.enumerate()\n\t\t{\n\t\t\tlet mut commit_message =\n\t\t\t\tdisplaybranch.top_commit_message.clone();\n\t\t\tif commit_message.len() > commit_message_length {\n\t\t\t\tcommit_message.unicode_truncate(\n\t\t\t\t\tcommit_message_length\n\t\t\t\t\t\t.saturating_sub(THREE_DOTS_LENGTH),\n\t\t\t\t);\n\t\t\t\tcommit_message += THREE_DOTS;\n\t\t\t}\n\n\t\t\tlet mut branch_name = displaybranch.name.clone();\n\t\t\tif branch_name.len()\n\t\t\t\t> branch_name_length.saturating_sub(THREE_DOTS_LENGTH)\n\t\t\t{\n\t\t\t\tbranch_name = branch_name\n\t\t\t\t\t.unicode_truncate(\n\t\t\t\t\t\tbranch_name_length\n\t\t\t\t\t\t\t.saturating_sub(THREE_DOTS_LENGTH),\n\t\t\t\t\t)\n\t\t\t\t\t.0\n\t\t\t\t\t.to_string();\n\t\t\t\tbranch_name += THREE_DOTS;\n\t\t\t}\n\n\t\t\tlet selected = (self.selection as usize\n\t\t\t\t- self.scroll.get_top())\n\t\t\t\t== i;\n\n\t\t\tlet is_head = displaybranch\n\t\t\t\t.local_details()\n\t\t\t\t.is_some_and(|details| details.is_head);\n\t\t\tlet is_head_str =\n\t\t\t\tif is_head { HEAD_SYMBOL } else { EMPTY_SYMBOL };\n\t\t\tlet upstream_tracking_str = match displaybranch.details {\n\t\t\t\tBranchDetails::Local(LocalBranch {\n\t\t\t\t\thas_upstream,\n\t\t\t\t\t..\n\t\t\t\t}) if has_upstream => UPSTREAM_SYMBOL,\n\t\t\t\tBranchDetails::Remote(RemoteBranch {\n\t\t\t\t\thas_tracking,\n\t\t\t\t\t..\n\t\t\t\t}) if has_tracking => TRACKING_SYMBOL,\n\t\t\t\t_ => EMPTY_SYMBOL,\n\t\t\t};\n\n\t\t\tlet span_prefix = Span::styled(\n\t\t\t\tformat!(\"{is_head_str}{upstream_tracking_str} \"),\n\t\t\t\ttheme.commit_author(selected),\n\t\t\t);\n\t\t\tlet span_hash = Span::styled(\n\t\t\t\tformat!(\n\t\t\t\t\t\"{} \",\n\t\t\t\t\tdisplaybranch.top_commit.get_short_string()\n\t\t\t\t),\n\t\t\t\ttheme.commit_hash(selected),\n\t\t\t);\n\t\t\tlet span_msg = Span::styled(\n\t\t\t\tcommit_message.clone(),\n\t\t\t\ttheme.text(true, selected),\n\t\t\t);\n\t\t\tlet span_name = Span::styled(\n\t\t\t\tformat!(\"{branch_name:branch_name_length$} \"),\n\t\t\t\ttheme.branch(selected, is_head),\n\t\t\t);\n\n\t\t\ttxt.push(Line::from(vec![\n\t\t\t\tspan_prefix,\n\t\t\t\tspan_name,\n\t\t\t\tspan_hash,\n\t\t\t\tspan_msg,\n\t\t\t]));\n\t\t}\n\n\t\tText::from(txt)\n\t}\n\n\t///\n\tfn switch_to_selected_branch(&mut self) -> Result<()> {\n\t\tif !self.valid_selection() {\n\t\t\tanyhow::bail!(\"no valid branch selected\");\n\t\t}\n\n\t\tlet status = sync::status::get_status(\n\t\t\t&self.repo.borrow(),\n\t\t\tStatusType::WorkingDir,\n\t\t\tNone,\n\t\t)\n\t\t.expect(\"Could not get status\");\n\n\t\tlet selected_branch = &self.branches[self.selection as usize];\n\t\tif status.is_empty() {\n\t\t\tif self.local {\n\t\t\t\tcheckout_branch(\n\t\t\t\t\t&self.repo.borrow(),\n\t\t\t\t\t&selected_branch.name,\n\t\t\t\t)?;\n\t\t\t\tself.hide();\n\t\t\t} else {\n\t\t\t\tcheckout_remote_branch(\n\t\t\t\t\t&self.repo.borrow(),\n\t\t\t\t\tselected_branch,\n\t\t\t\t)?;\n\t\t\t\tself.local = true;\n\t\t\t\tself.update_branches()?;\n\t\t\t}\n\t\t\tself.queue.push(InternalEvent::Update(NeedsUpdate::ALL));\n\t\t} else {\n\t\t\tself.queue.push(InternalEvent::CheckoutOption(\n\t\t\t\tselected_branch.clone(),\n\t\t\t));\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn draw_tabs(&self, f: &mut Frame, r: Rect) {\n\t\tlet tabs: Vec<Line> =\n\t\t\t[Span::raw(\"Local\"), Span::raw(\"Remote\")]\n\t\t\t\t.iter()\n\t\t\t\t.cloned()\n\t\t\t\t.map(Line::from)\n\t\t\t\t.collect();\n\n\t\tf.render_widget(\n\t\t\tTabs::new(tabs)\n\t\t\t\t.block(\n\t\t\t\t\tBlock::default()\n\t\t\t\t\t\t.borders(Borders::BOTTOM)\n\t\t\t\t\t\t.border_style(self.theme.block(false)),\n\t\t\t\t)\n\t\t\t\t.style(self.theme.tab(false))\n\t\t\t\t.highlight_style(self.theme.tab(true))\n\t\t\t\t.divider(strings::tab_divider(&self.key_config))\n\t\t\t\t.select(if self.local { 0 } else { 1 }),\n\t\t\tr,\n\t\t);\n\t}\n\n\tfn draw_list(&self, f: &mut Frame, r: Rect) -> Result<()> {\n\t\tlet height_in_lines = r.height as usize;\n\t\tself.current_height.set(height_in_lines.try_into()?);\n\n\t\tself.scroll.update(\n\t\t\tself.selection as usize,\n\t\t\tself.branches.len(),\n\t\t\theight_in_lines,\n\t\t);\n\n\t\tf.render_widget(\n\t\t\tParagraph::new(self.get_text(\n\t\t\t\t&self.theme,\n\t\t\t\tr.width,\n\t\t\t\theight_in_lines,\n\t\t\t))\n\t\t\t.alignment(Alignment::Left),\n\t\t\tr,\n\t\t);\n\n\t\tlet mut r = r;\n\t\tr.width += 1;\n\t\tr.height += 2;\n\t\tr.y = r.y.saturating_sub(1);\n\n\t\tself.scroll.draw(f, r, &self.theme);\n\n\t\tOk(())\n\t}\n\n\tfn rename_branch(&self) {\n\t\tlet cur_branch = &self.branches[self.selection as usize];\n\t\tself.queue.push(InternalEvent::RenameBranch(\n\t\t\tcur_branch.reference.clone(),\n\t\t\tcur_branch.name.clone(),\n\t\t));\n\t}\n\n\tfn delete_branch(&self) {\n\t\tlet reference =\n\t\t\tself.branches[self.selection as usize].reference.clone();\n\n\t\tself.queue.push(InternalEvent::ConfirmAction(\n\t\t\tif self.local {\n\t\t\t\tAction::DeleteLocalBranch(reference)\n\t\t\t} else {\n\t\t\t\tAction::DeleteRemoteBranch(reference)\n\t\t\t},\n\t\t));\n\t}\n\n\tfn add_commands_internal(&self, out: &mut Vec<CommandInfo>) {\n\t\tlet selection_is_cur_branch = self.selection_is_cur_branch();\n\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::scroll(&self.key_config),\n\t\t\ttrue,\n\t\t\ttrue,\n\t\t));\n\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::close_popup(&self.key_config),\n\t\t\ttrue,\n\t\t\ttrue,\n\t\t));\n\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::commit_details_open(&self.key_config),\n\t\t\ttrue,\n\t\t\ttrue,\n\t\t));\n\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::compare_with_head(&self.key_config),\n\t\t\t!selection_is_cur_branch,\n\t\t\ttrue,\n\t\t));\n\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::toggle_branch_popup(\n\t\t\t\t&self.key_config,\n\t\t\t\tself.local,\n\t\t\t),\n\t\t\ttrue,\n\t\t\ttrue,\n\t\t));\n\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::select_branch_popup(&self.key_config),\n\t\t\t!selection_is_cur_branch && self.valid_selection(),\n\t\t\ttrue,\n\t\t));\n\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::open_branch_create_popup(\n\t\t\t\t&self.key_config,\n\t\t\t),\n\t\t\ttrue,\n\t\t\tself.local,\n\t\t));\n\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::delete_branch_popup(&self.key_config),\n\t\t\t!selection_is_cur_branch,\n\t\t\ttrue,\n\t\t));\n\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::merge_branch_popup(&self.key_config),\n\t\t\t!selection_is_cur_branch,\n\t\t\ttrue,\n\t\t));\n\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::branch_popup_rebase(&self.key_config),\n\t\t\t!selection_is_cur_branch,\n\t\t\ttrue,\n\t\t));\n\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::rename_branch_popup(&self.key_config),\n\t\t\ttrue,\n\t\t\tself.local,\n\t\t));\n\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::fetch_remotes(&self.key_config),\n\t\t\tself.has_remotes,\n\t\t\ttrue,\n\t\t));\n\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::find_branch(&self.key_config),\n\t\t\ttrue,\n\t\t\ttrue,\n\t\t));\n\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::reset_branch(&self.key_config),\n\t\t\tself.valid_selection(),\n\t\t\ttrue,\n\t\t));\n\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::view_remotes(&self.key_config),\n\t\t\ttrue,\n\t\t\tself.has_remotes,\n\t\t));\n\t}\n}\n"
  },
  {
    "path": "src/popups/checkout_option.rs",
    "content": "use crate::components::{\n\tvisibility_blocking, CommandBlocking, CommandInfo, Component,\n\tDrawableComponent, EventState,\n};\nuse crate::queue::{InternalEvent, NeedsUpdate};\nuse crate::strings::CheckoutOptions;\nuse crate::try_or_popup;\nuse crate::{\n\tapp::Environment,\n\tkeys::{key_match, SharedKeyConfig},\n\tqueue::Queue,\n\tstrings,\n\tui::{self, style::SharedTheme},\n};\nuse anyhow::{Ok, Result};\nuse asyncgit::sync::branch::checkout_remote_branch;\nuse asyncgit::sync::status::discard_status;\nuse asyncgit::sync::{checkout_branch, BranchInfo, RepoPath};\nuse crossterm::event::Event;\nuse ratatui::{\n\tlayout::{Alignment, Rect},\n\ttext::{Line, Span},\n\twidgets::{Block, Borders, Clear, Paragraph},\n\tFrame,\n};\n\npub struct CheckoutOptionPopup {\n\tqueue: Queue,\n\trepo: RepoPath,\n\tbranch: Option<BranchInfo>,\n\toption: CheckoutOptions,\n\tvisible: bool,\n\tkey_config: SharedKeyConfig,\n\ttheme: SharedTheme,\n}\n\nimpl CheckoutOptionPopup {\n\t///\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\tqueue: env.queue.clone(),\n\t\t\trepo: env.repo.borrow().clone(),\n\t\t\tbranch: None,\n\t\t\toption: CheckoutOptions::KeepLocalChanges,\n\t\t\tvisible: false,\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\ttheme: env.theme.clone(),\n\t\t}\n\t}\n\n\tfn get_text(&self, _width: u16) -> Vec<Line<'_>> {\n\t\tlet mut txt: Vec<Line> = Vec::with_capacity(10);\n\n\t\ttxt.push(Line::from(vec![\n\t\t\tSpan::styled(\n\t\t\t\tString::from(\"Switch to: \"),\n\t\t\t\tself.theme.text(true, false),\n\t\t\t),\n\t\t\tSpan::styled(\n\t\t\t\tself.branch.as_ref().expect(\"No branch\").name.clone(),\n\t\t\t\tself.theme.commit_hash(false),\n\t\t\t),\n\t\t]));\n\n\t\tlet (kind_name, kind_desc) = self.option.to_string_pair();\n\n\t\ttxt.push(Line::from(vec![\n\t\t\tSpan::styled(\n\t\t\t\tString::from(\"How: \"),\n\t\t\t\tself.theme.text(true, false),\n\t\t\t),\n\t\t\tSpan::styled(kind_name, self.theme.text(true, true)),\n\t\t\tSpan::styled(kind_desc, self.theme.text(true, false)),\n\t\t]));\n\n\t\ttxt\n\t}\n\n\t///\n\tpub fn open(&mut self, branch: BranchInfo) -> Result<()> {\n\t\tself.show()?;\n\n\t\tself.branch = Some(branch);\n\n\t\tOk(())\n\t}\n\n\tfn checkout(&self) -> Result<()> {\n\t\tif let Some(branch) = &self.branch {\n\t\t\tif branch.is_local() {\n\t\t\t\tcheckout_branch(&self.repo, &branch.name)?;\n\t\t\t} else {\n\t\t\t\tcheckout_remote_branch(&self.repo, branch)?;\n\t\t\t}\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn handle_event(&mut self) -> Result<()> {\n\t\tmatch self.option {\n\t\t\tCheckoutOptions::KeepLocalChanges => {\n\t\t\t\tself.checkout()?;\n\t\t\t}\n\t\t\tCheckoutOptions::DiscardAllLocalChagnes => {\n\t\t\t\tdiscard_status(&self.repo)?;\n\t\t\t\tself.checkout()?;\n\t\t\t}\n\t\t}\n\n\t\tself.queue.push(InternalEvent::Update(NeedsUpdate::ALL));\n\t\tself.queue.push(InternalEvent::SelectBranch);\n\t\tself.hide();\n\n\t\tOk(())\n\t}\n\n\tconst fn change_kind(&mut self, incr: bool) {\n\t\tself.option = if incr {\n\t\t\tself.option.next()\n\t\t} else {\n\t\t\tself.option.previous()\n\t\t};\n\t}\n}\n\nimpl DrawableComponent for CheckoutOptionPopup {\n\tfn draw(&self, f: &mut Frame, area: Rect) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tconst SIZE: (u16, u16) = (55, 4);\n\t\t\tlet area =\n\t\t\t\tui::centered_rect_absolute(SIZE.0, SIZE.1, area);\n\n\t\t\tlet width = area.width;\n\n\t\t\tf.render_widget(Clear, area);\n\t\t\tf.render_widget(\n\t\t\t\tParagraph::new(self.get_text(width))\n\t\t\t\t\t.block(\n\t\t\t\t\t\tBlock::default()\n\t\t\t\t\t\t\t.borders(Borders::ALL)\n\t\t\t\t\t\t\t.title(Span::styled(\n\t\t\t\t\t\t\t\t\"Checkout options\",\n\t\t\t\t\t\t\t\tself.theme.title(true),\n\t\t\t\t\t\t\t))\n\t\t\t\t\t\t\t.border_style(self.theme.block(true)),\n\t\t\t\t\t)\n\t\t\t\t\t.alignment(Alignment::Left),\n\t\t\t\tarea,\n\t\t\t);\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for CheckoutOptionPopup {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.is_visible() || force_all {\n\t\t\tout.push(\n\t\t\t\tCommandInfo::new(\n\t\t\t\t\tstrings::commands::close_popup(&self.key_config),\n\t\t\t\t\ttrue,\n\t\t\t\t\ttrue,\n\t\t\t\t)\n\t\t\t\t.order(1),\n\t\t\t);\n\n\t\t\tout.push(\n\t\t\t\tCommandInfo::new(\n\t\t\t\t\tstrings::commands::reset_commit(&self.key_config),\n\t\t\t\t\ttrue,\n\t\t\t\t\ttrue,\n\t\t\t\t)\n\t\t\t\t.order(1),\n\t\t\t);\n\n\t\t\tout.push(\n\t\t\t\tCommandInfo::new(\n\t\t\t\t\tstrings::commands::reset_type(&self.key_config),\n\t\t\t\t\ttrue,\n\t\t\t\t\ttrue,\n\t\t\t\t)\n\t\t\t\t.order(1),\n\t\t\t);\n\t\t}\n\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(\n\t\t&mut self,\n\t\tevent: &crossterm::event::Event,\n\t) -> Result<EventState> {\n\t\tif self.is_visible() {\n\t\t\tif let Event::Key(key) = &event {\n\t\t\t\tif key_match(key, self.key_config.keys.exit_popup) {\n\t\t\t\t\tself.hide();\n\t\t\t\t} else if key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.move_down,\n\t\t\t\t) {\n\t\t\t\t\tself.change_kind(true);\n\t\t\t\t} else if key_match(key, self.key_config.keys.move_up)\n\t\t\t\t{\n\t\t\t\t\tself.change_kind(false);\n\t\t\t\t} else if key_match(key, self.key_config.keys.enter) {\n\t\t\t\t\ttry_or_popup!(\n\t\t\t\t\t\tself,\n\t\t\t\t\t\t\"checkout error:\",\n\t\t\t\t\t\tself.handle_event()\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn Ok(EventState::Consumed);\n\t\t}\n\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.visible = false;\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "src/popups/commit.rs",
    "content": "use crate::components::{\n\tvisibility_blocking, CommandBlocking, CommandInfo, Component,\n\tDrawableComponent, EventState, TextInputComponent,\n};\nuse crate::{\n\tapp::Environment,\n\tkeys::{key_match, SharedKeyConfig},\n\toptions::SharedOptions,\n\tqueue::{InternalEvent, NeedsUpdate, Queue},\n\tstrings, try_or_popup,\n\tui::style::SharedTheme,\n};\nuse anyhow::{bail, Ok, Result};\nuse asyncgit::sync::commit::commit_message_prettify;\nuse asyncgit::{\n\tcached,\n\tsync::{\n\t\tself, get_config_string, CommitId, HookResult,\n\t\tPrepareCommitMsgSource, RepoPathRef, RepoState,\n\t},\n\tStatusItem, StatusItemType,\n};\nuse crossterm::event::Event;\nuse easy_cast::Cast;\nuse ratatui::{\n\tlayout::{Alignment, Rect},\n\twidgets::Paragraph,\n\tFrame,\n};\n\nuse std::{\n\tfmt::Write as _,\n\tfs::{read_to_string, File},\n\tio::{Read, Write},\n\tpath::PathBuf,\n\tstr::FromStr,\n};\n\nuse super::ExternalEditorPopup;\n\nenum CommitResult {\n\tCommitDone,\n\tAborted,\n}\n\nenum Mode {\n\tNormal,\n\tAmend(CommitId),\n\tMerge(Vec<CommitId>),\n\tRevert,\n\tReword(CommitId),\n}\n\npub struct CommitPopup {\n\trepo: RepoPathRef,\n\tinput: TextInputComponent,\n\tmode: Mode,\n\tqueue: Queue,\n\tkey_config: SharedKeyConfig,\n\tgit_branch_name: cached::BranchName,\n\tcommit_template: Option<String>,\n\ttheme: SharedTheme,\n\tcommit_msg_history_idx: usize,\n\toptions: SharedOptions,\n\tverify: bool,\n}\n\nconst FIRST_LINE_LIMIT: usize = 50;\n\nimpl CommitPopup {\n\t///\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\tqueue: env.queue.clone(),\n\t\t\tmode: Mode::Normal,\n\t\t\tinput: TextInputComponent::new(\n\t\t\t\tenv,\n\t\t\t\t\"\",\n\t\t\t\t&strings::commit_msg(&env.key_config),\n\t\t\t\ttrue,\n\t\t\t),\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\tgit_branch_name: cached::BranchName::new(\n\t\t\t\tenv.repo.clone(),\n\t\t\t),\n\t\t\tcommit_template: None,\n\t\t\ttheme: env.theme.clone(),\n\t\t\trepo: env.repo.clone(),\n\t\t\tcommit_msg_history_idx: 0,\n\t\t\toptions: env.options.clone(),\n\t\t\tverify: true,\n\t\t}\n\t}\n\n\t///\n\tpub fn update(&mut self) {\n\t\tself.git_branch_name.lookup().ok();\n\t}\n\n\tfn draw_branch_name(&self, f: &mut Frame) {\n\t\tif let Some(name) = self.git_branch_name.last() {\n\t\t\tlet w = Paragraph::new(format!(\"{{{name}}}\"))\n\t\t\t\t.alignment(Alignment::Right);\n\n\t\t\tlet rect = {\n\t\t\t\tlet mut rect = self.input.get_area();\n\t\t\t\trect.height = 1;\n\t\t\t\trect.width = rect.width.saturating_sub(1);\n\t\t\t\trect\n\t\t\t};\n\n\t\t\tf.render_widget(w, rect);\n\t\t}\n\t}\n\n\tfn draw_warnings(&self, f: &mut Frame) {\n\t\tlet first_line = self\n\t\t\t.input\n\t\t\t.get_text()\n\t\t\t.lines()\n\t\t\t.next()\n\t\t\t.map(str::len)\n\t\t\t.unwrap_or_default();\n\n\t\tif first_line > FIRST_LINE_LIMIT {\n\t\t\tlet msg = strings::commit_first_line_warning(first_line);\n\t\t\tlet msg_length: u16 = msg.len().cast();\n\t\t\tlet w =\n\t\t\t\tParagraph::new(msg).style(self.theme.text_danger());\n\n\t\t\tlet rect = {\n\t\t\t\tlet mut rect = self.input.get_area();\n\t\t\t\trect.y += rect.height.saturating_sub(1);\n\t\t\t\trect.height = 1;\n\t\t\t\tlet offset =\n\t\t\t\t\trect.width.saturating_sub(msg_length + 1);\n\t\t\t\trect.width = rect.width.saturating_sub(offset + 1);\n\t\t\t\trect.x += offset;\n\n\t\t\t\trect\n\t\t\t};\n\n\t\t\tf.render_widget(w, rect);\n\t\t}\n\t}\n\n\tconst fn item_status_char(\n\t\titem_type: StatusItemType,\n\t) -> &'static str {\n\t\tmatch item_type {\n\t\t\tStatusItemType::Modified => \"modified\",\n\t\t\tStatusItemType::New => \"new file\",\n\t\t\tStatusItemType::Deleted => \"deleted\",\n\t\t\tStatusItemType::Renamed => \"renamed\",\n\t\t\tStatusItemType::Typechange => \" \",\n\t\t\tStatusItemType::Conflicted => \"conflicted\",\n\t\t}\n\t}\n\n\tpub fn show_editor(\n\t\t&mut self,\n\t\tchanges: Vec<StatusItem>,\n\t) -> Result<()> {\n\t\tlet file_path = sync::repo_dir(&self.repo.borrow())?\n\t\t\t.join(\"COMMIT_EDITMSG\");\n\n\t\t{\n\t\t\tlet mut file = File::create(&file_path)?;\n\t\t\tfile.write_fmt(format_args!(\n\t\t\t\t\"{}\\n\",\n\t\t\t\tself.input.get_text()\n\t\t\t))?;\n\t\t\tfile.write_all(\n\t\t\t\tstrings::commit_editor_msg(&self.key_config)\n\t\t\t\t\t.as_bytes(),\n\t\t\t)?;\n\n\t\t\tfile.write_all(b\"\\n#\\n# Changes to be committed:\")?;\n\n\t\t\tfor change in changes {\n\t\t\t\tlet status_char =\n\t\t\t\t\tSelf::item_status_char(change.status);\n\t\t\t\tlet message =\n\t\t\t\t\tformat!(\"\\n#\\t{status_char}: {}\", change.path);\n\t\t\t\tfile.write_all(message.as_bytes())?;\n\t\t\t}\n\t\t}\n\n\t\tExternalEditorPopup::open_file_in_editor(\n\t\t\t&self.repo.borrow(),\n\t\t\t&file_path,\n\t\t)?;\n\n\t\tlet mut message = String::new();\n\n\t\tlet mut file = File::open(&file_path)?;\n\t\tfile.read_to_string(&mut message)?;\n\t\tdrop(file);\n\t\tstd::fs::remove_file(&file_path)?;\n\n\t\tmessage =\n\t\t\tcommit_message_prettify(&self.repo.borrow(), message)?;\n\t\tself.input.set_text(message);\n\t\tself.input.show()?;\n\n\t\tOk(())\n\t}\n\n\tfn commit(&mut self) -> Result<()> {\n\t\tlet msg = self.input.get_text().to_string();\n\n\t\tif matches!(\n\t\t\tself.commit_with_msg(msg)?,\n\t\t\tCommitResult::CommitDone\n\t\t) {\n\t\t\tself.options\n\t\t\t\t.borrow_mut()\n\t\t\t\t.add_commit_msg(self.input.get_text());\n\t\t\tself.commit_msg_history_idx = 0;\n\n\t\t\tself.hide();\n\t\t\tself.queue.push(InternalEvent::Update(NeedsUpdate::ALL));\n\t\t\tself.queue.push(InternalEvent::StatusLastFileMoved);\n\t\t\tself.input.clear();\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn commit_with_msg(\n\t\t&mut self,\n\t\tmsg: String,\n\t) -> Result<CommitResult> {\n\t\t// on exit verify should always be on\n\t\tlet verify = self.verify;\n\t\tself.verify = true;\n\n\t\tif verify {\n\t\t\t// run pre commit hook - can reject commit\n\t\t\tif let HookResult::NotOk(e) =\n\t\t\t\tsync::hooks_pre_commit(&self.repo.borrow())?\n\t\t\t{\n\t\t\t\tlog::error!(\"pre-commit hook error: {e}\");\n\t\t\t\tself.queue.push(InternalEvent::ShowErrorMsg(\n\t\t\t\t\tformat!(\"pre-commit hook error:\\n{e}\"),\n\t\t\t\t));\n\t\t\t\treturn Ok(CommitResult::Aborted);\n\t\t\t}\n\t\t}\n\n\t\tlet mut msg =\n\t\t\tcommit_message_prettify(&self.repo.borrow(), msg)?;\n\n\t\tif verify {\n\t\t\t// run commit message check hook - can reject commit\n\t\t\tif let HookResult::NotOk(e) =\n\t\t\t\tsync::hooks_commit_msg(&self.repo.borrow(), &mut msg)?\n\t\t\t{\n\t\t\t\tlog::error!(\"commit-msg hook error: {e}\");\n\t\t\t\tself.queue.push(InternalEvent::ShowErrorMsg(\n\t\t\t\t\tformat!(\"commit-msg hook error:\\n{e}\"),\n\t\t\t\t));\n\t\t\t\treturn Ok(CommitResult::Aborted);\n\t\t\t}\n\t\t}\n\t\tself.do_commit(&msg)?;\n\n\t\tif let HookResult::NotOk(e) =\n\t\t\tsync::hooks_post_commit(&self.repo.borrow())?\n\t\t{\n\t\t\tlog::error!(\"post-commit hook error: {e}\");\n\t\t\tself.queue.push(InternalEvent::ShowErrorMsg(format!(\n\t\t\t\t\"post-commit hook error:\\n{e}\"\n\t\t\t)));\n\t\t}\n\n\t\tOk(CommitResult::CommitDone)\n\t}\n\n\tfn do_commit(&self, msg: &str) -> Result<()> {\n\t\tmatch &self.mode {\n\t\t\tMode::Normal => sync::commit(&self.repo.borrow(), msg)?,\n\t\t\tMode::Amend(amend) => {\n\t\t\t\tsync::amend(&self.repo.borrow(), *amend, msg)?\n\t\t\t}\n\t\t\tMode::Merge(ids) => {\n\t\t\t\tsync::merge_commit(&self.repo.borrow(), msg, ids)?\n\t\t\t}\n\t\t\tMode::Revert => {\n\t\t\t\tsync::commit_revert(&self.repo.borrow(), msg)?\n\t\t\t}\n\t\t\tMode::Reword(id) => {\n\t\t\t\tlet commit =\n\t\t\t\t\tsync::reword(&self.repo.borrow(), *id, msg)?;\n\t\t\t\tself.queue.push(InternalEvent::TabSwitchStatus);\n\n\t\t\t\tcommit\n\t\t\t}\n\t\t};\n\t\tOk(())\n\t}\n\n\tfn can_commit(&self) -> bool {\n\t\t!self.is_empty() && self.is_changed()\n\t}\n\n\tfn can_amend(&self) -> bool {\n\t\tmatches!(self.mode, Mode::Normal)\n\t\t\t&& sync::get_head(&self.repo.borrow()).is_ok()\n\t\t\t&& (self.is_empty() || !self.is_changed())\n\t}\n\n\tfn is_empty(&self) -> bool {\n\t\tself.input.get_text().is_empty()\n\t}\n\n\tfn is_changed(&self) -> bool {\n\t\tSome(self.input.get_text().trim())\n\t\t\t!= self.commit_template.as_ref().map(|s| s.trim())\n\t}\n\n\tfn amend(&mut self) -> Result<()> {\n\t\tif self.can_amend() {\n\t\t\tlet id = sync::get_head(&self.repo.borrow())?;\n\t\t\tself.mode = Mode::Amend(id);\n\n\t\t\tlet details =\n\t\t\t\tsync::get_commit_details(&self.repo.borrow(), id)?;\n\n\t\t\tself.input.set_title(strings::commit_title_amend());\n\n\t\t\tif let Some(msg) = details.message {\n\t\t\t\tself.input.set_text(msg.combine());\n\t\t\t}\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn signoff_commit(&mut self) {\n\t\tlet msg = self.input.get_text();\n\t\tlet signed_msg = self.add_sign_off(msg);\n\t\tif let std::result::Result::Ok(signed_msg) = signed_msg {\n\t\t\tself.input.set_text(signed_msg);\n\t\t}\n\t}\n\n\tconst fn toggle_verify(&mut self) {\n\t\tself.verify = !self.verify;\n\t}\n\n\tpub fn open(&mut self, reword: Option<CommitId>) -> Result<()> {\n\t\t//only clear text if it was not a normal commit dlg before, so to preserve old commit msg that was edited\n\t\tif !matches!(self.mode, Mode::Normal) {\n\t\t\tself.input.clear();\n\t\t}\n\n\t\tself.mode = Mode::Normal;\n\n\t\tlet repo_state = sync::repo_state(&self.repo.borrow())?;\n\n\t\tlet (mode, msg_source) = if repo_state != RepoState::Clean\n\t\t\t&& reword.is_some()\n\t\t{\n\t\t\tbail!(\"cannot reword while repo is not in a clean state\");\n\t\t} else if let Some(reword_id) = reword {\n\t\t\tself.input.set_text(\n\t\t\t\tsync::get_commit_details(\n\t\t\t\t\t&self.repo.borrow(),\n\t\t\t\t\treword_id,\n\t\t\t\t)?\n\t\t\t\t.message\n\t\t\t\t.unwrap_or_default()\n\t\t\t\t.combine(),\n\t\t\t);\n\t\t\tself.input.set_title(strings::commit_reword_title());\n\t\t\t(Mode::Reword(reword_id), PrepareCommitMsgSource::Message)\n\t\t} else {\n\t\t\tmatch repo_state {\n\t\t\t\tRepoState::Merge => {\n\t\t\t\t\tlet ids =\n\t\t\t\t\t\tsync::mergehead_ids(&self.repo.borrow())?;\n\t\t\t\t\tself.input\n\t\t\t\t\t\t.set_title(strings::commit_title_merge());\n\t\t\t\t\tself.input.set_text(sync::merge_msg(\n\t\t\t\t\t\t&self.repo.borrow(),\n\t\t\t\t\t)?);\n\t\t\t\t\t(Mode::Merge(ids), PrepareCommitMsgSource::Merge)\n\t\t\t\t}\n\t\t\t\tRepoState::Revert => {\n\t\t\t\t\tself.input\n\t\t\t\t\t\t.set_title(strings::commit_title_revert());\n\t\t\t\t\tself.input.set_text(sync::merge_msg(\n\t\t\t\t\t\t&self.repo.borrow(),\n\t\t\t\t\t)?);\n\t\t\t\t\t(Mode::Revert, PrepareCommitMsgSource::Message)\n\t\t\t\t}\n\n\t\t\t\t_ => {\n\t\t\t\t\tself.commit_template = get_config_string(\n\t\t\t\t\t\t&self.repo.borrow(),\n\t\t\t\t\t\t\"commit.template\",\n\t\t\t\t\t)\n\t\t\t\t\t.map_err(|e| {\n\t\t\t\t\t\tlog::error!(\"load git-config failed: {e}\");\n\t\t\t\t\t\te\n\t\t\t\t\t})\n\t\t\t\t\t.ok()\n\t\t\t\t\t.flatten()\n\t\t\t\t\t.and_then(|path| {\n\t\t\t\t\t\tshellexpand::full(path.as_str())\n\t\t\t\t\t\t\t.ok()\n\t\t\t\t\t\t\t.and_then(|path| {\n\t\t\t\t\t\t\t\tPathBuf::from_str(path.as_ref()).ok()\n\t\t\t\t\t\t\t})\n\t\t\t\t\t})\n\t\t\t\t\t.and_then(|path| {\n\t\t\t\t\t\tread_to_string(&path)\n\t\t\t\t\t\t\t.map_err(|e| {\n\t\t\t\t\t\t\t\tlog::error!(\"read commit.template failed: {e} (path: '{path:?}')\");\n\t\t\t\t\t\t\t\te\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t.ok()\n\t\t\t\t\t});\n\n\t\t\t\t\tlet msg_source = if self.is_empty() {\n\t\t\t\t\t\tif let Some(s) = &self.commit_template {\n\t\t\t\t\t\t\tself.input.set_text(s.clone());\n\t\t\t\t\t\t\tPrepareCommitMsgSource::Template\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tPrepareCommitMsgSource::Message\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tPrepareCommitMsgSource::Message\n\t\t\t\t\t};\n\t\t\t\t\tself.input.set_title(strings::commit_title());\n\n\t\t\t\t\t(Mode::Normal, msg_source)\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\tself.mode = mode;\n\n\t\tlet mut msg = self.input.get_text().to_string();\n\t\tif let HookResult::NotOk(e) = sync::hooks_prepare_commit_msg(\n\t\t\t&self.repo.borrow(),\n\t\t\tmsg_source,\n\t\t\t&mut msg,\n\t\t)? {\n\t\t\tlog::error!(\"prepare-commit-msg hook rejection: {e}\");\n\t\t}\n\t\tself.input.set_text(msg);\n\n\t\tself.commit_msg_history_idx = 0;\n\t\tself.input.show()?;\n\n\t\tOk(())\n\t}\n\n\tfn add_sign_off(&self, msg: &str) -> Result<String> {\n\t\tconst CONFIG_KEY_USER_NAME: &str = \"user.name\";\n\t\tconst CONFIG_KEY_USER_MAIL: &str = \"user.email\";\n\n\t\tlet user = get_config_string(\n\t\t\t&self.repo.borrow(),\n\t\t\tCONFIG_KEY_USER_NAME,\n\t\t)?;\n\n\t\tlet mail = get_config_string(\n\t\t\t&self.repo.borrow(),\n\t\t\tCONFIG_KEY_USER_MAIL,\n\t\t)?;\n\n\t\tlet mut msg = msg.to_owned();\n\t\tif let (Some(user), Some(mail)) = (user, mail) {\n\t\t\tlet _ = write!(msg, \"\\n\\nSigned-off-by: {user} <{mail}>\");\n\t\t}\n\n\t\tOk(msg)\n\t}\n}\n\nimpl DrawableComponent for CommitPopup {\n\tfn draw(&self, f: &mut Frame, rect: Rect) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tself.input.draw(f, rect)?;\n\t\t\tself.draw_branch_name(f);\n\t\t\tself.draw_warnings(f);\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for CommitPopup {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tself.input.commands(out, force_all);\n\n\t\tif self.is_visible() || force_all {\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::commit_submit(&self.key_config),\n\t\t\t\tself.can_commit(),\n\t\t\t\ttrue,\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::toggle_verify(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t\tself.verify,\n\t\t\t\t),\n\t\t\t\tself.can_commit(),\n\t\t\t\ttrue,\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::commit_amend(&self.key_config),\n\t\t\t\tself.can_amend(),\n\t\t\t\ttrue,\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::commit_signoff(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\ttrue,\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::commit_open_editor(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t),\n\t\t\t\ttrue,\n\t\t\t\ttrue,\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::commit_next_msg_from_history(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t),\n\t\t\t\tself.options.borrow().has_commit_msg_history(),\n\t\t\t\ttrue,\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::newline(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\ttrue,\n\t\t\t));\n\t\t}\n\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(&mut self, ev: &Event) -> Result<EventState> {\n\t\tif self.is_visible() {\n\t\t\tif let Event::Key(e) = ev {\n\t\t\t\tlet input_consumed =\n\t\t\t\t\tif key_match(e, self.key_config.keys.commit)\n\t\t\t\t\t\t&& self.can_commit()\n\t\t\t\t\t{\n\t\t\t\t\t\ttry_or_popup!(\n\t\t\t\t\t\t\tself,\n\t\t\t\t\t\t\t\"commit error:\",\n\t\t\t\t\t\t\tself.commit()\n\t\t\t\t\t\t);\n\t\t\t\t\t\ttrue\n\t\t\t\t\t} else if key_match(\n\t\t\t\t\t\te,\n\t\t\t\t\t\tself.key_config.keys.toggle_verify,\n\t\t\t\t\t) && self.can_commit()\n\t\t\t\t\t{\n\t\t\t\t\t\tself.toggle_verify();\n\t\t\t\t\t\ttrue\n\t\t\t\t\t} else if key_match(\n\t\t\t\t\t\te,\n\t\t\t\t\t\tself.key_config.keys.commit_amend,\n\t\t\t\t\t) && self.can_amend()\n\t\t\t\t\t{\n\t\t\t\t\t\tself.amend()?;\n\t\t\t\t\t\ttrue\n\t\t\t\t\t} else if key_match(\n\t\t\t\t\t\te,\n\t\t\t\t\t\tself.key_config.keys.open_commit_editor,\n\t\t\t\t\t) {\n\t\t\t\t\t\tself.queue.push(\n\t\t\t\t\t\t\tInternalEvent::OpenExternalEditor(None),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tself.hide();\n\t\t\t\t\t\ttrue\n\t\t\t\t\t} else if key_match(\n\t\t\t\t\t\te,\n\t\t\t\t\t\tself.key_config.keys.commit_history_next,\n\t\t\t\t\t) {\n\t\t\t\t\t\tif let Some(msg) = self\n\t\t\t\t\t\t\t.options\n\t\t\t\t\t\t\t.borrow()\n\t\t\t\t\t\t\t.commit_msg(self.commit_msg_history_idx)\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tself.input.set_text(msg);\n\t\t\t\t\t\t\tself.commit_msg_history_idx += 1;\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttrue\n\t\t\t\t\t} else if key_match(\n\t\t\t\t\t\te,\n\t\t\t\t\t\tself.key_config.keys.toggle_signoff,\n\t\t\t\t\t) {\n\t\t\t\t\t\tself.signoff_commit();\n\t\t\t\t\t\ttrue\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfalse\n\t\t\t\t\t};\n\n\t\t\t\tif !input_consumed {\n\t\t\t\t\tself.input.event(ev)?;\n\t\t\t\t}\n\n\t\t\t\t// stop key event propagation\n\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t}\n\t\t}\n\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.input.is_visible()\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.input.hide();\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.open(None)?;\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "src/popups/compare_commits.rs",
    "content": "use crate::components::{\n\tcommand_pump, event_pump, visibility_blocking, CommandBlocking,\n\tCommandInfo, CommitDetailsComponent, Component, DiffComponent,\n\tDrawableComponent, EventState,\n};\nuse crate::{\n\taccessors,\n\tapp::Environment,\n\tkeys::{key_match, SharedKeyConfig},\n\toptions::SharedOptions,\n\tpopups::InspectCommitOpen,\n\tqueue::{InternalEvent, Queue, StackablePopupOpen},\n\tstrings,\n};\nuse anyhow::Result;\nuse asyncgit::{\n\tsync::{self, commit_files::OldNew, CommitId, RepoPathRef},\n\tAsyncDiff, AsyncGitNotification, CommitFilesParams, DiffParams,\n\tDiffType,\n};\nuse crossterm::event::Event;\nuse ratatui::{\n\tlayout::{Constraint, Direction, Layout, Rect},\n\twidgets::Clear,\n\tFrame,\n};\n\npub struct CompareCommitsPopup {\n\trepo: RepoPathRef,\n\topen_request: Option<InspectCommitOpen>,\n\tdiff: DiffComponent,\n\tdetails: CommitDetailsComponent,\n\tgit_diff: AsyncDiff,\n\tvisible: bool,\n\tkey_config: SharedKeyConfig,\n\tqueue: Queue,\n\toptions: SharedOptions,\n}\n\nimpl DrawableComponent for CompareCommitsPopup {\n\tfn draw(&self, f: &mut Frame, rect: Rect) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tlet percentages = if self.diff.focused() {\n\t\t\t\t(0, 100)\n\t\t\t} else {\n\t\t\t\t(50, 50)\n\t\t\t};\n\n\t\t\tlet chunks = Layout::default()\n\t\t\t\t.direction(Direction::Horizontal)\n\t\t\t\t.constraints(\n\t\t\t\t\t[\n\t\t\t\t\t\tConstraint::Percentage(percentages.0),\n\t\t\t\t\t\tConstraint::Percentage(percentages.1),\n\t\t\t\t\t]\n\t\t\t\t\t.as_ref(),\n\t\t\t\t)\n\t\t\t\t.split(rect);\n\n\t\t\tf.render_widget(Clear, rect);\n\n\t\t\tself.details.draw(f, chunks[0])?;\n\t\t\tself.diff.draw(f, chunks[1])?;\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for CompareCommitsPopup {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.is_visible() || force_all {\n\t\t\tcommand_pump(\n\t\t\t\tout,\n\t\t\t\tforce_all,\n\t\t\t\tself.components().as_slice(),\n\t\t\t);\n\n\t\t\tout.push(\n\t\t\t\tCommandInfo::new(\n\t\t\t\t\tstrings::commands::close_popup(&self.key_config),\n\t\t\t\t\ttrue,\n\t\t\t\t\ttrue,\n\t\t\t\t)\n\t\t\t\t.order(1),\n\t\t\t);\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::diff_focus_right(&self.key_config),\n\t\t\t\tself.can_focus_diff(),\n\t\t\t\t!self.diff.focused() || force_all,\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::diff_focus_left(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\tself.diff.focused() || force_all,\n\t\t\t));\n\t\t}\n\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(&mut self, ev: &Event) -> Result<EventState> {\n\t\tif self.is_visible() {\n\t\t\tif event_pump(ev, self.components_mut().as_mut_slice())?\n\t\t\t\t.is_consumed()\n\t\t\t{\n\t\t\t\tif !self.details.is_visible() {\n\t\t\t\t\tself.hide_stacked(true);\n\t\t\t\t}\n\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t}\n\n\t\t\tif let Event::Key(e) = ev {\n\t\t\t\tif key_match(e, self.key_config.keys.exit_popup) {\n\t\t\t\t\tif self.diff.focused() {\n\t\t\t\t\t\tself.details.focus(true);\n\t\t\t\t\t\tself.diff.focus(false);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tself.hide_stacked(false);\n\t\t\t\t\t}\n\t\t\t\t} else if key_match(\n\t\t\t\t\te,\n\t\t\t\t\tself.key_config.keys.move_right,\n\t\t\t\t) && self.can_focus_diff()\n\t\t\t\t{\n\t\t\t\t\tself.details.focus(false);\n\t\t\t\t\tself.diff.focus(true);\n\t\t\t\t} else if key_match(e, self.key_config.keys.move_left)\n\t\t\t\t{\n\t\t\t\t\tself.hide_stacked(false);\n\t\t\t\t}\n\n\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t}\n\t\t}\n\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\tfn hide(&mut self) {\n\t\tself.visible = false;\n\t}\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\t\tself.details.show()?;\n\t\tself.details.focus(true);\n\t\tself.diff.focus(false);\n\t\tself.update()?;\n\t\tOk(())\n\t}\n}\n\nimpl CompareCommitsPopup {\n\taccessors!(self, [diff, details]);\n\n\t///\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\trepo: env.repo.clone(),\n\t\t\tdetails: CommitDetailsComponent::new(env),\n\t\t\tdiff: DiffComponent::new(env, true),\n\t\t\topen_request: None,\n\t\t\tgit_diff: AsyncDiff::new(\n\t\t\t\tenv.repo.borrow().clone(),\n\t\t\t\t&env.sender_git,\n\t\t\t),\n\t\t\tvisible: false,\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\tqueue: env.queue.clone(),\n\t\t\toptions: env.options.clone(),\n\t\t}\n\t}\n\n\t///\n\tpub fn open(&mut self, open: InspectCommitOpen) -> Result<()> {\n\t\tlet compare_id = if let Some(compare_id) = open.compare_id {\n\t\t\tcompare_id\n\t\t} else {\n\t\t\tsync::get_head_tuple(&self.repo.borrow())?.id\n\t\t};\n\t\tself.open_request = Some(InspectCommitOpen {\n\t\t\tcommit_id: open.commit_id,\n\t\t\tcompare_id: Some(compare_id),\n\t\t\ttags: open.tags,\n\t\t});\n\t\tself.show()?;\n\n\t\tOk(())\n\t}\n\n\t///\n\tpub fn any_work_pending(&self) -> bool {\n\t\tself.git_diff.is_pending() || self.details.any_work_pending()\n\t}\n\n\t///\n\tpub fn update_git(\n\t\t&mut self,\n\t\tev: AsyncGitNotification,\n\t) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tif ev == AsyncGitNotification::CommitFiles {\n\t\t\t\tself.update()?;\n\t\t\t} else if ev == AsyncGitNotification::Diff {\n\t\t\t\tself.update_diff()?;\n\t\t\t}\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn get_ids(&self) -> Option<OldNew<CommitId>> {\n\t\tlet other = self\n\t\t\t.open_request\n\t\t\t.as_ref()\n\t\t\t.and_then(|open| open.compare_id);\n\n\t\tlet this =\n\t\t\tself.open_request.as_ref().map(|open| open.commit_id);\n\n\t\tSome(OldNew {\n\t\t\told: other?,\n\t\t\tnew: this?,\n\t\t})\n\t}\n\n\t/// called when any tree component changed selection\n\tpub fn update_diff(&mut self) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tif let Some(ids) = self.get_ids() {\n\t\t\t\tif let Some(f) = self.details.files().selection_file()\n\t\t\t\t{\n\t\t\t\t\tlet diff_params = DiffParams {\n\t\t\t\t\t\tpath: f.path.clone(),\n\t\t\t\t\t\tdiff_type: DiffType::Commits(ids),\n\t\t\t\t\t\toptions: self.options.borrow().diff_options(),\n\t\t\t\t\t};\n\n\t\t\t\t\tif let Some((params, last)) =\n\t\t\t\t\t\tself.git_diff.last()?\n\t\t\t\t\t{\n\t\t\t\t\t\tif params == diff_params {\n\t\t\t\t\t\t\tself.diff.update(f.path, false, last);\n\t\t\t\t\t\t\treturn Ok(());\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tself.git_diff.request(diff_params)?;\n\t\t\t\t\tself.diff.clear(true);\n\t\t\t\t\treturn Ok(());\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tself.diff.clear(false);\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn update(&mut self) -> Result<()> {\n\t\tself.details.set_commits(\n\t\t\tself.get_ids().map(CommitFilesParams::from),\n\t\t\tNone,\n\t\t)?;\n\t\tself.update_diff()?;\n\n\t\tOk(())\n\t}\n\n\tfn can_focus_diff(&self) -> bool {\n\t\tself.details.files().selection_file().is_some()\n\t}\n\n\tfn hide_stacked(&mut self, stack: bool) {\n\t\tself.hide();\n\t\tif stack {\n\t\t\tif let Some(request) = self.open_request.clone() {\n\t\t\t\tself.queue.push(InternalEvent::PopupStackPush(\n\t\t\t\t\tStackablePopupOpen::CompareCommits(request),\n\t\t\t\t));\n\t\t\t}\n\t\t} else {\n\t\t\tself.queue.push(InternalEvent::PopupStackPop);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/popups/confirm.rs",
    "content": "use crate::{\n\tapp::Environment,\n\tcomponents::{\n\t\tvisibility_blocking, CommandBlocking, CommandInfo, Component,\n\t\tDrawableComponent, EventState,\n\t},\n\tkeys::{key_match, SharedKeyConfig},\n\tqueue::{Action, InternalEvent, Queue},\n\tstrings, ui,\n};\nuse anyhow::Result;\nuse crossterm::event::Event;\nuse ratatui::{layout::Rect, text::Text, widgets::Clear, Frame};\nuse std::borrow::Cow;\nuse ui::style::SharedTheme;\n\nuse super::popup_paragraph;\n\n///\npub struct ConfirmPopup {\n\ttarget: Option<Action>,\n\tvisible: bool,\n\tqueue: Queue,\n\ttheme: SharedTheme,\n\tkey_config: SharedKeyConfig,\n}\n\nimpl DrawableComponent for ConfirmPopup {\n\tfn draw(&self, f: &mut Frame, _rect: Rect) -> Result<()> {\n\t\tif self.visible {\n\t\t\tlet (title, msg) = self.get_text();\n\n\t\t\tlet txt = Text::styled(\n\t\t\t\tCow::from(msg),\n\t\t\t\tself.theme.text_danger(),\n\t\t\t);\n\n\t\t\tlet area = ui::centered_rect(50, 20, f.area());\n\t\t\tf.render_widget(Clear, area);\n\t\t\tf.render_widget(\n\t\t\t\tpopup_paragraph(&title, txt, &self.theme, true, true),\n\t\t\t\tarea,\n\t\t\t);\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for ConfirmPopup {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\t_force_all: bool,\n\t) -> CommandBlocking {\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::confirm_action(&self.key_config),\n\t\t\ttrue,\n\t\t\tself.visible,\n\t\t));\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::close_popup(&self.key_config),\n\t\t\ttrue,\n\t\t\tself.visible,\n\t\t));\n\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(&mut self, ev: &Event) -> Result<EventState> {\n\t\tif self.visible {\n\t\t\tif let Event::Key(e) = ev {\n\t\t\t\tif key_match(e, self.key_config.keys.exit_popup) {\n\t\t\t\t\tself.hide();\n\t\t\t\t} else if key_match(e, self.key_config.keys.enter) {\n\t\t\t\t\tself.confirm();\n\t\t\t\t}\n\n\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t}\n\t\t}\n\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.visible = false;\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\n\t\tOk(())\n\t}\n}\n\nimpl ConfirmPopup {\n\t///\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\ttarget: None,\n\t\t\tvisible: false,\n\t\t\tqueue: env.queue.clone(),\n\t\t\ttheme: env.theme.clone(),\n\t\t\tkey_config: env.key_config.clone(),\n\t\t}\n\t}\n\t///\n\tpub fn open(&mut self, a: Action) -> Result<()> {\n\t\tself.target = Some(a);\n\t\tself.show()?;\n\n\t\tOk(())\n\t}\n\t///\n\tpub fn confirm(&mut self) {\n\t\tif let Some(a) = self.target.take() {\n\t\t\tself.queue.push(InternalEvent::ConfirmedAction(a));\n\t\t}\n\n\t\tself.hide();\n\t}\n\n\tfn get_text(&self) -> (String, String) {\n\t\tif let Some(ref a) = self.target {\n\t\t\treturn match a {\n                Action::Reset(_) => (\n                    strings::confirm_title_reset(),\n                    strings::confirm_msg_reset(),\n                ),\n                Action::StashDrop(ids) => (\n                    strings::confirm_title_stashdrop(\n                        &self.key_config,ids.len()>1\n                    ),\n                    strings::confirm_msg_stashdrop(&self.key_config,ids),\n                ),\n                Action::StashPop(_) => (\n                    strings::confirm_title_stashpop(&self.key_config),\n                    strings::confirm_msg_stashpop(&self.key_config),\n                ),\n                Action::ResetHunk(_, _) => (\n                    strings::confirm_title_reset(),\n                    strings::confirm_msg_resethunk(&self.key_config),\n                ),\n                Action::ResetLines(_, lines) => (\n                    strings::confirm_title_reset(),\n                    strings::confirm_msg_reset_lines(lines.len()),\n                ),\n                Action::DeleteLocalBranch(branch_ref) => (\n                    strings::confirm_title_delete_branch(\n                        &self.key_config,\n                    ),\n                    strings::confirm_msg_delete_branch(\n                        &self.key_config,\n                        branch_ref,\n                    ),\n                ),\n                Action::DeleteRemoteBranch(branch_ref) => (\n                    strings::confirm_title_delete_remote_branch(\n                        &self.key_config,\n                    ),\n                    strings::confirm_msg_delete_remote_branch(\n                        &self.key_config,\n                        branch_ref,\n                    ),\n                ),\n\t\tAction::DeleteRemote(remote_name)=>(\n\t\t\tstrings::confirm_title_delete_remote(&self.key_config),\n\t\t\tstrings::confirm_msg_delete_remote(&self.key_config,remote_name),\n\t\t),\n                Action::DeleteTag(tag_name) => (\n                    strings::confirm_title_delete_tag(\n                        &self.key_config,\n                    ),\n                    strings::confirm_msg_delete_tag(\n                        &self.key_config,\n                        tag_name,\n                    ),\n                ),\n\t\t\t\tAction::DeleteRemoteTag(_tag_name,remote) => (\n                    strings::confirm_title_delete_tag_remote(),\n                    strings::confirm_msg_delete_tag_remote(remote),\n                ),\n                Action::ForcePush(branch, _force) => (\n                    strings::confirm_title_force_push(\n                        &self.key_config,\n                    ),\n                    strings::confirm_msg_force_push(\n                        &self.key_config,\n                        branch.rsplit('/').next().expect(\"There was no / in the head reference which is impossible in git\"),\n                    ),\n                ),\n                Action::PullMerge{incoming,rebase} => (\n                    strings::confirm_title_merge(&self.key_config,*rebase),\n                    strings::confirm_msg_merge(&self.key_config,*incoming,*rebase),\n                ),\n                Action::AbortMerge => (\n                    strings::confirm_title_abortmerge(),\n                    strings::confirm_msg_revertchanges(),\n                ),\n\t\t\t\tAction::AbortRebase => (\n                    strings::confirm_title_abortrebase(),\n                    strings::confirm_msg_abortrebase(),\n                ),\n\t\t\t\tAction::AbortRevert => (\n                    strings::confirm_title_abortrevert(),\n                    strings::confirm_msg_revertchanges(),\n                ),\n                Action::UndoCommit => (\n                    strings::confirm_title_undo_commit(),\n                    strings::confirm_msg_undo_commit(),\n                ),\n            };\n\t\t}\n\n\t\t(String::new(), String::new())\n\t}\n}\n"
  },
  {
    "path": "src/popups/create_branch.rs",
    "content": "use crate::components::{\n\tvisibility_blocking, CommandBlocking, CommandInfo, Component,\n\tDrawableComponent, EventState, InputType, TextInputComponent,\n};\nuse crate::{\n\tapp::Environment,\n\tkeys::{key_match, SharedKeyConfig},\n\tqueue::{InternalEvent, NeedsUpdate, Queue},\n\tstrings,\n\tui::style::SharedTheme,\n};\nuse anyhow::Result;\nuse asyncgit::sync::{self, RepoPathRef};\nuse crossterm::event::Event;\nuse easy_cast::Cast;\nuse ratatui::{layout::Rect, widgets::Paragraph, Frame};\n\npub struct CreateBranchPopup {\n\trepo: RepoPathRef,\n\tinput: TextInputComponent,\n\tqueue: Queue,\n\tkey_config: SharedKeyConfig,\n\ttheme: SharedTheme,\n}\n\nimpl DrawableComponent for CreateBranchPopup {\n\tfn draw(&self, f: &mut Frame, rect: Rect) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tself.input.draw(f, rect)?;\n\t\t\tself.draw_warnings(f);\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for CreateBranchPopup {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.is_visible() || force_all {\n\t\t\tself.input.commands(out, force_all);\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::create_branch_confirm_msg(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t),\n\t\t\t\ttrue,\n\t\t\t\ttrue,\n\t\t\t));\n\t\t}\n\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(&mut self, ev: &Event) -> Result<EventState> {\n\t\tif self.is_visible() {\n\t\t\tif self.input.event(ev)?.is_consumed() {\n\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t}\n\n\t\t\tif let Event::Key(e) = ev {\n\t\t\t\tif key_match(e, self.key_config.keys.enter) {\n\t\t\t\t\tself.create_branch();\n\t\t\t\t}\n\n\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t}\n\t\t}\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.input.is_visible()\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.input.hide();\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.input.show()?;\n\n\t\tOk(())\n\t}\n}\n\nimpl CreateBranchPopup {\n\t///\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\tqueue: env.queue.clone(),\n\t\t\tinput: TextInputComponent::new(\n\t\t\t\tenv,\n\t\t\t\t&strings::create_branch_popup_title(&env.key_config),\n\t\t\t\t&strings::create_branch_popup_msg(&env.key_config),\n\t\t\t\ttrue,\n\t\t\t)\n\t\t\t.with_input_type(InputType::Singleline),\n\t\t\ttheme: env.theme.clone(),\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\trepo: env.repo.clone(),\n\t\t}\n\t}\n\n\t///\n\tpub fn open(&mut self) -> Result<()> {\n\t\tself.show()?;\n\n\t\tOk(())\n\t}\n\n\t///\n\tpub fn create_branch(&mut self) {\n\t\tlet res = sync::create_branch(\n\t\t\t&self.repo.borrow(),\n\t\t\tself.input.get_text(),\n\t\t);\n\n\t\tself.input.clear();\n\t\tself.hide();\n\n\t\tmatch res {\n\t\t\tOk(_) => {\n\t\t\t\tself.queue.push(InternalEvent::Update(\n\t\t\t\t\tNeedsUpdate::ALL | NeedsUpdate::BRANCHES,\n\t\t\t\t));\n\t\t\t}\n\t\t\tErr(e) => {\n\t\t\t\tlog::error!(\"create branch: {e}\");\n\t\t\t\tself.queue.push(InternalEvent::ShowErrorMsg(\n\t\t\t\t\tformat!(\"create branch error:\\n{e}\"),\n\t\t\t\t));\n\t\t\t}\n\t\t}\n\t}\n\n\tfn draw_warnings(&self, f: &mut Frame) {\n\t\tlet current_text = self.input.get_text();\n\n\t\tif !current_text.is_empty() {\n\t\t\tlet valid = sync::validate_branch_name(current_text)\n\t\t\t\t.unwrap_or_default();\n\n\t\t\tif !valid {\n\t\t\t\tlet msg = strings::branch_name_invalid();\n\t\t\t\tlet msg_length: u16 = msg.len().cast();\n\t\t\t\tlet w = Paragraph::new(msg)\n\t\t\t\t\t.style(self.theme.text_danger());\n\n\t\t\t\tlet rect = {\n\t\t\t\t\tlet mut rect = self.input.get_area();\n\t\t\t\t\trect.y += rect.height.saturating_sub(1);\n\t\t\t\t\trect.height = 1;\n\t\t\t\t\tlet offset =\n\t\t\t\t\t\trect.width.saturating_sub(msg_length + 1);\n\t\t\t\t\trect.width =\n\t\t\t\t\t\trect.width.saturating_sub(offset + 1);\n\t\t\t\t\trect.x += offset;\n\n\t\t\t\t\trect\n\t\t\t\t};\n\n\t\t\t\tf.render_widget(w, rect);\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/popups/create_remote.rs",
    "content": "use anyhow::Result;\nuse asyncgit::sync::{self, validate_remote_name, RepoPathRef};\nuse crossterm::event::Event;\nuse easy_cast::Cast;\nuse ratatui::{widgets::Paragraph, Frame};\n\nuse crate::{\n\tapp::Environment,\n\tcomponents::{\n\t\tvisibility_blocking, CommandBlocking, CommandInfo, Component,\n\t\tDrawableComponent, EventState, InputType, TextInputComponent,\n\t},\n\tkeys::{key_match, SharedKeyConfig},\n\tqueue::{InternalEvent, NeedsUpdate, Queue},\n\tstrings,\n\tui::style::SharedTheme,\n};\n\n#[derive(Default)]\nenum State {\n\t// first we ask for a name for a new remote\n\t#[default]\n\tName,\n\t// second we ask for a url and carry with us the name previously entered\n\tUrl {\n\t\tname: String,\n\t},\n}\n\npub struct CreateRemotePopup {\n\trepo: RepoPathRef,\n\tinput: TextInputComponent,\n\tqueue: Queue,\n\tkey_config: SharedKeyConfig,\n\tstate: State,\n\ttheme: SharedTheme,\n}\n\nimpl DrawableComponent for CreateRemotePopup {\n\tfn draw(\n\t\t&self,\n\t\tf: &mut ratatui::Frame,\n\t\trect: ratatui::prelude::Rect,\n\t) -> anyhow::Result<()> {\n\t\tif self.is_visible() {\n\t\t\tself.input.draw(f, rect)?;\n\t\t\tself.draw_warnings(f);\n\t\t}\n\t\tOk(())\n\t}\n}\n\nimpl Component for CreateRemotePopup {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.is_visible() || force_all {\n\t\t\tself.input.commands(out, force_all);\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::remote_confirm_name_msg(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t),\n\t\t\t\ttrue,\n\t\t\t\ttrue,\n\t\t\t));\n\t\t}\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(\n\t\t&mut self,\n\t\tev: &crossterm::event::Event,\n\t) -> Result<EventState> {\n\t\tif self.is_visible() {\n\t\t\tif self.input.event(ev)?.is_consumed() {\n\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t}\n\n\t\t\tif let Event::Key(e) = ev {\n\t\t\t\tif key_match(e, self.key_config.keys.enter) {\n\t\t\t\t\tself.handle_submit();\n\t\t\t\t}\n\n\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t}\n\t\t}\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.input.is_visible()\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.input.hide();\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.input.clear();\n\t\tself.input.set_title(\n\t\t\tstrings::create_remote_popup_title_name(&self.key_config),\n\t\t);\n\t\tself.input.set_default_msg(\n\t\t\tstrings::create_remote_popup_msg_name(&self.key_config),\n\t\t);\n\n\t\tself.input.show()?;\n\n\t\tOk(())\n\t}\n}\n\nimpl CreateRemotePopup {\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\trepo: env.repo.clone(),\n\t\t\tqueue: env.queue.clone(),\n\t\t\tinput: TextInputComponent::new(env, \"\", \"\", true)\n\t\t\t\t.with_input_type(InputType::Singleline),\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\tstate: State::Name,\n\t\t\ttheme: env.theme.clone(),\n\t\t}\n\t}\n\n\tpub fn open(&mut self) -> Result<()> {\n\t\tself.state = State::Name;\n\t\tself.input.clear();\n\t\tself.show()?;\n\n\t\tOk(())\n\t}\n\n\tfn draw_warnings(&self, f: &mut Frame) {\n\t\tlet remote_name = match self.state {\n\t\t\tState::Name => self.input.get_text(),\n\t\t\tState::Url { .. } => return,\n\t\t};\n\n\t\tif !remote_name.is_empty() {\n\t\t\tlet valid = validate_remote_name(remote_name);\n\n\t\t\tif !valid {\n\t\t\t\tlet msg = strings::remote_name_invalid();\n\t\t\t\tlet msg_length: u16 = msg.len().cast();\n\t\t\t\tlet w = Paragraph::new(msg)\n\t\t\t\t\t.style(self.theme.text_danger());\n\n\t\t\t\tlet rect = {\n\t\t\t\t\tlet mut rect = self.input.get_area();\n\t\t\t\t\trect.y += rect.height.saturating_sub(1);\n\t\t\t\t\trect.height = 1;\n\t\t\t\t\tlet offset =\n\t\t\t\t\t\trect.width.saturating_sub(msg_length + 1);\n\t\t\t\t\trect.width =\n\t\t\t\t\t\trect.width.saturating_sub(offset + 1);\n\t\t\t\t\trect.x += offset;\n\n\t\t\t\t\trect\n\t\t\t\t};\n\n\t\t\t\tf.render_widget(w, rect);\n\t\t\t}\n\t\t}\n\t}\n\n\tfn handle_submit(&mut self) {\n\t\tmatch &self.state {\n\t\t\tState::Name => {\n\t\t\t\tself.state = State::Url {\n\t\t\t\t\tname: self.input.get_text().to_string(),\n\t\t\t\t};\n\n\t\t\t\tself.input.clear();\n\t\t\t\tself.input.set_title(\n\t\t\t\t\tstrings::create_remote_popup_title_url(\n\t\t\t\t\t\t&self.key_config,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tself.input.set_default_msg(\n\t\t\t\t\tstrings::create_remote_popup_msg_url(\n\t\t\t\t\t\t&self.key_config,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t\tState::Url { name } => {\n\t\t\t\tlet res = sync::add_remote(\n\t\t\t\t\t&self.repo.borrow(),\n\t\t\t\t\tname,\n\t\t\t\t\tself.input.get_text(),\n\t\t\t\t);\n\n\t\t\t\tmatch res {\n\t\t\t\t\tOk(()) => {\n\t\t\t\t\t\tself.queue.push(InternalEvent::Update(\n\t\t\t\t\t\t\tNeedsUpdate::ALL | NeedsUpdate::REMOTES,\n\t\t\t\t\t\t));\n\t\t\t\t\t}\n\t\t\t\t\tErr(e) => {\n\t\t\t\t\t\tlog::error!(\"create remote: {e}\");\n\t\t\t\t\t\tself.queue.push(InternalEvent::ShowErrorMsg(\n\t\t\t\t\t\t\tformat!(\"create remote error:\\n{e}\"),\n\t\t\t\t\t\t));\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tself.hide();\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/popups/externaleditor.rs",
    "content": "use crate::{\n\tapp::Environment,\n\tcomponents::{\n\t\tvisibility_blocking, CommandBlocking, CommandInfo, Component,\n\t\tDrawableComponent, EventState,\n\t},\n\tkeys::SharedKeyConfig,\n\tstrings,\n\tui::{self, style::SharedTheme},\n};\nuse anyhow::{anyhow, bail, Result};\nuse asyncgit::sync::{\n\tget_config_string, utils::repo_work_dir, RepoPath,\n};\nuse crossterm::{\n\tevent::Event,\n\tterminal::{EnterAlternateScreen, LeaveAlternateScreen},\n\tExecutableCommand,\n};\nuse ratatui::{\n\tlayout::Rect,\n\ttext::{Line, Span},\n\twidgets::{Block, BorderType, Borders, Clear, Paragraph},\n\tFrame,\n};\nuse scopeguard::defer;\nuse std::ffi::OsStr;\nuse std::{env, io, path::Path, process::Command};\n\n///\npub struct ExternalEditorPopup {\n\tvisible: bool,\n\ttheme: SharedTheme,\n\tkey_config: SharedKeyConfig,\n}\n\nimpl ExternalEditorPopup {\n\t///\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\tvisible: false,\n\t\t\ttheme: env.theme.clone(),\n\t\t\tkey_config: env.key_config.clone(),\n\t\t}\n\t}\n\n\t/// opens file at given `path` in an available editor\n\tpub fn open_file_in_editor(\n\t\trepo: &RepoPath,\n\t\tpath: &Path,\n\t) -> Result<()> {\n\t\tlet work_dir = repo_work_dir(repo)?;\n\n\t\tlet path = if path.is_relative() {\n\t\t\tPath::new(&work_dir).join(path)\n\t\t} else {\n\t\t\tpath.into()\n\t\t};\n\n\t\tif !path.exists() {\n\t\t\tbail!(\"file not found: {path:?}\");\n\t\t}\n\n\t\tio::stdout().execute(LeaveAlternateScreen)?;\n\t\tdefer! {\n\t\t\tio::stdout().execute(EnterAlternateScreen).expect(\"reset terminal\");\n\t\t}\n\n\t\tlet environment_options = [\"GIT_EDITOR\", \"VISUAL\", \"EDITOR\"];\n\n\t\tlet editor = env::var(environment_options[0])\n\t\t\t.ok()\n\t\t\t.or_else(|| {\n\t\t\t\tget_config_string(repo, \"core.editor\").ok()?\n\t\t\t})\n\t\t\t.or_else(|| env::var(environment_options[1]).ok())\n\t\t\t.or_else(|| env::var(environment_options[2]).ok())\n\t\t\t.unwrap_or_else(|| String::from(\"vi\"));\n\n\t\t// TODO: proper handling arguments containing whitespaces\n\t\t// This does not do the right thing if the input is `editor --something \"with spaces\"`\n\n\t\t// deal with \"editor name with spaces\" p1 p2 p3\n\t\t// and with \"editor_no_spaces\" p1 p2 p3\n\t\t// does not address spaces in pn\n\t\tlet mut echars = editor.chars().peekable();\n\n\t\tlet first_char = *echars.peek().ok_or_else(|| {\n\t\t\tanyhow!(\n\t\t\t\t\"editor env variable found empty: {}\",\n\t\t\t\tenvironment_options.join(\" or \")\n\t\t\t)\n\t\t})?;\n\t\tlet command: String = if first_char == '\\\"' {\n\t\t\techars\n\t\t\t\t.by_ref()\n\t\t\t\t.skip(1)\n\t\t\t\t.take_while(|c| *c != '\\\"')\n\t\t\t\t.collect()\n\t\t} else {\n\t\t\techars.by_ref().take_while(|c| *c != ' ').collect()\n\t\t};\n\n\t\tlet remainder_str = echars.collect::<String>();\n\t\tlet remainder = remainder_str.split_whitespace();\n\n\t\tlet mut args: Vec<&OsStr> =\n\t\t\tremainder.map(OsStr::new).collect();\n\n\t\targs.push(path.as_os_str());\n\n\t\tCommand::new(command.clone())\n\t\t\t.current_dir(work_dir)\n\t\t\t.args(args)\n\t\t\t.status()\n\t\t\t.map_err(|e| anyhow!(\"\\\"{command}\\\": {e}\"))?;\n\n\t\tOk(())\n\t}\n}\n\nimpl DrawableComponent for ExternalEditorPopup {\n\tfn draw(&self, f: &mut Frame, _rect: Rect) -> Result<()> {\n\t\tif self.visible {\n\t\t\tlet txt = Line::from(\n\t\t\t\tstrings::msg_opening_editor(&self.key_config)\n\t\t\t\t\t.split('\\n')\n\t\t\t\t\t.map(|string| {\n\t\t\t\t\t\tSpan::raw::<String>(string.to_string())\n\t\t\t\t\t})\n\t\t\t\t\t.collect::<Vec<Span>>(),\n\t\t\t);\n\n\t\t\tlet area = ui::centered_rect_absolute(25, 3, f.area());\n\t\t\tf.render_widget(Clear, area);\n\t\t\tf.render_widget(\n\t\t\t\tParagraph::new(txt)\n\t\t\t\t\t.block(\n\t\t\t\t\t\tBlock::default()\n\t\t\t\t\t\t\t.borders(Borders::ALL)\n\t\t\t\t\t\t\t.border_type(BorderType::Thick)\n\t\t\t\t\t\t\t.border_style(self.theme.block(true)),\n\t\t\t\t\t)\n\t\t\t\t\t.style(self.theme.block(true)),\n\t\t\t\tarea,\n\t\t\t);\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for ExternalEditorPopup {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.visible && !force_all {\n\t\t\tout.clear();\n\t\t}\n\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(&mut self, _ev: &Event) -> Result<EventState> {\n\t\tif self.visible {\n\t\t\treturn Ok(EventState::Consumed);\n\t\t}\n\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.visible = false;\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "src/popups/fetch.rs",
    "content": "use crate::{\n\tapp::Environment,\n\tcomponents::{\n\t\tvisibility_blocking, CommandBlocking, CommandInfo, Component,\n\t\tCredComponent, DrawableComponent, EventState,\n\t},\n\tkeys::SharedKeyConfig,\n\tqueue::{InternalEvent, NeedsUpdate, Queue},\n\tstrings,\n\tui::{self, style::SharedTheme},\n};\nuse anyhow::Result;\nuse asyncgit::{\n\tasyncjob::AsyncSingleJob,\n\tsync::{\n\t\tcred::{\n\t\t\textract_username_password, need_username_password,\n\t\t\tBasicAuthCredential,\n\t\t},\n\t\tRepoPathRef,\n\t},\n\tAsyncFetchJob, AsyncGitNotification, ProgressPercent,\n};\nuse crossterm::event::Event;\nuse ratatui::{\n\tlayout::Rect,\n\ttext::Span,\n\twidgets::{Block, BorderType, Borders, Clear, Gauge},\n\tFrame,\n};\n\n///\npub struct FetchPopup {\n\trepo: RepoPathRef,\n\tvisible: bool,\n\tasync_fetch: AsyncSingleJob<AsyncFetchJob>,\n\tprogress: Option<ProgressPercent>,\n\tpending: bool,\n\tqueue: Queue,\n\ttheme: SharedTheme,\n\tkey_config: SharedKeyConfig,\n\tinput_cred: CredComponent,\n}\n\nimpl FetchPopup {\n\t///\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\tqueue: env.queue.clone(),\n\t\t\tpending: false,\n\t\t\tvisible: false,\n\t\t\tasync_fetch: AsyncSingleJob::new(env.sender_git.clone()),\n\t\t\tprogress: None,\n\t\t\tinput_cred: CredComponent::new(env),\n\t\t\ttheme: env.theme.clone(),\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\trepo: env.repo.clone(),\n\t\t}\n\t}\n\n\t///\n\tpub fn fetch(&mut self) -> Result<()> {\n\t\tself.show()?;\n\t\tif need_username_password(&self.repo.borrow())? {\n\t\t\tlet cred = extract_username_password(&self.repo.borrow())\n\t\t\t\t.unwrap_or_else(|_| {\n\t\t\t\t\tBasicAuthCredential::new(None, None)\n\t\t\t\t});\n\t\t\tif cred.is_complete() {\n\t\t\t\tself.fetch_all(Some(cred));\n\t\t\t} else {\n\t\t\t\tself.input_cred.set_cred(cred);\n\t\t\t\tself.input_cred.show()?;\n\t\t\t}\n\t\t} else {\n\t\t\tself.fetch_all(None);\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn fetch_all(&mut self, cred: Option<BasicAuthCredential>) {\n\t\tself.pending = true;\n\t\tself.progress = None;\n\t\tself.progress = Some(ProgressPercent::empty());\n\t\tself.async_fetch.spawn(AsyncFetchJob::new(\n\t\t\tself.repo.borrow().clone(),\n\t\t\tcred,\n\t\t));\n\t}\n\n\t///\n\tpub const fn any_work_pending(&self) -> bool {\n\t\tself.pending\n\t}\n\n\t///\n\tpub fn update_git(&mut self, ev: AsyncGitNotification) {\n\t\tif self.is_visible() && ev == AsyncGitNotification::Fetch {\n\t\t\tself.update();\n\t\t}\n\t}\n\n\t///\n\tfn update(&mut self) {\n\t\tself.pending = self.async_fetch.is_pending();\n\t\tself.progress = self.async_fetch.progress();\n\n\t\tif !self.pending {\n\t\t\tself.hide();\n\t\t\tself.queue\n\t\t\t\t.push(InternalEvent::Update(NeedsUpdate::BRANCHES));\n\t\t}\n\t}\n}\n\nimpl DrawableComponent for FetchPopup {\n\tfn draw(&self, f: &mut Frame, rect: Rect) -> Result<()> {\n\t\tif self.visible {\n\t\t\tlet progress = self.progress.unwrap_or_default().progress;\n\n\t\t\tlet area = ui::centered_rect_absolute(30, 3, f.area());\n\n\t\t\tf.render_widget(Clear, area);\n\t\t\tf.render_widget(\n\t\t\t\tGauge::default()\n\t\t\t\t\t.block(\n\t\t\t\t\t\tBlock::default()\n\t\t\t\t\t\t\t.title(Span::styled(\n\t\t\t\t\t\t\t\tstrings::FETCH_POPUP_MSG,\n\t\t\t\t\t\t\t\tself.theme.title(true),\n\t\t\t\t\t\t\t))\n\t\t\t\t\t\t\t.borders(Borders::ALL)\n\t\t\t\t\t\t\t.border_type(BorderType::Thick)\n\t\t\t\t\t\t\t.border_style(self.theme.block(true)),\n\t\t\t\t\t)\n\t\t\t\t\t.gauge_style(self.theme.push_gauge())\n\t\t\t\t\t.percent(u16::from(progress)),\n\t\t\t\tarea,\n\t\t\t);\n\t\t\tself.input_cred.draw(f, rect)?;\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for FetchPopup {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.is_visible() || force_all {\n\t\t\tif !force_all {\n\t\t\t\tout.clear();\n\t\t\t}\n\n\t\t\tif self.input_cred.is_visible() {\n\t\t\t\treturn self.input_cred.commands(out, force_all);\n\t\t\t}\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::close_msg(&self.key_config),\n\t\t\t\t!self.pending,\n\t\t\t\tself.visible,\n\t\t\t));\n\t\t}\n\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(&mut self, ev: &Event) -> Result<EventState> {\n\t\tif self.visible {\n\t\t\tif let Event::Key(_) = ev {\n\t\t\t\tif self.input_cred.is_visible() {\n\t\t\t\t\tself.input_cred.event(ev)?;\n\n\t\t\t\t\tif self.input_cred.get_cred().is_complete()\n\t\t\t\t\t\t|| !self.input_cred.is_visible()\n\t\t\t\t\t{\n\t\t\t\t\t\tself.fetch_all(Some(\n\t\t\t\t\t\t\tself.input_cred.get_cred().clone(),\n\t\t\t\t\t\t));\n\t\t\t\t\t\tself.input_cred.hide();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn Ok(EventState::Consumed);\n\t\t}\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.visible = false;\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "src/popups/file_revlog.rs",
    "content": "use crate::{\n\tapp::Environment,\n\tcomponents::{\n\t\tevent_pump, visibility_blocking, CommandBlocking,\n\t\tCommandInfo, Component, DiffComponent, DrawableComponent,\n\t\tEventState, ItemBatch, ScrollType,\n\t},\n\tkeys::{key_match, SharedKeyConfig},\n\toptions::SharedOptions,\n\tqueue::{InternalEvent, NeedsUpdate, Queue, StackablePopupOpen},\n\tstrings,\n\tui::{draw_scrollbar, style::SharedTheme, Orientation},\n};\nuse anyhow::Result;\nuse asyncgit::{\n\tsync::{\n\t\tdiff_contains_file, get_commits_info, CommitId, RepoPathRef,\n\t},\n\tAsyncDiff, AsyncGitNotification, AsyncLog, DiffParams, DiffType,\n};\nuse chrono::{DateTime, Local};\nuse crossbeam_channel::Sender;\nuse crossterm::event::Event;\nuse ratatui::{\n\tlayout::{Constraint, Direction, Layout, Rect},\n\ttext::{Line, Span, Text},\n\twidgets::{Block, Borders, Cell, Clear, Row, Table, TableState},\n\tFrame,\n};\n\nuse super::{BlameFileOpen, InspectCommitOpen};\n\nconst SLICE_SIZE: usize = 1200;\n\n#[derive(Clone, Debug)]\npub struct FileRevOpen {\n\tpub file_path: String,\n\tpub selection: Option<usize>,\n}\n\nimpl FileRevOpen {\n\tpub const fn new(file_path: String) -> Self {\n\t\tSelf {\n\t\t\tfile_path,\n\t\t\tselection: None,\n\t\t}\n\t}\n}\n\n///\npub struct FileRevlogPopup {\n\tgit_log: Option<AsyncLog>,\n\tgit_diff: AsyncDiff,\n\ttheme: SharedTheme,\n\tqueue: Queue,\n\tsender: Sender<AsyncGitNotification>,\n\tdiff: DiffComponent,\n\tvisible: bool,\n\trepo_path: RepoPathRef,\n\topen_request: Option<FileRevOpen>,\n\ttable_state: std::cell::Cell<TableState>,\n\titems: ItemBatch,\n\tcount_total: usize,\n\tkey_config: SharedKeyConfig,\n\toptions: SharedOptions,\n\tcurrent_width: std::cell::Cell<usize>,\n\tcurrent_height: std::cell::Cell<usize>,\n}\n\nimpl FileRevlogPopup {\n\t///\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\ttheme: env.theme.clone(),\n\t\t\tqueue: env.queue.clone(),\n\t\t\tsender: env.sender_git.clone(),\n\t\t\tdiff: DiffComponent::new(env, true),\n\t\t\tgit_log: None,\n\t\t\tgit_diff: AsyncDiff::new(\n\t\t\t\tenv.repo.borrow().clone(),\n\t\t\t\t&env.sender_git,\n\t\t\t),\n\t\t\tvisible: false,\n\t\t\trepo_path: env.repo.clone(),\n\t\t\topen_request: None,\n\t\t\ttable_state: std::cell::Cell::new(TableState::default()),\n\t\t\titems: ItemBatch::default(),\n\t\t\tcount_total: 0,\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\tcurrent_width: std::cell::Cell::new(0),\n\t\t\tcurrent_height: std::cell::Cell::new(0),\n\t\t\toptions: env.options.clone(),\n\t\t}\n\t}\n\n\tfn components_mut(&mut self) -> Vec<&mut dyn Component> {\n\t\tvec![&mut self.diff]\n\t}\n\n\t///\n\tpub fn open(&mut self, open_request: FileRevOpen) -> Result<()> {\n\t\tself.open_request = Some(open_request.clone());\n\n\t\tlet filter = diff_contains_file(open_request.file_path);\n\t\tself.git_log = Some(AsyncLog::new(\n\t\t\tself.repo_path.borrow().clone(),\n\t\t\t&self.sender,\n\t\t\tSome(filter),\n\t\t));\n\n\t\tself.items.clear();\n\t\tself.set_selection(open_request.selection.unwrap_or(0));\n\n\t\tself.show()?;\n\n\t\tself.diff.focus(false);\n\t\tself.diff.clear(false);\n\n\t\tself.update()?;\n\n\t\tOk(())\n\t}\n\n\t///\n\tpub fn any_work_pending(&self) -> bool {\n\t\tself.git_diff.is_pending()\n\t\t\t|| self.git_log.as_ref().is_some_and(AsyncLog::is_pending)\n\t}\n\n\t///\n\tpub fn update(&mut self) -> Result<()> {\n\t\tif let Some(ref mut git_log) = self.git_log {\n\t\t\tgit_log.fetch()?;\n\n\t\t\tself.fetch_commits_if_needed()?;\n\t\t\tself.update_diff()?;\n\t\t}\n\n\t\tOk(())\n\t}\n\n\t///\n\tpub fn update_git(\n\t\t&mut self,\n\t\tevent: AsyncGitNotification,\n\t) -> Result<()> {\n\t\tif self.visible {\n\t\t\tmatch event {\n\t\t\t\tAsyncGitNotification::CommitFiles\n\t\t\t\t| AsyncGitNotification::Log => self.update()?,\n\t\t\t\tAsyncGitNotification::Diff => self.update_diff()?,\n\t\t\t\t_ => (),\n\t\t\t}\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tpub fn update_diff(&mut self) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tif let Some(commit_id) = self.selected_commit() {\n\t\t\t\tif let Some(open_request) = &self.open_request {\n\t\t\t\t\tlet diff_params = DiffParams {\n\t\t\t\t\t\tpath: open_request.file_path.clone(),\n\t\t\t\t\t\tdiff_type: DiffType::Commit(commit_id),\n\t\t\t\t\t\toptions: self.options.borrow().diff_options(),\n\t\t\t\t\t};\n\n\t\t\t\t\tif let Some((params, last)) =\n\t\t\t\t\t\tself.git_diff.last()?\n\t\t\t\t\t{\n\t\t\t\t\t\tif params == diff_params {\n\t\t\t\t\t\t\tself.diff.update(\n\t\t\t\t\t\t\t\topen_request.file_path.clone(),\n\t\t\t\t\t\t\t\tfalse,\n\t\t\t\t\t\t\t\tlast,\n\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t\treturn Ok(());\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tself.git_diff.request(diff_params)?;\n\t\t\t\t\tself.diff.clear(true);\n\n\t\t\t\t\treturn Ok(());\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tself.diff.clear(false);\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn fetch_commits(\n\t\t&mut self,\n\t\tnew_offset: usize,\n\t\tnew_max_offset: usize,\n\t) -> Result<()> {\n\t\tif let Some(git_log) = &mut self.git_log {\n\t\t\tlet amount = new_max_offset\n\t\t\t\t.saturating_sub(new_offset)\n\t\t\t\t.max(SLICE_SIZE);\n\n\t\t\tlet commits = get_commits_info(\n\t\t\t\t&self.repo_path.borrow(),\n\t\t\t\t&git_log.get_slice(new_offset, amount)?,\n\t\t\t\tself.current_width.get(),\n\t\t\t);\n\n\t\t\tif let Ok(commits) = commits {\n\t\t\t\tself.items.set_items(new_offset, commits, None);\n\t\t\t}\n\n\t\t\tself.count_total = git_log.count()?;\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn selected_commit(&self) -> Option<CommitId> {\n\t\tlet table_state = self.table_state.take();\n\n\t\tlet commit_id = table_state.selected().and_then(|selected| {\n\t\t\tself.items\n\t\t\t\t.iter()\n\t\t\t\t.nth(\n\t\t\t\t\tselected\n\t\t\t\t\t\t.saturating_sub(self.items.index_offset()),\n\t\t\t\t)\n\t\t\t\t.as_ref()\n\t\t\t\t.map(|entry| entry.id)\n\t\t});\n\n\t\tself.table_state.set(table_state);\n\n\t\tcommit_id\n\t}\n\n\tfn can_focus_diff(&self) -> bool {\n\t\tself.selected_commit().is_some()\n\t}\n\n\tfn get_title(&self) -> String {\n\t\tlet selected = {\n\t\t\tlet table = self.table_state.take();\n\t\t\tlet res = table.selected().unwrap_or_default();\n\t\t\tself.table_state.set(table);\n\t\t\tres\n\t\t};\n\t\tlet revisions = self.get_max_selection();\n\n\t\tself.open_request.as_ref().map_or_else(\n\t\t\t|| \"<no history available>\".into(),\n\t\t\t|open_request| {\n\t\t\t\tstrings::file_log_title(\n\t\t\t\t\t&open_request.file_path,\n\t\t\t\t\tselected,\n\t\t\t\t\trevisions,\n\t\t\t\t)\n\t\t\t},\n\t\t)\n\t}\n\n\tfn get_rows(&self, now: DateTime<Local>) -> Vec<Row<'_>> {\n\t\tself.items\n\t\t\t.iter()\n\t\t\t.map(|entry| {\n\t\t\t\tlet spans = Line::from(vec![\n\t\t\t\t\tSpan::styled(\n\t\t\t\t\t\tentry.hash_short.to_string(),\n\t\t\t\t\t\tself.theme.commit_hash(false),\n\t\t\t\t\t),\n\t\t\t\t\tSpan::raw(\" \"),\n\t\t\t\t\tSpan::styled(\n\t\t\t\t\t\tentry.time_to_string(now),\n\t\t\t\t\t\tself.theme.commit_time(false),\n\t\t\t\t\t),\n\t\t\t\t\tSpan::raw(\" \"),\n\t\t\t\t\tSpan::styled(\n\t\t\t\t\t\tentry.author.to_string(),\n\t\t\t\t\t\tself.theme.commit_author(false),\n\t\t\t\t\t),\n\t\t\t\t]);\n\n\t\t\t\tlet mut text = Text::from(spans);\n\t\t\t\ttext.extend(Text::raw(entry.msg.to_string()));\n\n\t\t\t\tlet cells = vec![Cell::from(\"\"), Cell::from(text)];\n\n\t\t\t\tRow::new(cells).height(2)\n\t\t\t})\n\t\t\t.collect()\n\t}\n\n\tfn get_max_selection(&self) -> usize {\n\t\tself.git_log.as_ref().map_or(0, |log| {\n\t\t\tlog.count().unwrap_or(0).saturating_sub(1)\n\t\t})\n\t}\n\n\tfn move_selection(\n\t\t&mut self,\n\t\tscroll_type: ScrollType,\n\t) -> Result<()> {\n\t\tlet old_selection =\n\t\t\tself.table_state.get_mut().selected().unwrap_or(0);\n\t\tlet max_selection = self.get_max_selection();\n\t\tlet height_in_items = self.current_height.get() / 2;\n\n\t\tlet new_selection = match scroll_type {\n\t\t\tScrollType::Up => old_selection.saturating_sub(1),\n\t\t\tScrollType::Down => {\n\t\t\t\told_selection.saturating_add(1).min(max_selection)\n\t\t\t}\n\t\t\tScrollType::Home => 0,\n\t\t\tScrollType::End => max_selection,\n\t\t\tScrollType::PageUp => old_selection\n\t\t\t\t.saturating_sub(height_in_items.saturating_sub(2)),\n\t\t\tScrollType::PageDown => old_selection\n\t\t\t\t.saturating_add(height_in_items.saturating_sub(2))\n\t\t\t\t.min(max_selection),\n\t\t};\n\n\t\tlet needs_update = new_selection != old_selection;\n\n\t\tif needs_update {\n\t\t\tself.queue.push(InternalEvent::Update(NeedsUpdate::DIFF));\n\t\t}\n\n\t\tself.set_selection(new_selection);\n\t\tself.fetch_commits_if_needed()?;\n\n\t\tOk(())\n\t}\n\n\tfn set_selection(&mut self, selection: usize) {\n\t\tlet height_in_items =\n\t\t\t(self.current_height.get().saturating_sub(2)) / 2;\n\n\t\tlet offset = *self.table_state.get_mut().offset_mut();\n\t\tlet min_offset = selection\n\t\t\t.saturating_sub(height_in_items.saturating_sub(1));\n\n\t\tlet new_offset = offset.clamp(min_offset, selection);\n\n\t\t*self.table_state.get_mut().offset_mut() = new_offset;\n\t\tself.table_state.get_mut().select(Some(selection));\n\t}\n\n\tfn fetch_commits_if_needed(&mut self) -> Result<()> {\n\t\tlet selection =\n\t\t\tself.table_state.get_mut().selected().unwrap_or(0);\n\t\tlet offset = *self.table_state.get_mut().offset_mut();\n\t\tlet height_in_items =\n\t\t\t(self.current_height.get().saturating_sub(2)) / 2;\n\t\tlet new_max_offset =\n\t\t\tselection.saturating_add(height_in_items);\n\n\t\tif self.items.needs_data(offset, new_max_offset) {\n\t\t\tself.fetch_commits(offset, new_max_offset)?;\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn get_selection(&self) -> Option<usize> {\n\t\tlet table_state = self.table_state.take();\n\t\tlet selection = table_state.selected();\n\t\tself.table_state.set(table_state);\n\n\t\tselection\n\t}\n\n\tfn draw_revlog(&self, f: &mut Frame, area: Rect) {\n\t\tlet constraints = [\n\t\t\t// type of change: (A)dded, (M)odified, (D)eleted\n\t\t\tConstraint::Length(1),\n\t\t\t// commit details\n\t\t\tConstraint::Percentage(100),\n\t\t];\n\n\t\tlet now = Local::now();\n\n\t\tlet title = self.get_title();\n\t\tlet rows = self.get_rows(now);\n\n\t\tlet table = Table::new(rows, constraints)\n\t\t\t.column_spacing(1)\n\t\t\t.row_highlight_style(self.theme.text(true, true))\n\t\t\t.block(\n\t\t\t\tBlock::default()\n\t\t\t\t\t.borders(Borders::ALL)\n\t\t\t\t\t.title(Span::styled(\n\t\t\t\t\t\ttitle,\n\t\t\t\t\t\tself.theme.title(true),\n\t\t\t\t\t))\n\t\t\t\t\t.border_style(self.theme.block(true)),\n\t\t\t);\n\n\t\tlet table_state = self.table_state.take();\n\t\t// We have to adjust the table state for drawing to account for the fact\n\t\t// that `self.items` not necessarily starts at index 0.\n\t\t//\n\t\t// When a user scrolls down, items outside of the current view are removed\n\t\t// when new data is fetched. Let’s have a look at an example: if the item at\n\t\t// index 50 is the first item in the current view and `self.items` has been\n\t\t// freshly fetched, the current offset is 50 and `self.items[0]` is the item\n\t\t// at index 50. Subtracting the current offset from the selected index\n\t\t// yields the correct index in `self.items`, in this case 0.\n\t\tlet mut adjusted_table_state = TableState::default()\n\t\t\t.with_selected(table_state.selected().map(|selected| {\n\t\t\t\tselected.saturating_sub(self.items.index_offset())\n\t\t\t}))\n\t\t\t.with_offset(\n\t\t\t\ttable_state\n\t\t\t\t\t.offset()\n\t\t\t\t\t.saturating_sub(self.items.index_offset()),\n\t\t\t);\n\n\t\tf.render_widget(Clear, area);\n\t\tf.render_stateful_widget(\n\t\t\ttable,\n\t\t\tarea,\n\t\t\t&mut adjusted_table_state,\n\t\t);\n\n\t\tdraw_scrollbar(\n\t\t\tf,\n\t\t\tarea,\n\t\t\t&self.theme,\n\t\t\tself.count_total,\n\t\t\ttable_state.selected().unwrap_or(0),\n\t\t\tOrientation::Vertical,\n\t\t);\n\n\t\tself.table_state.set(table_state);\n\t\tself.current_width.set(area.width.into());\n\t\tself.current_height.set(area.height.into());\n\t}\n\n\tfn hide_stacked(&mut self, stack: bool) {\n\t\tself.hide();\n\n\t\tif stack {\n\t\t\tif let Some(open_request) = self.open_request.clone() {\n\t\t\t\tself.queue.push(InternalEvent::PopupStackPush(\n\t\t\t\t\tStackablePopupOpen::FileRevlog(FileRevOpen {\n\t\t\t\t\t\tfile_path: open_request.file_path,\n\t\t\t\t\t\tselection: self.get_selection(),\n\t\t\t\t\t}),\n\t\t\t\t));\n\t\t\t}\n\t\t} else {\n\t\t\tself.queue.push(InternalEvent::PopupStackPop);\n\t\t}\n\t}\n}\n\nimpl DrawableComponent for FileRevlogPopup {\n\tfn draw(&self, f: &mut Frame, area: Rect) -> Result<()> {\n\t\tif self.visible {\n\t\t\tlet percentages = if self.diff.focused() {\n\t\t\t\t(0, 100)\n\t\t\t} else {\n\t\t\t\t(50, 50)\n\t\t\t};\n\n\t\t\tlet chunks = Layout::default()\n\t\t\t\t.direction(Direction::Horizontal)\n\t\t\t\t.constraints(\n\t\t\t\t\t[\n\t\t\t\t\t\tConstraint::Percentage(percentages.0),\n\t\t\t\t\t\tConstraint::Percentage(percentages.1),\n\t\t\t\t\t]\n\t\t\t\t\t.as_ref(),\n\t\t\t\t)\n\t\t\t\t.split(area);\n\n\t\t\tf.render_widget(Clear, area);\n\n\t\t\tself.draw_revlog(f, chunks[0]);\n\t\t\tself.diff.draw(f, chunks[1])?;\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for FileRevlogPopup {\n\tfn event(&mut self, event: &Event) -> Result<EventState> {\n\t\tif self.is_visible() {\n\t\t\tif event_pump(\n\t\t\t\tevent,\n\t\t\t\tself.components_mut().as_mut_slice(),\n\t\t\t)?\n\t\t\t.is_consumed()\n\t\t\t{\n\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t}\n\n\t\t\tif let Event::Key(key) = event {\n\t\t\t\tif key_match(key, self.key_config.keys.exit_popup) {\n\t\t\t\t\tif self.diff.focused() {\n\t\t\t\t\t\tself.diff.focus(false);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tself.hide_stacked(false);\n\t\t\t\t\t}\n\t\t\t\t} else if key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.move_right,\n\t\t\t\t) && self.can_focus_diff()\n\t\t\t\t{\n\t\t\t\t\tself.diff.focus(true);\n\t\t\t\t} else if key_match(key, self.key_config.keys.enter) {\n\t\t\t\t\tif let Some(commit_id) = self.selected_commit() {\n\t\t\t\t\t\tself.hide_stacked(true);\n\t\t\t\t\t\tself.queue.push(InternalEvent::OpenPopup(\n\t\t\t\t\t\t\tStackablePopupOpen::InspectCommit(\n\t\t\t\t\t\t\t\tInspectCommitOpen::new(commit_id),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t));\n\t\t\t\t\t}\n\t\t\t\t} else if key_match(key, self.key_config.keys.blame) {\n\t\t\t\t\tif let Some(open_request) =\n\t\t\t\t\t\tself.open_request.clone()\n\t\t\t\t\t{\n\t\t\t\t\t\tself.hide_stacked(true);\n\t\t\t\t\t\tself.queue.push(InternalEvent::OpenPopup(\n\t\t\t\t\t\t\tStackablePopupOpen::BlameFile(\n\t\t\t\t\t\t\t\tBlameFileOpen {\n\t\t\t\t\t\t\t\t\tfile_path: open_request.file_path,\n\t\t\t\t\t\t\t\t\tcommit_id: self.selected_commit(),\n\t\t\t\t\t\t\t\t\tselection: None,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t));\n\t\t\t\t\t}\n\t\t\t\t} else if key_match(key, self.key_config.keys.move_up)\n\t\t\t\t{\n\t\t\t\t\tself.move_selection(ScrollType::Up)?;\n\t\t\t\t} else if key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.move_down,\n\t\t\t\t) {\n\t\t\t\t\tself.move_selection(ScrollType::Down)?;\n\t\t\t\t} else if key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.shift_up,\n\t\t\t\t) || key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.home,\n\t\t\t\t) {\n\t\t\t\t\tself.move_selection(ScrollType::Home)?;\n\t\t\t\t} else if key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.shift_down,\n\t\t\t\t) || key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.end,\n\t\t\t\t) {\n\t\t\t\t\tself.move_selection(ScrollType::End)?;\n\t\t\t\t} else if key_match(key, self.key_config.keys.page_up)\n\t\t\t\t{\n\t\t\t\t\tself.move_selection(ScrollType::PageUp)?;\n\t\t\t\t} else if key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.page_down,\n\t\t\t\t) {\n\t\t\t\t\tself.move_selection(ScrollType::PageDown)?;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn Ok(EventState::Consumed);\n\t\t}\n\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.is_visible() || force_all {\n\t\t\tout.push(\n\t\t\t\tCommandInfo::new(\n\t\t\t\t\tstrings::commands::close_popup(&self.key_config),\n\t\t\t\t\ttrue,\n\t\t\t\t\ttrue,\n\t\t\t\t)\n\t\t\t\t.order(1),\n\t\t\t);\n\t\t\tout.push(\n\t\t\t\tCommandInfo::new(\n\t\t\t\t\tstrings::commands::log_details_toggle(\n\t\t\t\t\t\t&self.key_config,\n\t\t\t\t\t),\n\t\t\t\t\ttrue,\n\t\t\t\t\tself.selected_commit().is_some(),\n\t\t\t\t)\n\t\t\t\t.order(1),\n\t\t\t);\n\t\t\tout.push(\n\t\t\t\tCommandInfo::new(\n\t\t\t\t\tstrings::commands::blame_file(&self.key_config),\n\t\t\t\t\ttrue,\n\t\t\t\t\tself.selected_commit().is_some(),\n\t\t\t\t)\n\t\t\t\t.order(1),\n\t\t\t);\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::diff_focus_right(&self.key_config),\n\t\t\t\tself.can_focus_diff(),\n\t\t\t\t!self.diff.focused(),\n\t\t\t));\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::diff_focus_left(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\tself.diff.focused(),\n\t\t\t));\n\t\t}\n\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.visible = false;\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "src/popups/fuzzy_find.rs",
    "content": "use crate::components::{\n\tvisibility_blocking, CommandBlocking, CommandInfo, Component,\n\tDrawableComponent, EventState, FuzzyFinderTarget, InputType,\n\tScrollType, TextInputComponent,\n};\nuse crate::{\n\tapp::Environment,\n\tkeys::{key_match, SharedKeyConfig},\n\tqueue::{InternalEvent, Queue},\n\tstring_utils::trim_length_left,\n\tstrings,\n\tui::{self, style::SharedTheme},\n};\nuse anyhow::Result;\nuse crossterm::event::Event;\nuse fuzzy_matcher::FuzzyMatcher;\nuse ratatui::{\n\tlayout::{Constraint, Direction, Layout, Margin, Rect},\n\ttext::{Line, Span},\n\twidgets::{Block, Borders, Clear},\n\tFrame,\n};\nuse std::borrow::Cow;\nuse unicode_segmentation::UnicodeSegmentation;\n\npub struct FuzzyFindPopup {\n\tqueue: Queue,\n\tvisible: bool,\n\tfind_text: TextInputComponent,\n\tquery: Option<String>,\n\ttheme: SharedTheme,\n\tcontents: Vec<String>,\n\tselection: usize,\n\tselected_index: Option<usize>,\n\tfiltered: Vec<(usize, Vec<usize>)>,\n\tkey_config: SharedKeyConfig,\n\ttarget: Option<FuzzyFinderTarget>,\n}\n\nimpl FuzzyFindPopup {\n\t///\n\tpub fn new(env: &Environment) -> Self {\n\t\tlet mut find_text =\n\t\t\tTextInputComponent::new(env, \"\", \"start typing..\", false)\n\t\t\t\t.with_input_type(InputType::Singleline);\n\t\tfind_text.embed();\n\n\t\tSelf {\n\t\t\tqueue: env.queue.clone(),\n\t\t\tvisible: false,\n\t\t\tquery: None,\n\t\t\tfind_text,\n\t\t\ttheme: env.theme.clone(),\n\t\t\tcontents: Vec::new(),\n\t\t\tfiltered: Vec::new(),\n\t\t\tselected_index: None,\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\tselection: 0,\n\t\t\ttarget: None,\n\t\t}\n\t}\n\n\tfn update_query(&mut self) {\n\t\tif self.find_text.get_text().is_empty() {\n\t\t\tself.set_query(None);\n\t\t} else if self\n\t\t\t.query\n\t\t\t.as_ref()\n\t\t\t.is_none_or(|q| q != self.find_text.get_text())\n\t\t{\n\t\t\tself.set_query(Some(\n\t\t\t\tself.find_text.get_text().to_string(),\n\t\t\t));\n\t\t}\n\t}\n\n\tfn set_query(&mut self, query: Option<String>) {\n\t\tself.query = query;\n\n\t\tself.filtered.clear();\n\n\t\tif let Some(q) = &self.query {\n\t\t\tlet matcher =\n\t\t\t\tfuzzy_matcher::skim::SkimMatcherV2::default();\n\n\t\t\tlet mut contents = self\n\t\t\t\t.contents\n\t\t\t\t.iter()\n\t\t\t\t.enumerate()\n\t\t\t\t.filter_map(|a| {\n\t\t\t\t\tmatcher\n\t\t\t\t\t\t.fuzzy_indices(a.1, q)\n\t\t\t\t\t\t.map(|(score, indices)| (score, a.0, indices))\n\t\t\t\t})\n\t\t\t\t.collect::<Vec<(_, _, _)>>();\n\n\t\t\tcontents.sort_by(|(score1, _, _), (score2, _, _)| {\n\t\t\t\tscore2.cmp(score1)\n\t\t\t});\n\n\t\t\tself.filtered.extend(\n\t\t\t\tcontents.into_iter().map(|entry| (entry.1, entry.2)),\n\t\t\t);\n\t\t}\n\n\t\tself.selection = 0;\n\t\tself.refresh_selection();\n\t}\n\n\tfn refresh_selection(&mut self) {\n\t\tlet selection =\n\t\t\tself.filtered.get(self.selection).map(|a| a.0);\n\n\t\tif self.selected_index != selection {\n\t\t\tself.selected_index = selection;\n\n\t\t\tif let Some(idx) = self.selected_index {\n\t\t\t\tif let Some(target) = self.target {\n\t\t\t\t\tself.queue.push(\n\t\t\t\t\t\tInternalEvent::FuzzyFinderChanged(\n\t\t\t\t\t\t\tidx,\n\t\t\t\t\t\t\tself.contents[idx].clone(),\n\t\t\t\t\t\t\ttarget,\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tpub fn open(\n\t\t&mut self,\n\t\tcontents: Vec<String>,\n\t\ttarget: FuzzyFinderTarget,\n\t) -> Result<()> {\n\t\tself.show()?;\n\t\tself.find_text.show()?;\n\t\tself.find_text.set_text(String::new());\n\t\tself.query = None;\n\t\tself.target = Some(target);\n\t\tif self.contents != contents {\n\t\t\tself.contents = contents;\n\t\t}\n\t\tself.update_query();\n\n\t\tOk(())\n\t}\n\n\tfn move_selection(&mut self, move_type: ScrollType) -> bool {\n\t\tlet new_selection = match move_type {\n\t\t\tScrollType::Up => self.selection.saturating_sub(1),\n\t\t\tScrollType::Down => self.selection.saturating_add(1),\n\t\t\t_ => self.selection,\n\t\t};\n\n\t\tlet new_selection = new_selection\n\t\t\t.clamp(0, self.filtered.len().saturating_sub(1));\n\n\t\tif new_selection != self.selection {\n\t\t\tself.selection = new_selection;\n\t\t\tself.refresh_selection();\n\t\t\treturn true;\n\t\t}\n\n\t\tfalse\n\t}\n\n\t#[inline]\n\tfn draw_matches_list(&self, f: &mut Frame, mut area: Rect) {\n\t\t{\n\t\t\t// Block has two lines up and down which need to be considered\n\t\t\tconst HEIGHT_BLOCK_MARGIN: usize = 2;\n\n\t\t\tlet title = format!(\"Hits: {}\", self.filtered.len());\n\n\t\t\tlet height = usize::from(area.height);\n\t\t\tlet width = usize::from(area.width);\n\n\t\t\tlet list_height =\n\t\t\t\theight.saturating_sub(HEIGHT_BLOCK_MARGIN);\n\n\t\t\tlet scroll_skip =\n\t\t\t\tself.selection.saturating_sub(list_height);\n\n\t\t\tlet items = self\n\t\t\t\t.filtered\n\t\t\t\t.iter()\n\t\t\t\t.skip(scroll_skip)\n\t\t\t\t.take(height)\n\t\t\t\t.map(|(idx, indices)| {\n\t\t\t\t\tlet selected = self\n\t\t\t\t\t\t.selected_index\n\t\t\t\t\t\t.is_some_and(|index| index == *idx);\n\t\t\t\t\tlet full_text =\n\t\t\t\t\t\ttrim_length_left(&self.contents[*idx], width);\n\t\t\t\t\tlet trim_length =\n\t\t\t\t\t\tself.contents[*idx].graphemes(true).count()\n\t\t\t\t\t\t\t- full_text.graphemes(true).count();\n\t\t\t\t\tLine::from(\n\t\t\t\t\t\tfull_text\n\t\t\t\t\t\t\t.graphemes(true)\n\t\t\t\t\t\t\t.enumerate()\n\t\t\t\t\t\t\t.map(|(c_idx, c)| {\n\t\t\t\t\t\t\t\tSpan::styled(\n\t\t\t\t\t\t\t\t\tCow::from(c.to_string()),\n\t\t\t\t\t\t\t\t\tself.theme.text(\n\t\t\t\t\t\t\t\t\t\tselected,\n\t\t\t\t\t\t\t\t\t\tindices.contains(\n\t\t\t\t\t\t\t\t\t\t\t&(c_idx + trim_length),\n\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t.collect::<Vec<_>>(),\n\t\t\t\t\t)\n\t\t\t\t});\n\n\t\t\tui::draw_list_block(\n\t\t\t\tf,\n\t\t\t\tarea,\n\t\t\t\tBlock::default()\n\t\t\t\t\t.title(Span::styled(\n\t\t\t\t\t\ttitle,\n\t\t\t\t\t\tself.theme.title(true),\n\t\t\t\t\t))\n\t\t\t\t\t.borders(Borders::TOP),\n\t\t\t\titems,\n\t\t\t);\n\n\t\t\t// Draw scrollbar when needed\n\t\t\tif self.filtered.len() > list_height {\n\t\t\t\t// Reset list area margin\n\t\t\t\tarea.width += 1;\n\t\t\t\tarea.height += 1;\n\n\t\t\t\tui::draw_scrollbar(\n\t\t\t\t\tf,\n\t\t\t\t\tarea,\n\t\t\t\t\t&self.theme,\n\t\t\t\t\tself.filtered.len().saturating_sub(1),\n\t\t\t\t\tself.selection,\n\t\t\t\t\tui::Orientation::Vertical,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n}\n\nimpl DrawableComponent for FuzzyFindPopup {\n\tfn draw(&self, f: &mut Frame, area: Rect) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tconst MAX_SIZE: (u16, u16) = (50, 20);\n\n\t\t\tlet any_hits = !self.filtered.is_empty();\n\n\t\t\tlet area = ui::centered_rect_absolute(\n\t\t\t\tMAX_SIZE.0, MAX_SIZE.1, area,\n\t\t\t);\n\n\t\t\tlet area = if any_hits {\n\t\t\t\tarea\n\t\t\t} else {\n\t\t\t\tLayout::default()\n\t\t\t\t\t.direction(Direction::Vertical)\n\t\t\t\t\t.constraints(\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\tConstraint::Length(3),\n\t\t\t\t\t\t\tConstraint::Percentage(100),\n\t\t\t\t\t\t]\n\t\t\t\t\t\t.as_ref(),\n\t\t\t\t\t)\n\t\t\t\t\t.split(area)[0]\n\t\t\t};\n\n\t\t\tf.render_widget(Clear, area);\n\t\t\tf.render_widget(\n\t\t\t\tBlock::default()\n\t\t\t\t\t.borders(Borders::all())\n\t\t\t\t\t.style(self.theme.title(true))\n\t\t\t\t\t.title(Span::styled(\n\t\t\t\t\t\tstrings::POPUP_TITLE_FUZZY_FIND,\n\t\t\t\t\t\tself.theme.title(true),\n\t\t\t\t\t)),\n\t\t\t\tarea,\n\t\t\t);\n\n\t\t\tlet chunks = Layout::default()\n\t\t\t\t.direction(Direction::Vertical)\n\t\t\t\t.constraints(\n\t\t\t\t\t[\n\t\t\t\t\t\tConstraint::Length(1),\n\t\t\t\t\t\tConstraint::Percentage(100),\n\t\t\t\t\t]\n\t\t\t\t\t.as_ref(),\n\t\t\t\t)\n\t\t\t\t.split(area.inner(Margin {\n\t\t\t\t\thorizontal: 1,\n\t\t\t\t\tvertical: 1,\n\t\t\t\t}));\n\n\t\t\tself.find_text.draw(f, chunks[0])?;\n\n\t\t\tif any_hits {\n\t\t\t\tself.draw_matches_list(f, chunks[1]);\n\t\t\t}\n\t\t}\n\t\tOk(())\n\t}\n}\n\nimpl Component for FuzzyFindPopup {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.is_visible() || force_all {\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::scroll_popup(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\ttrue,\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::close_fuzzy_finder(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t),\n\t\t\t\ttrue,\n\t\t\t\ttrue,\n\t\t\t));\n\t\t}\n\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(\n\t\t&mut self,\n\t\tevent: &crossterm::event::Event,\n\t) -> Result<EventState> {\n\t\tif self.is_visible() {\n\t\t\tif let Event::Key(key) = event {\n\t\t\t\tif key_match(key, self.key_config.keys.exit_popup)\n\t\t\t\t\t|| key_match(key, self.key_config.keys.enter)\n\t\t\t\t{\n\t\t\t\t\tself.hide();\n\t\t\t\t} else if key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.popup_down,\n\t\t\t\t) {\n\t\t\t\t\tself.move_selection(ScrollType::Down);\n\t\t\t\t} else if key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.popup_up,\n\t\t\t\t) {\n\t\t\t\t\tself.move_selection(ScrollType::Up);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif self.find_text.event(event)?.is_consumed() {\n\t\t\t\tself.update_query();\n\t\t\t}\n\n\t\t\treturn Ok(EventState::Consumed);\n\t\t}\n\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.visible = false;\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "src/popups/goto_line.rs",
    "content": "use crate::{\n\tapp::Environment,\n\tcomponents::{\n\t\tvisibility_blocking, CommandBlocking, CommandInfo, Component,\n\t\tDrawableComponent, EventState,\n\t},\n\tkeys::{key_match, SharedKeyConfig},\n\tqueue::{InternalEvent, Queue},\n\tstrings,\n\tui::{self, style::SharedTheme},\n};\n\nuse ratatui::{\n\tlayout::Rect,\n\tstyle::{Color, Style},\n\twidgets::{Block, Clear, Paragraph},\n\tFrame,\n};\n\nuse anyhow::Result;\n\nuse crossterm::event::{Event, KeyCode};\n\npub struct GotoLinePopup {\n\tvisible: bool,\n\tinput: String,\n\tline_number: usize,\n\tkey_config: SharedKeyConfig,\n\tqueue: Queue,\n\ttheme: SharedTheme,\n\tinvalid_input: bool,\n\tmax_line: usize,\n}\n\nimpl GotoLinePopup {\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\tvisible: false,\n\t\t\tinput: String::new(),\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\tqueue: env.queue.clone(),\n\t\t\ttheme: env.theme.clone(),\n\t\t\tinvalid_input: false,\n\t\t\tmax_line: 0,\n\t\t\tline_number: 0,\n\t\t}\n\t}\n\n\tpub const fn open(&mut self, max_line: usize) {\n\t\tself.visible = true;\n\t\tself.max_line = max_line;\n\t}\n}\n\nimpl Component for GotoLinePopup {\n\t///\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.is_visible() || force_all {\n\t\t\tout.push(\n\t\t\t\tCommandInfo::new(\n\t\t\t\t\tstrings::commands::close_popup(&self.key_config),\n\t\t\t\t\ttrue,\n\t\t\t\t\ttrue,\n\t\t\t\t)\n\t\t\t\t.order(1),\n\t\t\t);\n\t\t\tout.push(\n\t\t\t\tCommandInfo::new(\n\t\t\t\t\tstrings::commands::goto_line(&self.key_config),\n\t\t\t\t\ttrue,\n\t\t\t\t\ttrue,\n\t\t\t\t)\n\t\t\t\t.order(1),\n\t\t\t);\n\t\t}\n\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\t///\n\tfn event(&mut self, event: &Event) -> Result<EventState> {\n\t\tif self.is_visible() {\n\t\t\tif let Event::Key(key) = event {\n\t\t\t\tif key_match(key, self.key_config.keys.exit_popup) {\n\t\t\t\t\tself.visible = false;\n\t\t\t\t\tself.input.clear();\n\t\t\t\t} else if let KeyCode::Char(c) = key.code {\n\t\t\t\t\tif c.is_ascii_digit() || c == '-' {\n\t\t\t\t\t\tself.input.push(c);\n\t\t\t\t\t}\n\t\t\t\t} else if key.code == KeyCode::Backspace {\n\t\t\t\t\tself.input.pop();\n\t\t\t\t} else if key_match(key, self.key_config.keys.enter) {\n\t\t\t\t\tself.visible = false;\n\t\t\t\t\tif self.invalid_input {\n\t\t\t\t\t\tself.queue.push(InternalEvent::ShowErrorMsg(\n                            format!(\"Invalid input: only numbers between -{} and {} (included) are allowed (-1 denotes the last line, -2 denotes the second to last line, and so on)\",self.max_line + 1, self.max_line))\n                            ,\n                        );\n\t\t\t\t\t} else if !self.input.is_empty() {\n\t\t\t\t\t\tself.queue.push(InternalEvent::GotoLine(\n\t\t\t\t\t\t\tself.line_number,\n\t\t\t\t\t\t));\n\t\t\t\t\t}\n\t\t\t\t\tself.input.clear();\n\t\t\t\t\tself.invalid_input = false;\n\t\t\t\t}\n\t\t\t}\n\t\t\tmatch self.input.parse::<isize>() {\n\t\t\t\tOk(input) => {\n\t\t\t\t\tlet mut max_value_allowed_abs = self.max_line;\n\t\t\t\t\t// negative indices are 1 based\n\t\t\t\t\tif input < 0 {\n\t\t\t\t\t\tmax_value_allowed_abs += 1;\n\t\t\t\t\t}\n\t\t\t\t\tlet input_abs = input.unsigned_abs();\n\t\t\t\t\tif input_abs > max_value_allowed_abs {\n\t\t\t\t\t\tself.invalid_input = true;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tself.invalid_input = false;\n\t\t\t\t\t\tself.line_number = if input >= 0 {\n\t\t\t\t\t\t\tinput_abs\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tmax_value_allowed_abs - input_abs\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tErr(_) => {\n\t\t\t\t\tif !self.input.is_empty() {\n\t\t\t\t\t\tself.invalid_input = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn Ok(EventState::Consumed);\n\t\t}\n\t\tOk(EventState::NotConsumed)\n\t}\n}\n\nimpl DrawableComponent for GotoLinePopup {\n\tfn draw(&self, f: &mut Frame, area: Rect) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tlet style = if self.invalid_input {\n\t\t\t\tStyle::default().fg(Color::Red)\n\t\t\t} else {\n\t\t\t\tself.theme.text(true, false)\n\t\t\t};\n\t\t\tlet input = Paragraph::new(self.input.as_str())\n\t\t\t\t.style(style)\n\t\t\t\t.block(Block::bordered().title(\"Go to\"));\n\n\t\t\tlet input_area = ui::centered_rect_absolute(15, 3, area);\n\t\t\tf.render_widget(Clear, input_area);\n\t\t\tf.render_widget(input, input_area);\n\t\t}\n\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "src/popups/help.rs",
    "content": "use crate::components::{\n\tvisibility_blocking, CommandBlocking, CommandInfo, Component,\n\tDrawableComponent, EventState,\n};\nuse crate::{\n\tapp::Environment,\n\tkeys::{key_match, SharedKeyConfig},\n\tstrings, ui,\n};\nuse anyhow::Result;\nuse asyncgit::hash;\nuse crossterm::event::Event;\nuse itertools::Itertools;\nuse ratatui::{\n\tlayout::{Alignment, Constraint, Direction, Layout, Rect},\n\tstyle::{Modifier, Style},\n\ttext::{Line, Span},\n\twidgets::{Block, BorderType, Borders, Clear, Paragraph},\n\tFrame,\n};\nuse std::{borrow::Cow, cmp};\nuse ui::style::SharedTheme;\n\n///\npub struct HelpPopup {\n\tcmds: Vec<CommandInfo>,\n\tvisible: bool,\n\tselection: u16,\n\ttheme: SharedTheme,\n\tkey_config: SharedKeyConfig,\n}\n\nimpl DrawableComponent for HelpPopup {\n\tfn draw(&self, f: &mut Frame, _rect: Rect) -> Result<()> {\n\t\tif self.visible {\n\t\t\tconst SIZE: (u16, u16) = (65, 24);\n\t\t\tlet scroll_threshold = SIZE.1 / 3;\n\t\t\tlet scroll =\n\t\t\t\tself.selection.saturating_sub(scroll_threshold);\n\n\t\t\tlet area =\n\t\t\t\tui::centered_rect_absolute(SIZE.0, SIZE.1, f.area());\n\n\t\t\tf.render_widget(Clear, area);\n\t\t\tf.render_widget(\n\t\t\t\tBlock::default()\n\t\t\t\t\t.title(strings::help_title(&self.key_config))\n\t\t\t\t\t.borders(Borders::ALL)\n\t\t\t\t\t.border_type(BorderType::Thick),\n\t\t\t\tarea,\n\t\t\t);\n\n\t\t\tlet chunks = Layout::default()\n\t\t\t\t.vertical_margin(1)\n\t\t\t\t.horizontal_margin(1)\n\t\t\t\t.direction(Direction::Vertical)\n\t\t\t\t.constraints(\n\t\t\t\t\t[Constraint::Min(1), Constraint::Length(1)]\n\t\t\t\t\t\t.as_ref(),\n\t\t\t\t)\n\t\t\t\t.split(area);\n\n\t\t\tf.render_widget(\n\t\t\t\tParagraph::new(self.get_text())\n\t\t\t\t\t.scroll((scroll, 0))\n\t\t\t\t\t.alignment(Alignment::Left),\n\t\t\t\tchunks[0],\n\t\t\t);\n\n\t\t\tui::draw_scrollbar(\n\t\t\t\tf,\n\t\t\t\tarea,\n\t\t\t\t&self.theme,\n\t\t\t\tself.cmds.len(),\n\t\t\t\tself.selection as usize,\n\t\t\t\tui::Orientation::Vertical,\n\t\t\t);\n\n\t\t\tf.render_widget(\n\t\t\t\tParagraph::new(Line::from(vec![Span::styled(\n\t\t\t\t\tCow::from(format!(\n\t\t\t\t\t\t\"gitui {}\",\n\t\t\t\t\t\tenv!(\"GITUI_BUILD_NAME\"),\n\t\t\t\t\t)),\n\t\t\t\t\tStyle::default(),\n\t\t\t\t)]))\n\t\t\t\t.alignment(Alignment::Right),\n\t\t\t\tchunks[1],\n\t\t\t);\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for HelpPopup {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\t// only if help is open we have no other commands available\n\t\tif self.visible && !force_all {\n\t\t\tout.clear();\n\t\t}\n\n\t\tif self.visible {\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::scroll(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\ttrue,\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::close_popup(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\ttrue,\n\t\t\t));\n\t\t}\n\n\t\tif !self.visible || force_all {\n\t\t\tout.push(\n\t\t\t\tCommandInfo::new(\n\t\t\t\t\tstrings::commands::help_open(&self.key_config),\n\t\t\t\t\ttrue,\n\t\t\t\t\ttrue,\n\t\t\t\t)\n\t\t\t\t.order(99),\n\t\t\t);\n\t\t}\n\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(&mut self, ev: &Event) -> Result<EventState> {\n\t\tif self.visible {\n\t\t\tif let Event::Key(e) = ev {\n\t\t\t\tif key_match(e, self.key_config.keys.exit_popup) {\n\t\t\t\t\tself.hide();\n\t\t\t\t} else if key_match(e, self.key_config.keys.move_down)\n\t\t\t\t{\n\t\t\t\t\tself.move_selection(true);\n\t\t\t\t} else if key_match(e, self.key_config.keys.move_up) {\n\t\t\t\t\tself.move_selection(false);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tOk(EventState::Consumed)\n\t\t} else if let Event::Key(k) = ev {\n\t\t\tif key_match(k, self.key_config.keys.open_help) {\n\t\t\t\tself.show()?;\n\t\t\t\tOk(EventState::Consumed)\n\t\t\t} else {\n\t\t\t\tOk(EventState::NotConsumed)\n\t\t\t}\n\t\t} else {\n\t\t\tOk(EventState::NotConsumed)\n\t\t}\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.visible = false;\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\n\t\tOk(())\n\t}\n}\n\nimpl HelpPopup {\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\tcmds: vec![],\n\t\t\tvisible: false,\n\t\t\tselection: 0,\n\t\t\ttheme: env.theme.clone(),\n\t\t\tkey_config: env.key_config.clone(),\n\t\t}\n\t}\n\t///\n\tpub fn set_cmds(&mut self, cmds: Vec<CommandInfo>) {\n\t\tself.cmds = cmds\n\t\t\t.into_iter()\n\t\t\t.filter(|e| !e.text.hide_help)\n\t\t\t.collect::<Vec<_>>();\n\t\tself.cmds.sort_by_key(|e| e.text.clone());\n\t\tself.cmds.dedup_by_key(|e| e.text.clone());\n\t\tself.cmds.sort_by_key(|e| hash(&e.text.group));\n\t}\n\n\tfn move_selection(&mut self, inc: bool) {\n\t\tlet mut new_selection = self.selection;\n\n\t\tnew_selection = if inc {\n\t\t\tnew_selection.saturating_add(1)\n\t\t} else {\n\t\t\tnew_selection.saturating_sub(1)\n\t\t};\n\t\tnew_selection = cmp::max(new_selection, 0);\n\n\t\tif let Ok(max) =\n\t\t\tu16::try_from(self.cmds.len().saturating_sub(1))\n\t\t{\n\t\t\tself.selection = cmp::min(new_selection, max);\n\t\t}\n\t}\n\n\tfn get_text(&self) -> Vec<Line<'_>> {\n\t\tlet mut txt: Vec<Line> = Vec::new();\n\n\t\tlet mut processed = 0_u16;\n\n\t\tfor (key, group) in\n\t\t\t&self.cmds.iter().chunk_by(|e| e.text.group)\n\t\t{\n\t\t\ttxt.push(Line::from(Span::styled(\n\t\t\t\tCow::from(key.to_string()),\n\t\t\t\tStyle::default().add_modifier(Modifier::REVERSED),\n\t\t\t)));\n\n\t\t\tfor command_info in group {\n\t\t\t\tlet is_selected = self.selection == processed;\n\n\t\t\t\tprocessed += 1;\n\n\t\t\t\ttxt.push(Line::from(Span::styled(\n\t\t\t\t\tCow::from(if is_selected {\n\t\t\t\t\t\tformat!(\">{}\", command_info.text.name)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tformat!(\" {}\", command_info.text.name)\n\t\t\t\t\t}),\n\t\t\t\t\tself.theme.text(true, is_selected),\n\t\t\t\t)));\n\n\t\t\t\tif is_selected {\n\t\t\t\t\ttxt.push(Line::from(Span::styled(\n\t\t\t\t\t\tCow::from(format!(\n\t\t\t\t\t\t\t\"  {}\\n\",\n\t\t\t\t\t\t\tcommand_info.text.desc\n\t\t\t\t\t\t)),\n\t\t\t\t\t\tself.theme.text(true, is_selected),\n\t\t\t\t\t)));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\ttxt\n\t}\n}\n"
  },
  {
    "path": "src/popups/inspect_commit.rs",
    "content": "use crate::components::{\n\tcommand_pump, event_pump, visibility_blocking, CommandBlocking,\n\tCommandInfo, CommitDetailsComponent, Component, DiffComponent,\n\tDrawableComponent, EventState,\n};\nuse crate::{\n\taccessors,\n\tapp::Environment,\n\tkeys::{key_match, SharedKeyConfig},\n\toptions::SharedOptions,\n\tqueue::{InternalEvent, Queue, StackablePopupOpen},\n\tstrings,\n};\nuse anyhow::Result;\nuse asyncgit::{\n\tsync::{CommitId, CommitTags},\n\tAsyncDiff, AsyncGitNotification, DiffParams, DiffType,\n};\nuse crossterm::event::Event;\nuse ratatui::{\n\tlayout::{Constraint, Direction, Layout, Rect},\n\twidgets::Clear,\n\tFrame,\n};\n\nuse super::FileTreeOpen;\n\n#[derive(Clone, Debug)]\npub struct InspectCommitOpen {\n\tpub commit_id: CommitId,\n\t/// in case we wanna compare\n\tpub compare_id: Option<CommitId>,\n\tpub tags: Option<CommitTags>,\n}\n\nimpl InspectCommitOpen {\n\tpub const fn new(commit_id: CommitId) -> Self {\n\t\tSelf {\n\t\t\tcommit_id,\n\t\t\tcompare_id: None,\n\t\t\ttags: None,\n\t\t}\n\t}\n\n\tpub const fn new_with_tags(\n\t\tcommit_id: CommitId,\n\t\ttags: Option<CommitTags>,\n\t) -> Self {\n\t\tSelf {\n\t\t\tcommit_id,\n\t\t\tcompare_id: None,\n\t\t\ttags,\n\t\t}\n\t}\n}\n\npub struct InspectCommitPopup {\n\tqueue: Queue,\n\topen_request: Option<InspectCommitOpen>,\n\tdiff: DiffComponent,\n\tdetails: CommitDetailsComponent,\n\tgit_diff: AsyncDiff,\n\tvisible: bool,\n\tkey_config: SharedKeyConfig,\n\toptions: SharedOptions,\n}\n\nimpl DrawableComponent for InspectCommitPopup {\n\tfn draw(&self, f: &mut Frame, rect: Rect) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tlet percentages = if self.diff.focused() {\n\t\t\t\t(0, 100)\n\t\t\t} else {\n\t\t\t\t(50, 50)\n\t\t\t};\n\n\t\t\tlet chunks = Layout::default()\n\t\t\t\t.direction(Direction::Horizontal)\n\t\t\t\t.constraints(\n\t\t\t\t\t[\n\t\t\t\t\t\tConstraint::Percentage(percentages.0),\n\t\t\t\t\t\tConstraint::Percentage(percentages.1),\n\t\t\t\t\t]\n\t\t\t\t\t.as_ref(),\n\t\t\t\t)\n\t\t\t\t.split(rect);\n\n\t\t\tf.render_widget(Clear, rect);\n\n\t\t\tself.details.draw(f, chunks[0])?;\n\t\t\tself.diff.draw(f, chunks[1])?;\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for InspectCommitPopup {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.is_visible() || force_all {\n\t\t\tcommand_pump(\n\t\t\t\tout,\n\t\t\t\tforce_all,\n\t\t\t\tself.components().as_slice(),\n\t\t\t);\n\n\t\t\tout.push(\n\t\t\t\tCommandInfo::new(\n\t\t\t\t\tstrings::commands::close_popup(&self.key_config),\n\t\t\t\t\ttrue,\n\t\t\t\t\ttrue,\n\t\t\t\t)\n\t\t\t\t.order(1),\n\t\t\t);\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::diff_focus_right(&self.key_config),\n\t\t\t\tself.can_focus_diff(),\n\t\t\t\t!self.diff.focused() || force_all,\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::close_popup(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\tself.diff.focused() || force_all,\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::inspect_file_tree(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t),\n\t\t\t\ttrue,\n\t\t\t\ttrue,\n\t\t\t));\n\t\t}\n\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(&mut self, ev: &Event) -> Result<EventState> {\n\t\tif self.is_visible() {\n\t\t\tif event_pump(ev, self.components_mut().as_mut_slice())?\n\t\t\t\t.is_consumed()\n\t\t\t{\n\t\t\t\tif !self.details.is_visible() {\n\t\t\t\t\tself.hide_stacked(true);\n\t\t\t\t}\n\n\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t}\n\n\t\t\tif let Event::Key(e) = ev {\n\t\t\t\tif key_match(e, self.key_config.keys.exit_popup) {\n\t\t\t\t\tif self.diff.focused() {\n\t\t\t\t\t\tself.details.focus(true);\n\t\t\t\t\t\tself.diff.focus(false);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tself.hide_stacked(false);\n\t\t\t\t\t}\n\t\t\t\t} else if key_match(\n\t\t\t\t\te,\n\t\t\t\t\tself.key_config.keys.move_right,\n\t\t\t\t) && self.can_focus_diff()\n\t\t\t\t{\n\t\t\t\t\tself.details.focus(false);\n\t\t\t\t\tself.diff.focus(true);\n\t\t\t\t} else if key_match(e, self.key_config.keys.move_left)\n\t\t\t\t{\n\t\t\t\t\tself.hide_stacked(false);\n\t\t\t\t} else if key_match(\n\t\t\t\t\te,\n\t\t\t\t\tself.key_config.keys.open_file_tree,\n\t\t\t\t) {\n\t\t\t\t\tif let Some(commit_id) = self\n\t\t\t\t\t\t.open_request\n\t\t\t\t\t\t.as_ref()\n\t\t\t\t\t\t.map(|open_commit| open_commit.commit_id)\n\t\t\t\t\t{\n\t\t\t\t\t\tself.hide_stacked(true);\n\t\t\t\t\t\tself.queue.push(InternalEvent::OpenPopup(\n\t\t\t\t\t\t\tStackablePopupOpen::FileTree(\n\t\t\t\t\t\t\t\tFileTreeOpen::new(commit_id),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t));\n\t\t\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t\t\t}\n\t\t\t\t\treturn Ok(EventState::NotConsumed);\n\t\t\t\t}\n\n\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t}\n\t\t}\n\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\tfn hide(&mut self) {\n\t\tself.visible = false;\n\t}\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\t\tself.details.show()?;\n\t\tself.details.focus(true);\n\t\tself.diff.focus(false);\n\t\tself.update()?;\n\t\tOk(())\n\t}\n}\n\nimpl InspectCommitPopup {\n\taccessors!(self, [diff, details]);\n\n\t///\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\tqueue: env.queue.clone(),\n\t\t\tdetails: CommitDetailsComponent::new(env),\n\t\t\tdiff: DiffComponent::new(env, true),\n\t\t\topen_request: None,\n\t\t\tgit_diff: AsyncDiff::new(\n\t\t\t\tenv.repo.borrow().clone(),\n\t\t\t\t&env.sender_git,\n\t\t\t),\n\t\t\tvisible: false,\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\toptions: env.options.clone(),\n\t\t}\n\t}\n\n\t///\n\tpub fn open(&mut self, open: InspectCommitOpen) -> Result<()> {\n\t\tself.open_request = Some(open);\n\t\tself.show()?;\n\n\t\tOk(())\n\t}\n\n\t///\n\tpub fn any_work_pending(&self) -> bool {\n\t\tself.git_diff.is_pending() || self.details.any_work_pending()\n\t}\n\n\t///\n\tpub fn update_git(\n\t\t&mut self,\n\t\tev: AsyncGitNotification,\n\t) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tif ev == AsyncGitNotification::CommitFiles {\n\t\t\t\tself.update()?;\n\t\t\t} else if ev == AsyncGitNotification::Diff {\n\t\t\t\tself.update_diff()?;\n\t\t\t}\n\t\t}\n\n\t\tOk(())\n\t}\n\n\t/// called when any tree component changed selection\n\tpub fn update_diff(&mut self) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tif let Some(request) = &self.open_request {\n\t\t\t\tif let Some(f) = self.details.files().selection_file()\n\t\t\t\t{\n\t\t\t\t\tlet diff_params = DiffParams {\n\t\t\t\t\t\tpath: f.path.clone(),\n\t\t\t\t\t\tdiff_type: DiffType::Commit(\n\t\t\t\t\t\t\trequest.commit_id,\n\t\t\t\t\t\t),\n\t\t\t\t\t\toptions: self.options.borrow().diff_options(),\n\t\t\t\t\t};\n\n\t\t\t\t\tif let Some((params, last)) =\n\t\t\t\t\t\tself.git_diff.last()?\n\t\t\t\t\t{\n\t\t\t\t\t\tif params == diff_params {\n\t\t\t\t\t\t\tself.diff.update(f.path, false, last);\n\t\t\t\t\t\t\treturn Ok(());\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tself.git_diff.request(diff_params)?;\n\t\t\t\t\tself.diff.clear(true);\n\t\t\t\t\treturn Ok(());\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tself.diff.clear(false);\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn update(&mut self) -> Result<()> {\n\t\tif let Some(request) = &self.open_request {\n\t\t\tself.details.set_commits(\n\t\t\t\tSome(request.commit_id.into()),\n\t\t\t\trequest.tags.as_ref(),\n\t\t\t)?;\n\t\t\tself.update_diff()?;\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn can_focus_diff(&self) -> bool {\n\t\tself.details.files().selection_file().is_some()\n\t}\n\n\tfn hide_stacked(&mut self, stack: bool) {\n\t\tself.hide();\n\n\t\tif stack {\n\t\t\tif let Some(open_request) = self.open_request.take() {\n\t\t\t\tself.queue.push(InternalEvent::PopupStackPush(\n\t\t\t\t\tStackablePopupOpen::InspectCommit(open_request),\n\t\t\t\t));\n\t\t\t}\n\t\t} else {\n\t\t\tself.queue.push(InternalEvent::PopupStackPop);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/popups/log_search.rs",
    "content": "use crate::components::{\n\tvisibility_blocking, CommandBlocking, CommandInfo, Component,\n\tDrawableComponent, EventState, InputType, TextInputComponent,\n};\nuse crate::{\n\tapp::Environment,\n\tkeys::{key_match, SharedKeyConfig},\n\tqueue::{InternalEvent, Queue},\n\tstrings::{self, POPUP_COMMIT_SHA_INVALID},\n\tui::{self, style::SharedTheme},\n};\nuse anyhow::Result;\nuse asyncgit::sync::{\n\tCommitId, LogFilterSearchOptions, RepoPathRef, SearchFields,\n\tSearchOptions,\n};\nuse crossterm::event::Event;\nuse easy_cast::Cast;\nuse ratatui::{\n\tlayout::{\n\t\tAlignment, Constraint, Direction, Layout, Margin, Rect,\n\t},\n\ttext::{Line, Span},\n\twidgets::{Block, Borders, Clear, Paragraph},\n\tFrame,\n};\n\nenum Selection {\n\tEnterText,\n\tFuzzyOption,\n\tCaseOption,\n\tSummarySearch,\n\tMessageBodySearch,\n\tFilenameSearch,\n\tAuthorsSearch,\n}\n\nenum PopupMode {\n\tSearch,\n\tJumpCommitSha,\n}\n\npub struct LogSearchPopupPopup {\n\trepo: RepoPathRef,\n\tqueue: Queue,\n\tvisible: bool,\n\tmode: PopupMode,\n\tselection: Selection,\n\tkey_config: SharedKeyConfig,\n\tfind_text: TextInputComponent,\n\toptions: (SearchFields, SearchOptions),\n\ttheme: SharedTheme,\n\tjump_commit_id: Option<CommitId>,\n}\n\nimpl LogSearchPopupPopup {\n\t///\n\tpub fn new(env: &Environment) -> Self {\n\t\tlet mut find_text =\n\t\t\tTextInputComponent::new(env, \"\", \"search text\", false)\n\t\t\t\t.with_input_type(InputType::Singleline);\n\t\tfind_text.embed();\n\t\tfind_text.enabled(true);\n\n\t\tSelf {\n\t\t\trepo: env.repo.clone(),\n\t\t\tqueue: env.queue.clone(),\n\t\t\tvisible: false,\n\t\t\tmode: PopupMode::Search,\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\toptions: (\n\t\t\t\tSearchFields::default(),\n\t\t\t\tSearchOptions::default(),\n\t\t\t),\n\t\t\ttheme: env.theme.clone(),\n\t\t\tfind_text,\n\t\t\tselection: Selection::EnterText,\n\t\t\tjump_commit_id: None,\n\t\t}\n\t}\n\n\tpub fn open(&mut self) -> Result<()> {\n\t\tself.show()?;\n\t\tself.selection = Selection::EnterText;\n\t\tself.find_text.show()?;\n\t\tself.find_text.set_text(String::new());\n\t\tself.find_text.enabled(true);\n\n\t\tself.set_mode(&PopupMode::Search);\n\n\t\tOk(())\n\t}\n\n\tfn set_mode(&mut self, mode: &PopupMode) {\n\t\tself.find_text.set_text(String::new());\n\n\t\tmatch mode {\n\t\t\tPopupMode::Search => {\n\t\t\t\tself.mode = PopupMode::Search;\n\t\t\t\tself.find_text.set_default_msg(\"search text\".into());\n\t\t\t\tself.find_text.enabled(matches!(\n\t\t\t\t\tself.selection,\n\t\t\t\t\tSelection::EnterText\n\t\t\t\t));\n\t\t\t}\n\t\t\tPopupMode::JumpCommitSha => {\n\t\t\t\tself.mode = PopupMode::JumpCommitSha;\n\t\t\t\tself.jump_commit_id = None;\n\t\t\t\tself.find_text.set_default_msg(\"commit sha\".into());\n\t\t\t\tself.find_text.enabled(false);\n\t\t\t\tself.selection = Selection::EnterText;\n\t\t\t}\n\t\t}\n\t}\n\n\tfn execute_confirm(&mut self) {\n\t\tself.hide();\n\n\t\tif !self.is_valid() {\n\t\t\treturn;\n\t\t}\n\n\t\tmatch self.mode {\n\t\t\tPopupMode::Search => {\n\t\t\t\tself.queue.push(InternalEvent::CommitSearch(\n\t\t\t\t\tLogFilterSearchOptions {\n\t\t\t\t\t\tfields: self.options.0,\n\t\t\t\t\t\toptions: self.options.1,\n\t\t\t\t\t\tsearch_pattern: self\n\t\t\t\t\t\t\t.find_text\n\t\t\t\t\t\t\t.get_text()\n\t\t\t\t\t\t\t.to_string(),\n\t\t\t\t\t},\n\t\t\t\t));\n\t\t\t}\n\t\t\tPopupMode::JumpCommitSha => {\n\t\t\t\tlet commit_id = self.jump_commit_id\n                    .expect(\"Commit id must have value here because it's already validated\");\n\t\t\t\tself.queue.push(InternalEvent::SelectCommitInRevlog(\n\t\t\t\t\tcommit_id,\n\t\t\t\t));\n\t\t\t}\n\t\t}\n\t}\n\n\tfn is_valid(&self) -> bool {\n\t\tmatch self.mode {\n\t\t\tPopupMode::Search => {\n\t\t\t\t!self.find_text.get_text().trim().is_empty()\n\t\t\t}\n\t\t\tPopupMode::JumpCommitSha => self.jump_commit_id.is_some(),\n\t\t}\n\t}\n\n\tfn validate_commit_sha(&mut self) {\n\t\tlet path = self.repo.borrow();\n\t\tif let Ok(commit_id) = CommitId::from_revision(\n\t\t\t&path,\n\t\t\tself.find_text.get_text().trim(),\n\t\t) {\n\t\t\tself.jump_commit_id = Some(commit_id);\n\t\t} else {\n\t\t\tself.jump_commit_id = None;\n\t\t}\n\t}\n\n\tfn get_text_options(&self) -> Vec<Line<'_>> {\n\t\tlet x_summary =\n\t\t\tif self.options.0.contains(SearchFields::MESSAGE_SUMMARY)\n\t\t\t{\n\t\t\t\t\"X\"\n\t\t\t} else {\n\t\t\t\t\" \"\n\t\t\t};\n\n\t\tlet x_body =\n\t\t\tif self.options.0.contains(SearchFields::MESSAGE_BODY) {\n\t\t\t\t\"X\"\n\t\t\t} else {\n\t\t\t\t\" \"\n\t\t\t};\n\n\t\tlet x_files =\n\t\t\tif self.options.0.contains(SearchFields::FILENAMES) {\n\t\t\t\t\"X\"\n\t\t\t} else {\n\t\t\t\t\" \"\n\t\t\t};\n\n\t\tlet x_authors =\n\t\t\tif self.options.0.contains(SearchFields::AUTHORS) {\n\t\t\t\t\"X\"\n\t\t\t} else {\n\t\t\t\t\" \"\n\t\t\t};\n\n\t\tlet x_opt_fuzzy =\n\t\t\tif self.options.1.contains(SearchOptions::FUZZY_SEARCH) {\n\t\t\t\t\"X\"\n\t\t\t} else {\n\t\t\t\t\" \"\n\t\t\t};\n\n\t\tlet x_opt_casesensitive =\n\t\t\tif self.options.1.contains(SearchOptions::CASE_SENSITIVE)\n\t\t\t{\n\t\t\t\t\"X\"\n\t\t\t} else {\n\t\t\t\t\" \"\n\t\t\t};\n\n\t\tvec![\n\t\t\tLine::from(vec![Span::styled(\n\t\t\t\tformat!(\"[{x_opt_fuzzy}] fuzzy search\"),\n\t\t\t\tself.theme.text(\n\t\t\t\t\tmatches!(self.selection, Selection::FuzzyOption),\n\t\t\t\t\tfalse,\n\t\t\t\t),\n\t\t\t)]),\n\t\t\tLine::from(vec![Span::styled(\n\t\t\t\tformat!(\"[{x_opt_casesensitive}] case sensitive\"),\n\t\t\t\tself.theme.text(\n\t\t\t\t\tmatches!(self.selection, Selection::CaseOption),\n\t\t\t\t\tfalse,\n\t\t\t\t),\n\t\t\t)]),\n\t\t\tLine::from(vec![Span::styled(\n\t\t\t\tformat!(\"[{x_summary}] summary\"),\n\t\t\t\tself.theme.text(\n\t\t\t\t\tmatches!(\n\t\t\t\t\t\tself.selection,\n\t\t\t\t\t\tSelection::SummarySearch\n\t\t\t\t\t),\n\t\t\t\t\tfalse,\n\t\t\t\t),\n\t\t\t)]),\n\t\t\tLine::from(vec![Span::styled(\n\t\t\t\tformat!(\"[{x_body}] message body\"),\n\t\t\t\tself.theme.text(\n\t\t\t\t\tmatches!(\n\t\t\t\t\t\tself.selection,\n\t\t\t\t\t\tSelection::MessageBodySearch\n\t\t\t\t\t),\n\t\t\t\t\tfalse,\n\t\t\t\t),\n\t\t\t)]),\n\t\t\tLine::from(vec![Span::styled(\n\t\t\t\tformat!(\"[{x_files}] committed files\"),\n\t\t\t\tself.theme.text(\n\t\t\t\t\tmatches!(\n\t\t\t\t\t\tself.selection,\n\t\t\t\t\t\tSelection::FilenameSearch\n\t\t\t\t\t),\n\t\t\t\t\tfalse,\n\t\t\t\t),\n\t\t\t)]),\n\t\t\tLine::from(vec![Span::styled(\n\t\t\t\tformat!(\"[{x_authors}] authors\"),\n\t\t\t\tself.theme.text(\n\t\t\t\t\tmatches!(\n\t\t\t\t\t\tself.selection,\n\t\t\t\t\t\tSelection::AuthorsSearch\n\t\t\t\t\t),\n\t\t\t\t\tfalse,\n\t\t\t\t),\n\t\t\t)]),\n\t\t]\n\t}\n\n\tconst fn option_selected(&self) -> bool {\n\t\t!matches!(self.selection, Selection::EnterText)\n\t}\n\n\tfn toggle_option(&mut self) {\n\t\tmatch self.selection {\n\t\t\tSelection::EnterText => (),\n\t\t\tSelection::FuzzyOption => {\n\t\t\t\tself.options.1.toggle(SearchOptions::FUZZY_SEARCH);\n\t\t\t}\n\t\t\tSelection::CaseOption => {\n\t\t\t\tself.options.1.toggle(SearchOptions::CASE_SENSITIVE);\n\t\t\t}\n\t\t\tSelection::SummarySearch => {\n\t\t\t\tself.options.0.toggle(SearchFields::MESSAGE_SUMMARY);\n\n\t\t\t\tif self.options.0.is_empty() {\n\t\t\t\t\tself.options\n\t\t\t\t\t\t.0\n\t\t\t\t\t\t.set(SearchFields::MESSAGE_BODY, true);\n\t\t\t\t}\n\t\t\t}\n\t\t\tSelection::MessageBodySearch => {\n\t\t\t\tself.options.0.toggle(SearchFields::MESSAGE_BODY);\n\n\t\t\t\tif self.options.0.is_empty() {\n\t\t\t\t\tself.options.0.set(SearchFields::FILENAMES, true);\n\t\t\t\t}\n\t\t\t}\n\t\t\tSelection::FilenameSearch => {\n\t\t\t\tself.options.0.toggle(SearchFields::FILENAMES);\n\n\t\t\t\tif self.options.0.is_empty() {\n\t\t\t\t\tself.options.0.set(SearchFields::AUTHORS, true);\n\t\t\t\t}\n\t\t\t}\n\t\t\tSelection::AuthorsSearch => {\n\t\t\t\tself.options.0.toggle(SearchFields::AUTHORS);\n\n\t\t\t\tif self.options.0.is_empty() {\n\t\t\t\t\tself.options\n\t\t\t\t\t\t.0\n\t\t\t\t\t\t.set(SearchFields::MESSAGE_SUMMARY, true);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tconst fn move_selection(&mut self, arg: bool) {\n\t\tif arg {\n\t\t\t//up\n\t\t\tself.selection = match self.selection {\n\t\t\t\tSelection::EnterText => Selection::AuthorsSearch,\n\t\t\t\tSelection::FuzzyOption => Selection::EnterText,\n\t\t\t\tSelection::CaseOption => Selection::FuzzyOption,\n\t\t\t\tSelection::SummarySearch => Selection::CaseOption,\n\t\t\t\tSelection::MessageBodySearch => {\n\t\t\t\t\tSelection::SummarySearch\n\t\t\t\t}\n\t\t\t\tSelection::FilenameSearch => {\n\t\t\t\t\tSelection::MessageBodySearch\n\t\t\t\t}\n\t\t\t\tSelection::AuthorsSearch => Selection::FilenameSearch,\n\t\t\t};\n\t\t} else {\n\t\t\tself.selection = match self.selection {\n\t\t\t\tSelection::EnterText => Selection::FuzzyOption,\n\t\t\t\tSelection::FuzzyOption => Selection::CaseOption,\n\t\t\t\tSelection::CaseOption => Selection::SummarySearch,\n\t\t\t\tSelection::SummarySearch => {\n\t\t\t\t\tSelection::MessageBodySearch\n\t\t\t\t}\n\t\t\t\tSelection::MessageBodySearch => {\n\t\t\t\t\tSelection::FilenameSearch\n\t\t\t\t}\n\t\t\t\tSelection::FilenameSearch => Selection::AuthorsSearch,\n\t\t\t\tSelection::AuthorsSearch => Selection::EnterText,\n\t\t\t};\n\t\t}\n\n\t\tself.find_text\n\t\t\t.enabled(matches!(self.selection, Selection::EnterText));\n\t}\n\n\tfn draw_search_mode(\n\t\t&self,\n\t\tf: &mut Frame,\n\t\tarea: Rect,\n\t) -> Result<()> {\n\t\tconst SIZE: (u16, u16) = (60, 10);\n\t\tlet area = ui::centered_rect_absolute(SIZE.0, SIZE.1, area);\n\n\t\tf.render_widget(Clear, area);\n\t\tf.render_widget(\n\t\t\tBlock::default()\n\t\t\t\t.borders(Borders::all())\n\t\t\t\t.style(self.theme.title(true))\n\t\t\t\t.title(Span::styled(\n\t\t\t\t\tstrings::POPUP_TITLE_LOG_SEARCH,\n\t\t\t\t\tself.theme.title(true),\n\t\t\t\t)),\n\t\t\tarea,\n\t\t);\n\n\t\tlet chunks = Layout::default()\n\t\t\t.direction(Direction::Vertical)\n\t\t\t.constraints(\n\t\t\t\t[Constraint::Length(1), Constraint::Percentage(100)]\n\t\t\t\t\t.as_ref(),\n\t\t\t)\n\t\t\t.split(area.inner(Margin {\n\t\t\t\thorizontal: 1,\n\t\t\t\tvertical: 1,\n\t\t\t}));\n\n\t\tself.find_text.draw(f, chunks[0])?;\n\n\t\tf.render_widget(\n\t\t\tParagraph::new(self.get_text_options())\n\t\t\t\t.block(\n\t\t\t\t\tBlock::default()\n\t\t\t\t\t\t.borders(Borders::TOP)\n\t\t\t\t\t\t.border_style(self.theme.block(true)),\n\t\t\t\t)\n\t\t\t\t.alignment(Alignment::Left),\n\t\t\tchunks[1],\n\t\t);\n\n\t\tOk(())\n\t}\n\n\tfn draw_commit_sha_mode(\n\t\t&self,\n\t\tf: &mut Frame,\n\t\tarea: Rect,\n\t) -> Result<()> {\n\t\tconst SIZE: (u16, u16) = (60, 3);\n\t\tlet area = ui::centered_rect_absolute(SIZE.0, SIZE.1, area);\n\n\t\tlet mut block_style = self.theme.title(true);\n\n\t\tlet show_invalid = !self.is_valid()\n\t\t\t&& !self.find_text.get_text().trim().is_empty();\n\n\t\tif show_invalid {\n\t\t\tblock_style = block_style.patch(self.theme.text_danger());\n\t\t}\n\n\t\tf.render_widget(Clear, area);\n\t\tf.render_widget(\n\t\t\tBlock::default()\n\t\t\t\t.borders(Borders::all())\n\t\t\t\t.style(block_style)\n\t\t\t\t.title(Span::styled(\n\t\t\t\t\tstrings::POPUP_TITLE_LOG_SEARCH,\n\t\t\t\t\tself.theme.title(true),\n\t\t\t\t)),\n\t\t\tarea,\n\t\t);\n\n\t\tlet chunks = Layout::default()\n\t\t\t.direction(Direction::Vertical)\n\t\t\t.constraints([Constraint::Length(1)].as_ref())\n\t\t\t.split(area.inner(Margin {\n\t\t\t\thorizontal: 1,\n\t\t\t\tvertical: 1,\n\t\t\t}));\n\n\t\tself.find_text.draw(f, chunks[0])?;\n\n\t\tif show_invalid {\n\t\t\tself.draw_invalid_sha(f);\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn draw_invalid_sha(&self, f: &mut Frame) {\n\t\tlet msg_length: u16 = POPUP_COMMIT_SHA_INVALID.len().cast();\n\t\tlet w = Paragraph::new(POPUP_COMMIT_SHA_INVALID)\n\t\t\t.style(self.theme.text_danger());\n\n\t\tlet rect = {\n\t\t\tlet mut rect = self.find_text.get_area();\n\t\t\trect.y += rect.height;\n\t\t\trect.height = 1;\n\t\t\tlet offset = rect.width.saturating_sub(msg_length);\n\t\t\trect.width = rect.width.saturating_sub(offset);\n\t\t\trect.x += offset;\n\n\t\t\trect\n\t\t};\n\n\t\tf.render_widget(w, rect);\n\t}\n\n\t#[inline]\n\tfn event_search_mode(\n\t\t&mut self,\n\t\tevent: &crossterm::event::Event,\n\t) -> Result<EventState> {\n\t\tif let Event::Key(key) = &event {\n\t\t\tif key_match(key, self.key_config.keys.exit_popup) {\n\t\t\t\tself.hide();\n\t\t\t} else if key_match(key, self.key_config.keys.enter)\n\t\t\t\t&& self.is_valid()\n\t\t\t{\n\t\t\t\tself.execute_confirm();\n\t\t\t} else if key_match(key, self.key_config.keys.popup_up) {\n\t\t\t\tself.move_selection(true);\n\t\t\t} else if key_match(\n\t\t\t\tkey,\n\t\t\t\tself.key_config.keys.find_commit_sha,\n\t\t\t) {\n\t\t\t\tself.set_mode(&PopupMode::JumpCommitSha);\n\t\t\t} else if key_match(key, self.key_config.keys.popup_down)\n\t\t\t{\n\t\t\t\tself.move_selection(false);\n\t\t\t} else if key_match(\n\t\t\t\tkey,\n\t\t\t\tself.key_config.keys.log_mark_commit,\n\t\t\t) && self.option_selected()\n\t\t\t{\n\t\t\t\tself.toggle_option();\n\t\t\t} else if !self.option_selected() {\n\t\t\t\tself.find_text.event(event)?;\n\t\t\t}\n\t\t}\n\n\t\tOk(EventState::Consumed)\n\t}\n\n\t#[inline]\n\tfn event_commit_sha_mode(\n\t\t&mut self,\n\t\tevent: &crossterm::event::Event,\n\t) -> Result<EventState> {\n\t\tif let Event::Key(key) = &event {\n\t\t\tif key_match(key, self.key_config.keys.exit_popup) {\n\t\t\t\tself.set_mode(&PopupMode::Search);\n\t\t\t} else if key_match(key, self.key_config.keys.enter)\n\t\t\t\t&& self.is_valid()\n\t\t\t{\n\t\t\t\tself.execute_confirm();\n\t\t\t} else if self.find_text.event(event)?.is_consumed() {\n\t\t\t\tself.validate_commit_sha();\n\t\t\t\tself.find_text.enabled(\n\t\t\t\t\t!self.find_text.get_text().trim().is_empty(),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tOk(EventState::Consumed)\n\t}\n}\n\nimpl DrawableComponent for LogSearchPopupPopup {\n\tfn draw(&self, f: &mut Frame, area: Rect) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tmatch self.mode {\n\t\t\t\tPopupMode::Search => {\n\t\t\t\t\tself.draw_search_mode(f, area)?;\n\t\t\t\t}\n\t\t\t\tPopupMode::JumpCommitSha => {\n\t\t\t\t\tself.draw_commit_sha_mode(f, area)?;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for LogSearchPopupPopup {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.is_visible() || force_all {\n\t\t\tout.push(\n\t\t\t\tCommandInfo::new(\n\t\t\t\t\tstrings::commands::close_popup(&self.key_config),\n\t\t\t\t\ttrue,\n\t\t\t\t\ttrue,\n\t\t\t\t)\n\t\t\t\t.order(1),\n\t\t\t);\n\n\t\t\tif matches!(self.mode, PopupMode::Search) {\n\t\t\t\tout.push(\n\t\t\t\t\tCommandInfo::new(\n\t\t\t\t\t\tstrings::commands::scroll_popup(\n\t\t\t\t\t\t\t&self.key_config,\n\t\t\t\t\t\t),\n\t\t\t\t\t\ttrue,\n\t\t\t\t\t\ttrue,\n\t\t\t\t\t)\n\t\t\t\t\t.order(1),\n\t\t\t\t);\n\t\t\t\tout.push(\n\t\t\t\t\tCommandInfo::new(\n\t\t\t\t\t\tstrings::commands::toggle_option(\n\t\t\t\t\t\t\t&self.key_config,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tself.option_selected(),\n\t\t\t\t\t\ttrue,\n\t\t\t\t\t)\n\t\t\t\t\t.order(1),\n\t\t\t\t);\n\t\t\t\tout.push(\n\t\t\t\t\tCommandInfo::new(\n\t\t\t\t\t\tstrings::commands::find_commit_sha(\n\t\t\t\t\t\t\t&self.key_config,\n\t\t\t\t\t\t),\n\t\t\t\t\t\ttrue,\n\t\t\t\t\t\ttrue,\n\t\t\t\t\t)\n\t\t\t\t\t.order(1),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::confirm_action(&self.key_config),\n\t\t\t\tself.is_valid(),\n\t\t\t\tself.visible,\n\t\t\t));\n\t\t}\n\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(\n\t\t&mut self,\n\t\tevent: &crossterm::event::Event,\n\t) -> Result<EventState> {\n\t\tif !self.is_visible() {\n\t\t\treturn Ok(EventState::NotConsumed);\n\t\t}\n\n\t\tmatch self.mode {\n\t\t\tPopupMode::Search => self.event_search_mode(event),\n\t\t\tPopupMode::JumpCommitSha => {\n\t\t\t\tself.event_commit_sha_mode(event)\n\t\t\t}\n\t\t}\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.visible = false;\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "src/popups/mod.rs",
    "content": "mod blame_file;\nmod branchlist;\nmod checkout_option;\nmod commit;\nmod compare_commits;\nmod confirm;\nmod create_branch;\nmod create_remote;\nmod externaleditor;\nmod fetch;\nmod file_revlog;\nmod fuzzy_find;\nmod goto_line;\nmod help;\nmod inspect_commit;\nmod log_search;\nmod msg;\nmod options;\nmod pull;\nmod push;\nmod push_tags;\nmod remotelist;\nmod rename_branch;\nmod rename_remote;\nmod reset;\nmod revision_files;\nmod stashmsg;\nmod submodules;\nmod tag_commit;\nmod taglist;\nmod update_remote_url;\n\npub use blame_file::{BlameFileOpen, BlameFilePopup};\npub use branchlist::BranchListPopup;\npub use checkout_option::CheckoutOptionPopup;\npub use commit::CommitPopup;\npub use compare_commits::CompareCommitsPopup;\npub use confirm::ConfirmPopup;\npub use create_branch::CreateBranchPopup;\npub use create_remote::CreateRemotePopup;\npub use externaleditor::ExternalEditorPopup;\npub use fetch::FetchPopup;\npub use file_revlog::{FileRevOpen, FileRevlogPopup};\npub use fuzzy_find::FuzzyFindPopup;\npub use goto_line::GotoLinePopup;\npub use help::HelpPopup;\npub use inspect_commit::{InspectCommitOpen, InspectCommitPopup};\npub use log_search::LogSearchPopupPopup;\npub use msg::MsgPopup;\npub use options::{AppOption, OptionsPopup};\npub use pull::PullPopup;\npub use push::PushPopup;\npub use push_tags::PushTagsPopup;\npub use remotelist::RemoteListPopup;\npub use rename_branch::RenameBranchPopup;\npub use rename_remote::RenameRemotePopup;\npub use reset::ResetPopup;\npub use revision_files::{FileTreeOpen, RevisionFilesPopup};\npub use stashmsg::StashMsgPopup;\npub use submodules::SubmodulesListPopup;\npub use tag_commit::TagCommitPopup;\npub use taglist::TagListPopup;\npub use update_remote_url::UpdateRemoteUrlPopup;\n\nuse crate::ui::style::Theme;\nuse ratatui::{\n\tlayout::Alignment,\n\ttext::{Span, Text},\n\twidgets::{Block, BorderType, Borders, Paragraph, Wrap},\n};\n\nfn popup_paragraph<'a, T>(\n\ttitle: &'a str,\n\tcontent: T,\n\ttheme: &Theme,\n\tfocused: bool,\n\tblock: bool,\n) -> Paragraph<'a>\nwhere\n\tT: Into<Text<'a>>,\n{\n\tlet paragraph = Paragraph::new(content.into())\n\t\t.alignment(Alignment::Left)\n\t\t.wrap(Wrap { trim: true });\n\n\tif block {\n\t\tparagraph.block(\n\t\t\tBlock::default()\n\t\t\t\t.title(Span::styled(title, theme.title(focused)))\n\t\t\t\t.borders(Borders::ALL)\n\t\t\t\t.border_type(BorderType::Thick)\n\t\t\t\t.border_style(theme.block(focused)),\n\t\t)\n\t} else {\n\t\tparagraph\n\t}\n}\n"
  },
  {
    "path": "src/popups/msg.rs",
    "content": "use crate::components::{\n\tvisibility_blocking, CommandBlocking, CommandInfo, Component,\n\tDrawableComponent, EventState, ScrollType, VerticalScroll,\n};\nuse crate::strings::order;\nuse crate::{\n\tapp::Environment,\n\tkeys::{key_match, SharedKeyConfig},\n\tstrings, ui,\n};\nuse anyhow::Result;\nuse crossterm::event::Event;\nuse ratatui::text::Line;\nuse ratatui::{\n\tlayout::{Alignment, Rect},\n\ttext::Span,\n\twidgets::{Block, BorderType, Borders, Clear, Paragraph, Wrap},\n\tFrame,\n};\nuse ui::style::SharedTheme;\n\npub struct MsgPopup {\n\ttitle: String,\n\tmsg: String,\n\tvisible: bool,\n\ttheme: SharedTheme,\n\tkey_config: SharedKeyConfig,\n\tscroll: VerticalScroll,\n}\n\nconst POPUP_HEIGHT: u16 = 25;\nconst BORDER_WIDTH: u16 = 2;\nconst MINIMUM_WIDTH: u16 = 60;\n\nimpl DrawableComponent for MsgPopup {\n\tfn draw(&self, f: &mut Frame, _rect: Rect) -> Result<()> {\n\t\tif !self.visible {\n\t\t\treturn Ok(());\n\t\t}\n\n\t\tlet max_width = f.area().width.max(MINIMUM_WIDTH);\n\n\t\t// determine the maximum width of text block\n\t\tlet width = self\n\t\t\t.msg\n\t\t\t.lines()\n\t\t\t.map(str::len)\n\t\t\t.max()\n\t\t\t.unwrap_or(0)\n\t\t\t.saturating_add(BORDER_WIDTH.into())\n\t\t\t.clamp(MINIMUM_WIDTH.into(), max_width.into())\n\t\t\t.try_into()\n\t\t\t.expect(\"can't fail because we're clamping to u16 value\");\n\n\t\tlet area =\n\t\t\tui::centered_rect_absolute(width, POPUP_HEIGHT, f.area());\n\n\t\t// Wrap lines and break words if there is not enough space\n\t\tlet wrapped_msg = bwrap::wrap_maybrk!(\n\t\t\t&self.msg,\n\t\t\tarea.width.saturating_sub(BORDER_WIDTH).into()\n\t\t);\n\n\t\tlet msg_lines: Vec<String> =\n\t\t\twrapped_msg.lines().map(String::from).collect();\n\t\tlet line_num = msg_lines.len();\n\n\t\tlet height = POPUP_HEIGHT\n\t\t\t.saturating_sub(BORDER_WIDTH)\n\t\t\t.min(f.area().height.saturating_sub(BORDER_WIDTH));\n\n\t\tlet top =\n\t\t\tself.scroll.update_no_selection(line_num, height.into());\n\n\t\tlet scrolled_lines = msg_lines\n\t\t\t.iter()\n\t\t\t.skip(top)\n\t\t\t.take(height.into())\n\t\t\t.map(|line| {\n\t\t\t\tLine::from(vec![Span::styled(\n\t\t\t\t\tline.clone(),\n\t\t\t\t\tself.theme.text(true, false),\n\t\t\t\t)])\n\t\t\t})\n\t\t\t.collect::<Vec<Line>>();\n\n\t\tf.render_widget(Clear, area);\n\t\tf.render_widget(\n\t\t\tParagraph::new(scrolled_lines)\n\t\t\t\t.block(\n\t\t\t\t\tBlock::default()\n\t\t\t\t\t\t.title(Span::styled(\n\t\t\t\t\t\t\tself.title.as_str(),\n\t\t\t\t\t\t\tself.theme.text_danger(),\n\t\t\t\t\t\t))\n\t\t\t\t\t\t.borders(Borders::ALL)\n\t\t\t\t\t\t.border_type(BorderType::Thick),\n\t\t\t\t)\n\t\t\t\t.alignment(Alignment::Left)\n\t\t\t\t.wrap(Wrap { trim: true }),\n\t\t\tarea,\n\t\t);\n\n\t\tself.scroll.draw(f, area, &self.theme);\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for MsgPopup {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\t_force_all: bool,\n\t) -> CommandBlocking {\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::close_popup(&self.key_config),\n\t\t\ttrue,\n\t\t\tself.visible,\n\t\t));\n\t\tout.push(\n\t\t\tCommandInfo::new(\n\t\t\t\tstrings::commands::navigate_commit_message(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t),\n\t\t\t\ttrue,\n\t\t\t\tself.visible,\n\t\t\t)\n\t\t\t.order(order::NAV),\n\t\t);\n\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(&mut self, ev: &Event) -> Result<EventState> {\n\t\tif self.visible {\n\t\t\tif let Event::Key(e) = ev {\n\t\t\t\tif key_match(e, self.key_config.keys.exit_popup) {\n\t\t\t\t\tself.hide();\n\t\t\t\t} else if key_match(\n\t\t\t\t\te,\n\t\t\t\t\tself.key_config.keys.popup_down,\n\t\t\t\t) {\n\t\t\t\t\tself.scroll.move_top(ScrollType::Down);\n\t\t\t\t} else if key_match(e, self.key_config.keys.popup_up)\n\t\t\t\t{\n\t\t\t\t\tself.scroll.move_top(ScrollType::Up);\n\t\t\t\t}\n\t\t\t}\n\t\t\tOk(EventState::Consumed)\n\t\t} else {\n\t\t\tOk(EventState::NotConsumed)\n\t\t}\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.visible = false;\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\n\t\tOk(())\n\t}\n}\n\nimpl MsgPopup {\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\ttitle: String::new(),\n\t\t\tmsg: String::new(),\n\t\t\tvisible: false,\n\t\t\ttheme: env.theme.clone(),\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\tscroll: VerticalScroll::new(),\n\t\t}\n\t}\n\n\tfn set_new_msg(\n\t\t&mut self,\n\t\tmsg: &str,\n\t\ttitle: String,\n\t) -> Result<()> {\n\t\tself.title = title;\n\t\tself.msg = msg.to_string();\n\t\tself.scroll.reset();\n\t\tself.show()\n\t}\n\n\t///\n\tpub fn show_error(&mut self, msg: &str) -> Result<()> {\n\t\tself.set_new_msg(\n\t\t\tmsg,\n\t\t\tstrings::msg_title_error(&self.key_config),\n\t\t)\n\t}\n\n\t///\n\tpub fn show_info(&mut self, msg: &str) -> Result<()> {\n\t\tself.set_new_msg(\n\t\t\tmsg,\n\t\t\tstrings::msg_title_info(&self.key_config),\n\t\t)\n\t}\n}\n"
  },
  {
    "path": "src/popups/options.rs",
    "content": "use crate::{\n\tapp::Environment,\n\tcomponents::{\n\t\tstring_width_align, visibility_blocking, CommandBlocking,\n\t\tCommandInfo, Component, DrawableComponent, EventState,\n\t},\n\tkeys::{key_match, SharedKeyConfig},\n\toptions::SharedOptions,\n\tqueue::{InternalEvent, Queue},\n\tstrings,\n\tui::{self, style::SharedTheme},\n};\nuse anyhow::Result;\nuse asyncgit::sync::ShowUntrackedFilesConfig;\nuse crossterm::event::Event;\nuse ratatui::{\n\tlayout::{Alignment, Rect},\n\tstyle::{Modifier, Style},\n\ttext::{Line, Span},\n\twidgets::{Block, Borders, Clear, Paragraph},\n\tFrame,\n};\n\n#[derive(Clone, Copy, PartialEq, Eq)]\npub enum AppOption {\n\tStatusShowUntracked,\n\tDiffIgnoreWhitespaces,\n\tDiffContextLines,\n\tDiffInterhunkLines,\n}\n\npub struct OptionsPopup {\n\tselection: AppOption,\n\tqueue: Queue,\n\tvisible: bool,\n\tkey_config: SharedKeyConfig,\n\toptions: SharedOptions,\n\ttheme: SharedTheme,\n}\n\nimpl OptionsPopup {\n\t///\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\tselection: AppOption::StatusShowUntracked,\n\t\t\tqueue: env.queue.clone(),\n\t\t\tvisible: false,\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\toptions: env.options.clone(),\n\t\t\ttheme: env.theme.clone(),\n\t\t}\n\t}\n\n\tfn get_text(&self, width: u16) -> Vec<Line<'_>> {\n\t\tlet mut txt: Vec<Line> = Vec::with_capacity(10);\n\n\t\tself.add_status(&mut txt, width);\n\n\t\ttxt\n\t}\n\n\tfn add_status(&self, txt: &mut Vec<Line>, width: u16) {\n\t\tSelf::add_header(txt, \"Status\");\n\n\t\tself.add_entry(\n\t\t\ttxt,\n\t\t\twidth,\n\t\t\t\"Show untracked\",\n\t\t\tmatch self.options.borrow().status_show_untracked() {\n\t\t\t\tNone => \"Gitconfig\",\n\t\t\t\tSome(ShowUntrackedFilesConfig::No) => \"No\",\n\t\t\t\tSome(ShowUntrackedFilesConfig::Normal) => \"Normal\",\n\t\t\t\tSome(ShowUntrackedFilesConfig::All) => \"All\",\n\t\t\t},\n\t\t\tself.is_select(AppOption::StatusShowUntracked),\n\t\t);\n\t\tSelf::add_header(txt, \"\");\n\n\t\tlet diff = self.options.borrow().diff_options();\n\t\tSelf::add_header(txt, \"Diff\");\n\t\tself.add_entry(\n\t\t\ttxt,\n\t\t\twidth,\n\t\t\t\"Ignore whitespaces\",\n\t\t\t&diff.ignore_whitespace.to_string(),\n\t\t\tself.is_select(AppOption::DiffIgnoreWhitespaces),\n\t\t);\n\t\tself.add_entry(\n\t\t\ttxt,\n\t\t\twidth,\n\t\t\t\"Context lines\",\n\t\t\t&diff.context.to_string(),\n\t\t\tself.is_select(AppOption::DiffContextLines),\n\t\t);\n\t\tself.add_entry(\n\t\t\ttxt,\n\t\t\twidth,\n\t\t\t\"Inter hunk lines\",\n\t\t\t&diff.interhunk_lines.to_string(),\n\t\t\tself.is_select(AppOption::DiffInterhunkLines),\n\t\t);\n\t}\n\n\tfn is_select(&self, kind: AppOption) -> bool {\n\t\tself.selection == kind\n\t}\n\n\tfn add_header(txt: &mut Vec<Line>, header: &'static str) {\n\t\ttxt.push(Line::from(vec![Span::styled(\n\t\t\theader,\n\t\t\t//TODO: use style\n\t\t\tStyle::default().add_modifier(Modifier::UNDERLINED),\n\t\t)]));\n\t}\n\n\tfn add_entry(\n\t\t&self,\n\t\ttxt: &mut Vec<Line>,\n\t\twidth: u16,\n\t\tentry: &'static str,\n\t\tvalue: &str,\n\t\tselected: bool,\n\t) {\n\t\tlet half = usize::from(width / 2);\n\t\ttxt.push(Line::from(vec![\n\t\t\tSpan::styled(\n\t\t\t\tstring_width_align(entry, half),\n\t\t\t\tself.theme.text(true, false),\n\t\t\t),\n\t\t\tSpan::styled(\n\t\t\t\tformat!(\"{value:^half$}\"),\n\t\t\t\tself.theme.text(true, selected),\n\t\t\t),\n\t\t]));\n\t}\n\n\tconst fn move_selection(&mut self, up: bool) {\n\t\tif up {\n\t\t\tself.selection = match self.selection {\n\t\t\t\tAppOption::StatusShowUntracked => {\n\t\t\t\t\tAppOption::DiffInterhunkLines\n\t\t\t\t}\n\t\t\t\tAppOption::DiffIgnoreWhitespaces => {\n\t\t\t\t\tAppOption::StatusShowUntracked\n\t\t\t\t}\n\t\t\t\tAppOption::DiffContextLines => {\n\t\t\t\t\tAppOption::DiffIgnoreWhitespaces\n\t\t\t\t}\n\t\t\t\tAppOption::DiffInterhunkLines => {\n\t\t\t\t\tAppOption::DiffContextLines\n\t\t\t\t}\n\t\t\t};\n\t\t} else {\n\t\t\tself.selection = match self.selection {\n\t\t\t\tAppOption::StatusShowUntracked => {\n\t\t\t\t\tAppOption::DiffIgnoreWhitespaces\n\t\t\t\t}\n\t\t\t\tAppOption::DiffIgnoreWhitespaces => {\n\t\t\t\t\tAppOption::DiffContextLines\n\t\t\t\t}\n\t\t\t\tAppOption::DiffContextLines => {\n\t\t\t\t\tAppOption::DiffInterhunkLines\n\t\t\t\t}\n\t\t\t\tAppOption::DiffInterhunkLines => {\n\t\t\t\t\tAppOption::StatusShowUntracked\n\t\t\t\t}\n\t\t\t};\n\t\t}\n\t}\n\n\tfn switch_option(&self, right: bool) {\n\t\tif right {\n\t\t\tmatch self.selection {\n\t\t\t\tAppOption::StatusShowUntracked => {\n\t\t\t\t\tlet untracked =\n\t\t\t\t\t\tself.options.borrow().status_show_untracked();\n\n\t\t\t\t\tlet untracked = match untracked {\n\t\t\t\t\t\tNone => {\n\t\t\t\t\t\t\tSome(ShowUntrackedFilesConfig::Normal)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tSome(ShowUntrackedFilesConfig::Normal) => {\n\t\t\t\t\t\t\tSome(ShowUntrackedFilesConfig::All)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tSome(ShowUntrackedFilesConfig::All) => {\n\t\t\t\t\t\t\tSome(ShowUntrackedFilesConfig::No)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tSome(ShowUntrackedFilesConfig::No) => None,\n\t\t\t\t\t};\n\n\t\t\t\t\tself.options\n\t\t\t\t\t\t.borrow_mut()\n\t\t\t\t\t\t.set_status_show_untracked(untracked);\n\t\t\t\t}\n\t\t\t\tAppOption::DiffIgnoreWhitespaces => {\n\t\t\t\t\tself.options\n\t\t\t\t\t\t.borrow_mut()\n\t\t\t\t\t\t.diff_toggle_whitespace();\n\t\t\t\t}\n\t\t\t\tAppOption::DiffContextLines => {\n\t\t\t\t\tself.options\n\t\t\t\t\t\t.borrow_mut()\n\t\t\t\t\t\t.diff_context_change(true);\n\t\t\t\t}\n\t\t\t\tAppOption::DiffInterhunkLines => {\n\t\t\t\t\tself.options\n\t\t\t\t\t\t.borrow_mut()\n\t\t\t\t\t\t.diff_hunk_lines_change(true);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tmatch self.selection {\n\t\t\t\tAppOption::StatusShowUntracked => {\n\t\t\t\t\tlet untracked =\n\t\t\t\t\t\tself.options.borrow().status_show_untracked();\n\n\t\t\t\t\tlet untracked = match untracked {\n\t\t\t\t\t\tNone => Some(ShowUntrackedFilesConfig::No),\n\t\t\t\t\t\tSome(ShowUntrackedFilesConfig::No) => {\n\t\t\t\t\t\t\tSome(ShowUntrackedFilesConfig::All)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tSome(ShowUntrackedFilesConfig::All) => {\n\t\t\t\t\t\t\tSome(ShowUntrackedFilesConfig::Normal)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tSome(ShowUntrackedFilesConfig::Normal) => {\n\t\t\t\t\t\t\tNone\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tself.options\n\t\t\t\t\t\t.borrow_mut()\n\t\t\t\t\t\t.set_status_show_untracked(untracked);\n\t\t\t\t}\n\t\t\t\tAppOption::DiffIgnoreWhitespaces => {\n\t\t\t\t\tself.options\n\t\t\t\t\t\t.borrow_mut()\n\t\t\t\t\t\t.diff_toggle_whitespace();\n\t\t\t\t}\n\t\t\t\tAppOption::DiffContextLines => {\n\t\t\t\t\tself.options\n\t\t\t\t\t\t.borrow_mut()\n\t\t\t\t\t\t.diff_context_change(false);\n\t\t\t\t}\n\t\t\t\tAppOption::DiffInterhunkLines => {\n\t\t\t\t\tself.options\n\t\t\t\t\t\t.borrow_mut()\n\t\t\t\t\t\t.diff_hunk_lines_change(false);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tself.queue\n\t\t\t.push(InternalEvent::OptionSwitched(self.selection));\n\t}\n}\n\nimpl DrawableComponent for OptionsPopup {\n\tfn draw(&self, f: &mut Frame, area: Rect) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tconst SIZE: (u16, u16) = (50, 10);\n\t\t\tlet area =\n\t\t\t\tui::centered_rect_absolute(SIZE.0, SIZE.1, area);\n\n\t\t\tlet width = area.width;\n\n\t\t\tf.render_widget(Clear, area);\n\t\t\tf.render_widget(\n\t\t\t\tParagraph::new(self.get_text(width))\n\t\t\t\t\t.block(\n\t\t\t\t\t\tBlock::default()\n\t\t\t\t\t\t\t.borders(Borders::ALL)\n\t\t\t\t\t\t\t.title(Span::styled(\n\t\t\t\t\t\t\t\t\"Options\",\n\t\t\t\t\t\t\t\tself.theme.title(true),\n\t\t\t\t\t\t\t))\n\t\t\t\t\t\t\t.border_style(self.theme.block(true)),\n\t\t\t\t\t)\n\t\t\t\t\t.alignment(Alignment::Left),\n\t\t\t\tarea,\n\t\t\t);\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for OptionsPopup {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.is_visible() || force_all {\n\t\t\tout.push(\n\t\t\t\tCommandInfo::new(\n\t\t\t\t\tstrings::commands::close_popup(&self.key_config),\n\t\t\t\t\ttrue,\n\t\t\t\t\ttrue,\n\t\t\t\t)\n\t\t\t\t.order(1),\n\t\t\t);\n\t\t\tout.push(\n\t\t\t\tCommandInfo::new(\n\t\t\t\t\tstrings::commands::navigate_tree(\n\t\t\t\t\t\t&self.key_config,\n\t\t\t\t\t),\n\t\t\t\t\ttrue,\n\t\t\t\t\ttrue,\n\t\t\t\t)\n\t\t\t\t.order(1),\n\t\t\t);\n\t\t}\n\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(\n\t\t&mut self,\n\t\tevent: &crossterm::event::Event,\n\t) -> Result<EventState> {\n\t\tif self.is_visible() {\n\t\t\tif let Event::Key(key) = &event {\n\t\t\t\tif key_match(key, self.key_config.keys.exit_popup) {\n\t\t\t\t\tself.hide();\n\t\t\t\t} else if key_match(key, self.key_config.keys.move_up)\n\t\t\t\t{\n\t\t\t\t\tself.move_selection(true);\n\t\t\t\t} else if key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.move_down,\n\t\t\t\t) {\n\t\t\t\t\tself.move_selection(false);\n\t\t\t\t} else if key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.move_right,\n\t\t\t\t) {\n\t\t\t\t\tself.switch_option(true);\n\t\t\t\t} else if key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.move_left,\n\t\t\t\t) {\n\t\t\t\t\tself.switch_option(false);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn Ok(EventState::Consumed);\n\t\t}\n\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.visible = false;\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "src/popups/pull.rs",
    "content": "use crate::{\n\tapp::Environment,\n\tcomponents::{\n\t\tvisibility_blocking, CommandBlocking, CommandInfo, Component,\n\t\tCredComponent, DrawableComponent, EventState,\n\t},\n\tkeys::SharedKeyConfig,\n\tpopups::PushPopup,\n\tqueue::{Action, InternalEvent, Queue},\n\tstrings, try_or_popup,\n\tui::{self, style::SharedTheme},\n};\nuse anyhow::Result;\nuse asyncgit::{\n\tsync::{\n\t\tself,\n\t\tcred::{\n\t\t\textract_username_password_for_fetch,\n\t\t\tneed_username_password_for_fetch, BasicAuthCredential,\n\t\t},\n\t\tremotes::get_default_remote_for_fetch,\n\t\tRepoPathRef,\n\t},\n\tAsyncGitNotification, AsyncPull, FetchRequest, RemoteProgress,\n};\n\nuse crossterm::event::Event;\nuse ratatui::{\n\tlayout::Rect,\n\ttext::Span,\n\twidgets::{Block, BorderType, Borders, Clear, Gauge},\n\tFrame,\n};\n\n///\npub struct PullPopup {\n\trepo: RepoPathRef,\n\tvisible: bool,\n\tgit_fetch: AsyncPull,\n\tprogress: Option<RemoteProgress>,\n\tpending: bool,\n\tbranch: String,\n\tqueue: Queue,\n\ttheme: SharedTheme,\n\tkey_config: SharedKeyConfig,\n\tinput_cred: CredComponent,\n}\n\nimpl PullPopup {\n\t///\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\trepo: env.repo.clone(),\n\t\t\tqueue: env.queue.clone(),\n\t\t\tpending: false,\n\t\t\tvisible: false,\n\t\t\tbranch: String::new(),\n\t\t\tgit_fetch: AsyncPull::new(\n\t\t\t\tenv.repo.borrow().clone(),\n\t\t\t\t&env.sender_git,\n\t\t\t),\n\t\t\tprogress: None,\n\t\t\tinput_cred: CredComponent::new(env),\n\t\t\ttheme: env.theme.clone(),\n\t\t\tkey_config: env.key_config.clone(),\n\t\t}\n\t}\n\n\t///\n\tpub fn fetch(&mut self, branch: String) -> Result<()> {\n\t\tself.branch = branch;\n\t\tself.show()?;\n\t\tif need_username_password_for_fetch(&self.repo.borrow())? {\n\t\t\tlet cred = extract_username_password_for_fetch(\n\t\t\t\t&self.repo.borrow(),\n\t\t\t)\n\t\t\t.unwrap_or_else(|_| BasicAuthCredential::new(None, None));\n\t\t\tif cred.is_complete() {\n\t\t\t\tself.fetch_from_remote(Some(cred))\n\t\t\t} else {\n\t\t\t\tself.input_cred.set_cred(cred);\n\t\t\t\tself.input_cred.show()\n\t\t\t}\n\t\t} else {\n\t\t\tself.fetch_from_remote(None)\n\t\t}\n\t}\n\n\tfn fetch_from_remote(\n\t\t&mut self,\n\t\tcred: Option<BasicAuthCredential>,\n\t) -> Result<()> {\n\t\tself.pending = true;\n\t\tself.progress = None;\n\t\tself.git_fetch.request(FetchRequest {\n\t\t\tremote: get_default_remote_for_fetch(\n\t\t\t\t&self.repo.borrow(),\n\t\t\t)?,\n\t\t\tbranch: self.branch.clone(),\n\t\t\tbasic_credential: cred,\n\t\t})?;\n\n\t\tOk(())\n\t}\n\n\t///\n\tpub const fn any_work_pending(&self) -> bool {\n\t\tself.pending\n\t}\n\n\t///\n\tpub fn update_git(&mut self, ev: AsyncGitNotification) {\n\t\tif self.is_visible() && ev == AsyncGitNotification::Pull {\n\t\t\tif let Err(error) = self.update() {\n\t\t\t\tself.pending = false;\n\t\t\t\tself.hide();\n\t\t\t\tself.queue.push(InternalEvent::ShowErrorMsg(\n\t\t\t\t\tformat!(\"fetch failed:\\n{error}\"),\n\t\t\t\t));\n\t\t\t}\n\t\t}\n\t}\n\n\t///\n\tfn update(&mut self) -> Result<()> {\n\t\tself.pending = self.git_fetch.is_pending()?;\n\t\tself.progress = self.git_fetch.progress()?;\n\n\t\tif !self.pending {\n\t\t\tif let Some((_bytes, err)) =\n\t\t\t\tself.git_fetch.last_result()?\n\t\t\t{\n\t\t\t\tif err.is_empty() {\n\t\t\t\t\tself.try_ff_merge()?;\n\t\t\t\t} else {\n\t\t\t\t\tanyhow::bail!(err);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tOk(())\n\t}\n\n\t// check if something is incoming and try a ff merge then\n\tfn try_ff_merge(&mut self) -> Result<()> {\n\t\tlet branch_compare = sync::branch_compare_upstream(\n\t\t\t&self.repo.borrow(),\n\t\t\t&self.branch,\n\t\t)?;\n\t\tif branch_compare.behind > 0 {\n\t\t\tlet ff_res = sync::branch_merge_upstream_fastforward(\n\t\t\t\t&self.repo.borrow(),\n\t\t\t\t&self.branch,\n\t\t\t);\n\t\t\tif let Err(e) = ff_res {\n\t\t\t\tlog::trace!(\"ff failed: {e}\");\n\t\t\t\tself.confirm_merge(branch_compare.behind);\n\t\t\t}\n\t\t}\n\n\t\tself.hide();\n\n\t\tOk(())\n\t}\n\n\tpub fn try_conflict_free_merge(&self, rebase: bool) {\n\t\tif rebase {\n\t\t\ttry_or_popup!(\n\t\t\t\tself,\n\t\t\t\t\"rebase failed:\",\n\t\t\t\tsync::merge_upstream_rebase(\n\t\t\t\t\t&self.repo.borrow(),\n\t\t\t\t\t&self.branch\n\t\t\t\t)\n\t\t\t);\n\t\t} else {\n\t\t\ttry_or_popup!(\n\t\t\t\tself,\n\t\t\t\t\"merge failed:\",\n\t\t\t\tsync::merge_upstream_commit(\n\t\t\t\t\t&self.repo.borrow(),\n\t\t\t\t\t&self.branch\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\t}\n\n\tfn confirm_merge(&mut self, incoming: usize) {\n\t\tself.queue.push(InternalEvent::ConfirmAction(\n\t\t\tAction::PullMerge {\n\t\t\t\tincoming,\n\t\t\t\trebase: sync::config_is_pull_rebase(\n\t\t\t\t\t&self.repo.borrow(),\n\t\t\t\t)\n\t\t\t\t.unwrap_or_default(),\n\t\t\t},\n\t\t));\n\t\tself.hide();\n\t}\n}\n\nimpl DrawableComponent for PullPopup {\n\tfn draw(&self, f: &mut Frame, rect: Rect) -> Result<()> {\n\t\tif self.visible {\n\t\t\tlet (state, progress) =\n\t\t\t\tPushPopup::get_progress(self.progress.as_ref());\n\n\t\t\tlet area = ui::centered_rect_absolute(30, 3, f.area());\n\n\t\t\tf.render_widget(Clear, area);\n\t\t\tf.render_widget(\n\t\t\t\tGauge::default()\n\t\t\t\t\t.label(state.as_str())\n\t\t\t\t\t.block(\n\t\t\t\t\t\tBlock::default()\n\t\t\t\t\t\t\t.title(Span::styled(\n\t\t\t\t\t\t\t\tstrings::PULL_POPUP_MSG,\n\t\t\t\t\t\t\t\tself.theme.title(true),\n\t\t\t\t\t\t\t))\n\t\t\t\t\t\t\t.borders(Borders::ALL)\n\t\t\t\t\t\t\t.border_type(BorderType::Thick)\n\t\t\t\t\t\t\t.border_style(self.theme.block(true)),\n\t\t\t\t\t)\n\t\t\t\t\t.gauge_style(self.theme.push_gauge())\n\t\t\t\t\t.percent(u16::from(progress)),\n\t\t\t\tarea,\n\t\t\t);\n\t\t\tself.input_cred.draw(f, rect)?;\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for PullPopup {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.is_visible() || force_all {\n\t\t\tif !force_all {\n\t\t\t\tout.clear();\n\t\t\t}\n\n\t\t\tif self.input_cred.is_visible() {\n\t\t\t\treturn self.input_cred.commands(out, force_all);\n\t\t\t}\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::close_msg(&self.key_config),\n\t\t\t\t!self.pending,\n\t\t\t\tself.visible,\n\t\t\t));\n\t\t}\n\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(&mut self, ev: &Event) -> Result<EventState> {\n\t\tif self.visible {\n\t\t\tif let Event::Key(_) = ev {\n\t\t\t\tif self.input_cred.is_visible() {\n\t\t\t\t\tself.input_cred.event(ev)?;\n\n\t\t\t\t\tif self.input_cred.get_cred().is_complete()\n\t\t\t\t\t\t|| !self.input_cred.is_visible()\n\t\t\t\t\t{\n\t\t\t\t\t\tself.fetch_from_remote(Some(\n\t\t\t\t\t\t\tself.input_cred.get_cred().clone(),\n\t\t\t\t\t\t))?;\n\t\t\t\t\t\tself.input_cred.hide();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn Ok(EventState::Consumed);\n\t\t}\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.visible = false;\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "src/popups/push.rs",
    "content": "use crate::{\n\tapp::Environment,\n\tcomponents::{\n\t\tvisibility_blocking, CommandBlocking, CommandInfo, Component,\n\t\tCredComponent, DrawableComponent, EventState,\n\t},\n\tkeys::{key_match, SharedKeyConfig},\n\tqueue::{InternalEvent, Queue},\n\tstrings,\n\tui::{self, style::SharedTheme},\n};\nuse anyhow::Result;\nuse asyncgit::{\n\tsync::{\n\t\tcred::{\n\t\t\textract_username_password_for_push,\n\t\t\tneed_username_password_for_push, BasicAuthCredential,\n\t\t},\n\t\tget_branch_remote, hooks_pre_push,\n\t\tremotes::get_default_remote_for_push,\n\t\tHookResult, RepoPathRef,\n\t},\n\tAsyncGitNotification, AsyncPush, PushRequest, PushType,\n\tRemoteProgress, RemoteProgressState,\n};\nuse crossterm::event::Event;\nuse ratatui::{\n\tlayout::Rect,\n\ttext::Span,\n\twidgets::{Block, BorderType, Borders, Clear, Gauge},\n\tFrame,\n};\n\n///\n#[derive(PartialEq, Eq)]\nenum PushComponentModifier {\n\tNone,\n\tForce,\n\tDelete,\n\tForceDelete,\n}\n\nimpl PushComponentModifier {\n\tpub(crate) fn force(&self) -> bool {\n\t\tself == &Self::Force || self == &Self::ForceDelete\n\t}\n\tpub(crate) fn delete(&self) -> bool {\n\t\tself == &Self::Delete || self == &Self::ForceDelete\n\t}\n}\n\n///\npub struct PushPopup {\n\trepo: RepoPathRef,\n\tmodifier: PushComponentModifier,\n\tvisible: bool,\n\tgit_push: AsyncPush,\n\tprogress: Option<RemoteProgress>,\n\tpending: bool,\n\tbranch: String,\n\tpush_type: PushType,\n\tqueue: Queue,\n\ttheme: SharedTheme,\n\tkey_config: SharedKeyConfig,\n\tinput_cred: CredComponent,\n}\n\nimpl PushPopup {\n\t///\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\trepo: env.repo.clone(),\n\t\t\tqueue: env.queue.clone(),\n\t\t\tmodifier: PushComponentModifier::None,\n\t\t\tpending: false,\n\t\t\tvisible: false,\n\t\t\tbranch: String::new(),\n\t\t\tpush_type: PushType::Branch,\n\t\t\tgit_push: AsyncPush::new(\n\t\t\t\tenv.repo.borrow().clone(),\n\t\t\t\t&env.sender_git,\n\t\t\t),\n\t\t\tprogress: None,\n\t\t\tinput_cred: CredComponent::new(env),\n\t\t\ttheme: env.theme.clone(),\n\t\t\tkey_config: env.key_config.clone(),\n\t\t}\n\t}\n\n\t///\n\tpub fn push(\n\t\t&mut self,\n\t\tbranch: String,\n\t\tpush_type: PushType,\n\t\tforce: bool,\n\t\tdelete: bool,\n\t) -> Result<()> {\n\t\tself.branch = branch;\n\t\tself.push_type = push_type;\n\t\tself.modifier = match (force, delete) {\n\t\t\t(true, true) => PushComponentModifier::ForceDelete,\n\t\t\t(false, true) => PushComponentModifier::Delete,\n\t\t\t(true, false) => PushComponentModifier::Force,\n\t\t\t(false, false) => PushComponentModifier::None,\n\t\t};\n\n\t\tself.show()?;\n\n\t\tif need_username_password_for_push(&self.repo.borrow())? {\n\t\t\tlet cred = extract_username_password_for_push(\n\t\t\t\t&self.repo.borrow(),\n\t\t\t)\n\t\t\t.unwrap_or_else(|_| BasicAuthCredential::new(None, None));\n\t\t\tif cred.is_complete() {\n\t\t\t\tself.push_to_remote(Some(cred), force)\n\t\t\t} else {\n\t\t\t\tself.input_cred.set_cred(cred);\n\t\t\t\tself.input_cred.show()\n\t\t\t}\n\t\t} else {\n\t\t\tself.push_to_remote(None, force)\n\t\t}\n\t}\n\n\tfn push_to_remote(\n\t\t&mut self,\n\t\tcred: Option<BasicAuthCredential>,\n\t\tforce: bool,\n\t) -> Result<()> {\n\t\tlet remote = if let Ok(Some(remote)) =\n\t\t\tget_branch_remote(&self.repo.borrow(), &self.branch)\n\t\t{\n\t\t\tlog::info!(\"push: branch '{}' has upstream for remote '{}' - using that\",self.branch,remote);\n\t\t\tremote\n\t\t} else {\n\t\t\tlog::info!(\"push: branch '{}' has no upstream - looking up default remote\",self.branch);\n\t\t\tlet remote =\n\t\t\t\tget_default_remote_for_push(&self.repo.borrow())?;\n\t\t\tlog::info!(\n\t\t\t\t\"push: branch '{}' to remote '{}'\",\n\t\t\t\tself.branch,\n\t\t\t\tremote\n\t\t\t);\n\t\t\tremote\n\t\t};\n\n\t\t// run pre push hook - can reject push\n\t\tlet repo = self.repo.borrow();\n\t\tif let HookResult::NotOk(e) = hooks_pre_push(\n\t\t\t&repo,\n\t\t\t&remote,\n\t\t\t&asyncgit::sync::PrePushTarget::Branch {\n\t\t\t\tbranch: &self.branch,\n\t\t\t\tdelete: self.modifier.delete(),\n\t\t\t},\n\t\t\tcred.clone(),\n\t\t)? {\n\t\t\tlog::error!(\"pre-push hook failed: {e}\");\n\t\t\tself.queue.push(InternalEvent::ShowErrorMsg(format!(\n\t\t\t\t\"pre-push hook failed:\\n{e}\"\n\t\t\t)));\n\t\t\tself.pending = false;\n\t\t\tself.visible = false;\n\t\t\treturn Ok(());\n\t\t}\n\n\t\tself.pending = true;\n\t\tself.progress = None;\n\t\tself.git_push.request(PushRequest {\n\t\t\tremote,\n\t\t\tbranch: self.branch.clone(),\n\t\t\tpush_type: self.push_type,\n\t\t\tforce,\n\t\t\tdelete: self.modifier.delete(),\n\t\t\tbasic_credential: cred,\n\t\t})?;\n\t\tOk(())\n\t}\n\n\t///\n\tpub fn update_git(\n\t\t&mut self,\n\t\tev: AsyncGitNotification,\n\t) -> Result<()> {\n\t\tif self.is_visible() && ev == AsyncGitNotification::Push {\n\t\t\tself.update()?;\n\t\t}\n\n\t\tOk(())\n\t}\n\n\t///\n\tfn update(&mut self) -> Result<()> {\n\t\tself.pending = self.git_push.is_pending()?;\n\t\tself.progress = self.git_push.progress()?;\n\n\t\tif !self.pending {\n\t\t\tif let Some(err) = self.git_push.last_result()? {\n\t\t\t\tself.queue.push(InternalEvent::ShowErrorMsg(\n\t\t\t\t\tformat!(\"push failed:\\n{err}\"),\n\t\t\t\t));\n\t\t\t}\n\t\t\tself.hide();\n\t\t}\n\n\t\tOk(())\n\t}\n\n\t///\n\tpub const fn any_work_pending(&self) -> bool {\n\t\tself.pending\n\t}\n\n\t///\n\tpub fn get_progress(\n\t\tprogress: Option<&RemoteProgress>,\n\t) -> (String, u8) {\n\t\tprogress.as_ref().map_or_else(\n\t\t\t|| (strings::PUSH_POPUP_PROGRESS_NONE.into(), 0),\n\t\t\t|progress| {\n\t\t\t\t(\n\t\t\t\t\tSelf::progress_state_name(&progress.state),\n\t\t\t\t\tprogress.get_progress_percent(),\n\t\t\t\t)\n\t\t\t},\n\t\t)\n\t}\n\n\tfn progress_state_name(state: &RemoteProgressState) -> String {\n\t\tmatch state {\n\t\t\tRemoteProgressState::PackingAddingObject => {\n\t\t\t\tstrings::PUSH_POPUP_STATES_ADDING\n\t\t\t}\n\t\t\tRemoteProgressState::PackingDeltafiction => {\n\t\t\t\tstrings::PUSH_POPUP_STATES_DELTAS\n\t\t\t}\n\t\t\tRemoteProgressState::Pushing => {\n\t\t\t\tstrings::PUSH_POPUP_STATES_PUSHING\n\t\t\t}\n\t\t\tRemoteProgressState::Transfer => {\n\t\t\t\tstrings::PUSH_POPUP_STATES_TRANSFER\n\t\t\t}\n\t\t\tRemoteProgressState::Done => {\n\t\t\t\tstrings::PUSH_POPUP_STATES_DONE\n\t\t\t}\n\t\t}\n\t\t.into()\n\t}\n}\n\nimpl DrawableComponent for PushPopup {\n\tfn draw(&self, f: &mut Frame, rect: Rect) -> Result<()> {\n\t\tif self.visible {\n\t\t\tlet (state, progress) =\n\t\t\t\tSelf::get_progress(self.progress.as_ref());\n\n\t\t\tlet area = ui::centered_rect_absolute(30, 3, f.area());\n\n\t\t\tf.render_widget(Clear, area);\n\t\t\tf.render_widget(\n\t\t\t\tGauge::default()\n\t\t\t\t\t.label(state.as_str())\n\t\t\t\t\t.block(\n\t\t\t\t\t\tBlock::default()\n\t\t\t\t\t\t\t.title(Span::styled(\n\t\t\t\t\t\t\t\tif self.modifier.force() {\n\t\t\t\t\t\t\t\t\tstrings::FORCE_PUSH_POPUP_MSG\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tstrings::PUSH_POPUP_MSG\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tself.theme.title(true),\n\t\t\t\t\t\t\t))\n\t\t\t\t\t\t\t.borders(Borders::ALL)\n\t\t\t\t\t\t\t.border_type(BorderType::Thick)\n\t\t\t\t\t\t\t.border_style(self.theme.block(true)),\n\t\t\t\t\t)\n\t\t\t\t\t.gauge_style(self.theme.push_gauge())\n\t\t\t\t\t.percent(u16::from(progress)),\n\t\t\t\tarea,\n\t\t\t);\n\t\t\tself.input_cred.draw(f, rect)?;\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for PushPopup {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.is_visible() || force_all {\n\t\t\tif !force_all {\n\t\t\t\tout.clear();\n\t\t\t}\n\n\t\t\tif self.input_cred.is_visible() {\n\t\t\t\treturn self.input_cred.commands(out, force_all);\n\t\t\t}\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::close_msg(&self.key_config),\n\t\t\t\t!self.pending,\n\t\t\t\tself.visible,\n\t\t\t));\n\t\t}\n\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(&mut self, ev: &Event) -> Result<EventState> {\n\t\tif self.visible {\n\t\t\tif let Event::Key(e) = ev {\n\t\t\t\tif self.input_cred.is_visible() {\n\t\t\t\t\tself.input_cred.event(ev)?;\n\n\t\t\t\t\tif self.input_cred.get_cred().is_complete()\n\t\t\t\t\t\t|| !self.input_cred.is_visible()\n\t\t\t\t\t{\n\t\t\t\t\t\tself.push_to_remote(\n\t\t\t\t\t\t\tSome(self.input_cred.get_cred().clone()),\n\t\t\t\t\t\t\tself.modifier.force(),\n\t\t\t\t\t\t)?;\n\t\t\t\t\t\tself.input_cred.hide();\n\t\t\t\t\t}\n\t\t\t\t} else if key_match(\n\t\t\t\t\te,\n\t\t\t\t\tself.key_config.keys.exit_popup,\n\t\t\t\t) && !self.pending\n\t\t\t\t{\n\t\t\t\t\tself.hide();\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn Ok(EventState::Consumed);\n\t\t}\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.visible = false;\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "src/popups/push_tags.rs",
    "content": "use crate::{\n\tapp::Environment,\n\tcomponents::{\n\t\tvisibility_blocking, CommandBlocking, CommandInfo, Component,\n\t\tCredComponent, DrawableComponent, EventState,\n\t},\n\tkeys::{key_match, SharedKeyConfig},\n\tqueue::{InternalEvent, Queue},\n\tstrings,\n\tui::{self, style::SharedTheme},\n};\nuse anyhow::Result;\nuse asyncgit::{\n\tsync::{\n\t\tcred::{\n\t\t\textract_username_password, need_username_password,\n\t\t\tBasicAuthCredential,\n\t\t},\n\t\tget_default_remote, hooks_pre_push, AsyncProgress,\n\t\tHookResult, PushTagsProgress, RepoPathRef,\n\t},\n\tAsyncGitNotification, AsyncPushTags, PushTagsRequest,\n};\nuse crossterm::event::Event;\nuse ratatui::{\n\tlayout::Rect,\n\ttext::Span,\n\twidgets::{Block, BorderType, Borders, Clear, Gauge},\n\tFrame,\n};\n\n///\npub struct PushTagsPopup {\n\trepo: RepoPathRef,\n\tvisible: bool,\n\tgit_push: AsyncPushTags,\n\tprogress: Option<PushTagsProgress>,\n\tpending: bool,\n\tqueue: Queue,\n\ttheme: SharedTheme,\n\tkey_config: SharedKeyConfig,\n\tinput_cred: CredComponent,\n}\n\nimpl PushTagsPopup {\n\t///\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\trepo: env.repo.clone(),\n\t\t\tqueue: env.queue.clone(),\n\t\t\tpending: false,\n\t\t\tvisible: false,\n\t\t\tgit_push: AsyncPushTags::new(\n\t\t\t\tenv.repo.borrow().clone(),\n\t\t\t\t&env.sender_git,\n\t\t\t),\n\t\t\tprogress: None,\n\t\t\tinput_cred: CredComponent::new(env),\n\t\t\ttheme: env.theme.clone(),\n\t\t\tkey_config: env.key_config.clone(),\n\t\t}\n\t}\n\n\t///\n\tpub fn push_tags(&mut self) -> Result<()> {\n\t\tself.show()?;\n\t\tif need_username_password(&self.repo.borrow())? {\n\t\t\tlet cred = extract_username_password(&self.repo.borrow())\n\t\t\t\t.unwrap_or_else(|_| {\n\t\t\t\t\tBasicAuthCredential::new(None, None)\n\t\t\t\t});\n\t\t\tif cred.is_complete() {\n\t\t\t\tself.push_to_remote(Some(cred))\n\t\t\t} else {\n\t\t\t\tself.input_cred.set_cred(cred);\n\t\t\t\tself.input_cred.show()\n\t\t\t}\n\t\t} else {\n\t\t\tself.push_to_remote(None)\n\t\t}\n\t}\n\n\tfn push_to_remote(\n\t\t&mut self,\n\t\tcred: Option<BasicAuthCredential>,\n\t) -> Result<()> {\n\t\tlet remote = get_default_remote(&self.repo.borrow())?;\n\n\t\tlet repo = self.repo.borrow();\n\t\tif let HookResult::NotOk(e) = hooks_pre_push(\n\t\t\t&repo,\n\t\t\t&remote,\n\t\t\t&asyncgit::sync::PrePushTarget::Tags,\n\t\t\tcred.clone(),\n\t\t)? {\n\t\t\tlog::error!(\"pre-push hook failed: {e}\");\n\t\t\tself.queue.push(InternalEvent::ShowErrorMsg(format!(\n\t\t\t\t\"pre-push hook failed:\\n{e}\"\n\t\t\t)));\n\t\t\tself.pending = false;\n\t\t\tself.visible = false;\n\t\t\treturn Ok(());\n\t\t}\n\n\t\tself.pending = true;\n\t\tself.progress = None;\n\t\tself.git_push.request(PushTagsRequest {\n\t\t\tremote,\n\t\t\tbasic_credential: cred,\n\t\t})?;\n\t\tOk(())\n\t}\n\n\t///\n\tpub fn update_git(\n\t\t&mut self,\n\t\tev: AsyncGitNotification,\n\t) -> Result<()> {\n\t\tif self.is_visible() && ev == AsyncGitNotification::PushTags {\n\t\t\tself.update()?;\n\t\t}\n\n\t\tOk(())\n\t}\n\n\t///\n\tfn update(&mut self) -> Result<()> {\n\t\tself.pending = self.git_push.is_pending()?;\n\t\tself.progress = self.git_push.progress()?;\n\n\t\tif !self.pending {\n\t\t\tif let Some(err) = self.git_push.last_result()? {\n\t\t\t\tself.queue.push(InternalEvent::ShowErrorMsg(\n\t\t\t\t\tformat!(\"push tags failed:\\n{err}\"),\n\t\t\t\t));\n\t\t\t}\n\t\t\tself.hide();\n\t\t}\n\n\t\tOk(())\n\t}\n\n\t///\n\tpub const fn any_work_pending(&self) -> bool {\n\t\tself.pending\n\t}\n\n\t///\n\tpub fn get_progress(\n\t\tprogress: Option<&PushTagsProgress>,\n\t) -> (String, u8) {\n\t\tprogress.as_ref().map_or_else(\n\t\t\t|| (strings::PUSH_POPUP_PROGRESS_NONE.into(), 0),\n\t\t\t|progress| {\n\t\t\t\t(\n\t\t\t\t\tSelf::progress_state_name(progress),\n\t\t\t\t\tprogress.progress().progress,\n\t\t\t\t)\n\t\t\t},\n\t\t)\n\t}\n\n\tfn progress_state_name(progress: &PushTagsProgress) -> String {\n\t\tmatch progress {\n\t\t\tPushTagsProgress::CheckRemote => {\n\t\t\t\tstrings::PUSH_TAGS_STATES_FETCHING\n\t\t\t}\n\t\t\tPushTagsProgress::Push { .. } => {\n\t\t\t\tstrings::PUSH_TAGS_STATES_PUSHING\n\t\t\t}\n\t\t\tPushTagsProgress::Done => strings::PUSH_TAGS_STATES_DONE,\n\t\t}\n\t\t.to_string()\n\t}\n}\n\nimpl DrawableComponent for PushTagsPopup {\n\tfn draw(&self, f: &mut Frame, rect: Rect) -> Result<()> {\n\t\tif self.visible {\n\t\t\tlet (state, progress) =\n\t\t\t\tSelf::get_progress(self.progress.as_ref());\n\n\t\t\tlet area = ui::centered_rect_absolute(30, 3, f.area());\n\n\t\t\tf.render_widget(Clear, area);\n\t\t\tf.render_widget(\n\t\t\t\tGauge::default()\n\t\t\t\t\t.label(state.as_str())\n\t\t\t\t\t.block(\n\t\t\t\t\t\tBlock::default()\n\t\t\t\t\t\t\t.title(Span::styled(\n\t\t\t\t\t\t\t\tstrings::PUSH_TAGS_POPUP_MSG,\n\t\t\t\t\t\t\t\tself.theme.title(true),\n\t\t\t\t\t\t\t))\n\t\t\t\t\t\t\t.borders(Borders::ALL)\n\t\t\t\t\t\t\t.border_type(BorderType::Thick)\n\t\t\t\t\t\t\t.border_style(self.theme.block(true)),\n\t\t\t\t\t)\n\t\t\t\t\t.gauge_style(self.theme.push_gauge())\n\t\t\t\t\t.percent(u16::from(progress)),\n\t\t\t\tarea,\n\t\t\t);\n\t\t\tself.input_cred.draw(f, rect)?;\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for PushTagsPopup {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.is_visible() || force_all {\n\t\t\tif !force_all {\n\t\t\t\tout.clear();\n\t\t\t}\n\n\t\t\tif self.input_cred.is_visible() {\n\t\t\t\treturn self.input_cred.commands(out, force_all);\n\t\t\t}\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::close_msg(&self.key_config),\n\t\t\t\t!self.pending,\n\t\t\t\tself.visible,\n\t\t\t));\n\t\t}\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(&mut self, ev: &Event) -> Result<EventState> {\n\t\tif self.visible {\n\t\t\tif let Event::Key(e) = ev {\n\t\t\t\tif self.input_cred.is_visible() {\n\t\t\t\t\tself.input_cred.event(ev)?;\n\n\t\t\t\t\tif self.input_cred.get_cred().is_complete()\n\t\t\t\t\t\t|| !self.input_cred.is_visible()\n\t\t\t\t\t{\n\t\t\t\t\t\tself.push_to_remote(Some(\n\t\t\t\t\t\t\tself.input_cred.get_cred().clone(),\n\t\t\t\t\t\t))?;\n\t\t\t\t\t\tself.input_cred.hide();\n\t\t\t\t\t}\n\t\t\t\t} else if key_match(\n\t\t\t\t\te,\n\t\t\t\t\tself.key_config.keys.exit_popup,\n\t\t\t\t) && !self.pending\n\t\t\t\t{\n\t\t\t\t\tself.hide();\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn Ok(EventState::Consumed);\n\t\t}\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.visible = false;\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "src/popups/remotelist.rs",
    "content": "use std::cell::Cell;\n\nuse asyncgit::sync::{get_remote_url, get_remotes, RepoPathRef};\nuse ratatui::{\n\tlayout::{\n\t\tAlignment, Constraint, Direction, Layout, Margin, Rect,\n\t},\n\ttext::{Line, Span, Text},\n\twidgets::{Block, BorderType, Borders, Clear, Paragraph, Wrap},\n\tFrame,\n};\nuse unicode_truncate::UnicodeTruncateStr;\n\nuse crate::{\n\tapp::Environment,\n\tcomponents::{\n\t\tvisibility_blocking, CommandBlocking, CommandInfo, Component,\n\t\tDrawableComponent, EventState, ScrollType, VerticalScroll,\n\t},\n\tkeys::{key_match, SharedKeyConfig},\n\tqueue::{Action, InternalEvent, Queue},\n\tstrings,\n\tui::{self, style::SharedTheme, Size},\n};\nuse anyhow::Result;\nuse crossterm::event::{Event, KeyEvent};\n\npub struct RemoteListPopup {\n\tremote_names: Vec<String>,\n\trepo: RepoPathRef,\n\tvisible: bool,\n\tcurrent_height: Cell<u16>,\n\tqueue: Queue,\n\tselection: u16,\n\tscroll: VerticalScroll,\n\ttheme: SharedTheme,\n\tkey_config: SharedKeyConfig,\n}\n\nimpl DrawableComponent for RemoteListPopup {\n\tfn draw(&self, f: &mut Frame, rect: Rect) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tconst PERCENT_SIZE: Size = Size::new(40, 30);\n\t\t\tconst MIN_SIZE: Size = Size::new(30, 20);\n\t\t\tlet area = ui::centered_rect(\n\t\t\t\tPERCENT_SIZE.width,\n\t\t\t\tPERCENT_SIZE.height,\n\t\t\t\trect,\n\t\t\t);\n\t\t\tlet area = ui::rect_inside(MIN_SIZE, rect.into(), area);\n\t\t\tlet area = area.intersection(rect);\n\t\t\tf.render_widget(Clear, area);\n\t\t\tf.render_widget(\n\t\t\t\tBlock::default()\n\t\t\t\t\t.title(strings::POPUP_TITLE_REMOTES)\n\t\t\t\t\t.border_type(BorderType::Thick)\n\t\t\t\t\t.borders(Borders::ALL),\n\t\t\t\tarea,\n\t\t\t);\n\t\t\tlet area = area.inner(Margin {\n\t\t\t\tvertical: 1,\n\t\t\t\thorizontal: 1,\n\t\t\t});\n\t\t\tlet chunks = Layout::default()\n\t\t\t\t.direction(Direction::Vertical)\n\t\t\t\t.constraints(vec![\n\t\t\t\t\tConstraint::Min(1),\n\t\t\t\t\tConstraint::Length(1),\n\t\t\t\t\tConstraint::Length(2),\n\t\t\t\t])\n\t\t\t\t.split(area);\n\t\t\tself.draw_remotes_list(f, chunks[0])?;\n\t\t\tself.draw_separator(f, chunks[1]);\n\t\t\tself.draw_selected_remote_details(f, chunks[2]);\n\t\t}\n\t\tOk(())\n\t}\n}\n\nimpl Component for RemoteListPopup {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.is_visible() || force_all {\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::scroll(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\ttrue,\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::close_popup(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\tself.is_visible(),\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::update_remote_name(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t),\n\t\t\t\ttrue,\n\t\t\t\tself.valid_selection(),\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::update_remote_url(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t),\n\t\t\t\ttrue,\n\t\t\t\tself.valid_selection(),\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::create_remote(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\tself.valid_selection(),\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::delete_remote_popup(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t),\n\t\t\t\ttrue,\n\t\t\t\tself.valid_selection(),\n\t\t\t));\n\t\t}\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(&mut self, ev: &Event) -> Result<EventState> {\n\t\tif !self.visible {\n\t\t\treturn Ok(EventState::NotConsumed);\n\t\t}\n\n\t\tif let Event::Key(e) = ev {\n\t\t\tif self.move_event(e)?.is_consumed() {\n\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t} else if key_match(e, self.key_config.keys.add_remote) {\n\t\t\t\tself.queue.push(InternalEvent::CreateRemote);\n\t\t\t} else if key_match(e, self.key_config.keys.delete_remote)\n\t\t\t\t&& self.valid_selection()\n\t\t\t{\n\t\t\t\tself.delete_remote();\n\t\t\t} else if key_match(\n\t\t\t\te,\n\t\t\t\tself.key_config.keys.update_remote_name,\n\t\t\t) && self.valid_selection()\n\t\t\t{\n\t\t\t\tself.rename_remote();\n\t\t\t} else if key_match(\n\t\t\t\te,\n\t\t\t\tself.key_config.keys.update_remote_url,\n\t\t\t) && self.valid_selection()\n\t\t\t{\n\t\t\t\tself.update_remote_url();\n\t\t\t}\n\t\t}\n\t\tOk(EventState::Consumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.visible = false;\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\t\tOk(())\n\t}\n}\n\nimpl RemoteListPopup {\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\tremote_names: Vec::new(),\n\t\t\trepo: env.repo.clone(),\n\t\t\tvisible: false,\n\t\t\tscroll: VerticalScroll::new(),\n\t\t\ttheme: env.theme.clone(),\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\tqueue: env.queue.clone(),\n\t\t\tcurrent_height: Cell::new(0),\n\t\t\tselection: 0,\n\t\t}\n\t}\n\n\tfn move_event(&mut self, e: &KeyEvent) -> Result<EventState> {\n\t\tif key_match(e, self.key_config.keys.exit_popup) {\n\t\t\tself.hide();\n\t\t} else if key_match(e, self.key_config.keys.move_down) {\n\t\t\treturn self\n\t\t\t\t.move_selection(ScrollType::Up)\n\t\t\t\t.map(Into::into);\n\t\t} else if key_match(e, self.key_config.keys.move_up) {\n\t\t\treturn self\n\t\t\t\t.move_selection(ScrollType::Down)\n\t\t\t\t.map(Into::into);\n\t\t} else if key_match(e, self.key_config.keys.page_down) {\n\t\t\treturn self\n\t\t\t\t.move_selection(ScrollType::PageDown)\n\t\t\t\t.map(Into::into);\n\t\t} else if key_match(e, self.key_config.keys.page_up) {\n\t\t\treturn self\n\t\t\t\t.move_selection(ScrollType::PageUp)\n\t\t\t\t.map(Into::into);\n\t\t} else if key_match(e, self.key_config.keys.home) {\n\t\t\treturn self\n\t\t\t\t.move_selection(ScrollType::Home)\n\t\t\t\t.map(Into::into);\n\t\t} else if key_match(e, self.key_config.keys.end) {\n\t\t\treturn self\n\t\t\t\t.move_selection(ScrollType::End)\n\t\t\t\t.map(Into::into);\n\t\t}\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\t///\n\tpub fn open(&mut self) -> Result<()> {\n\t\tself.show()?;\n\t\tself.update_remotes()?;\n\t\tOk(())\n\t}\n\n\tfn get_text(\n\t\t&self,\n\t\ttheme: &SharedTheme,\n\t\twidth_available: u16,\n\t\theight: usize,\n\t) -> Text<'_> {\n\t\tconst THREE_DOTS: &str = \"...\";\n\t\tconst THREE_DOTS_LENGTH: usize = THREE_DOTS.len(); // \"...\"\n\n\t\tlet name_length: usize = (width_available as usize)\n\t\t\t.saturating_sub(THREE_DOTS_LENGTH);\n\n\t\tText::from(\n\t\t\tself.remote_names\n\t\t\t\t.iter()\n\t\t\t\t.skip(self.scroll.get_top())\n\t\t\t\t.take(height)\n\t\t\t\t.enumerate()\n\t\t\t\t.map(|(i, remote)| {\n\t\t\t\t\tlet selected = (self.selection as usize\n\t\t\t\t\t\t- self.scroll.get_top())\n\t\t\t\t\t\t== i;\n\t\t\t\t\tlet mut remote_name = remote.clone();\n\t\t\t\t\tif remote_name.len()\n\t\t\t\t\t\t> name_length\n\t\t\t\t\t\t\t.saturating_sub(THREE_DOTS_LENGTH)\n\t\t\t\t\t{\n\t\t\t\t\t\tremote_name = remote_name\n\t\t\t\t\t\t\t.unicode_truncate(\n\t\t\t\t\t\t\t\tname_length.saturating_sub(\n\t\t\t\t\t\t\t\t\tTHREE_DOTS_LENGTH,\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t.0\n\t\t\t\t\t\t\t.to_string();\n\t\t\t\t\t\tremote_name += THREE_DOTS;\n\t\t\t\t\t}\n\t\t\t\t\tlet span_name = Span::styled(\n\t\t\t\t\t\tformat!(\"{remote_name:name_length$}\"),\n\t\t\t\t\t\ttheme.text(true, selected),\n\t\t\t\t\t);\n\t\t\t\t\tLine::from(vec![span_name])\n\t\t\t\t})\n\t\t\t\t.collect::<Vec<_>>(),\n\t\t)\n\t}\n\n\tfn draw_remotes_list(\n\t\t&self,\n\t\tf: &mut Frame,\n\t\tr: Rect,\n\t) -> Result<()> {\n\t\tlet height_in_lines = r.height as usize;\n\t\tself.current_height.set(height_in_lines.try_into()?);\n\n\t\tself.scroll.update(\n\t\t\tself.selection as usize,\n\t\t\tself.remote_names.len(),\n\t\t\theight_in_lines,\n\t\t);\n\n\t\tf.render_widget(\n\t\t\tParagraph::new(self.get_text(\n\t\t\t\t&self.theme,\n\t\t\t\tr.width.saturating_add(1),\n\t\t\t\theight_in_lines,\n\t\t\t))\n\t\t\t.alignment(Alignment::Left),\n\t\t\tr,\n\t\t);\n\n\t\tlet mut r = r;\n\t\tr.width += 1;\n\t\tr.height += 2;\n\t\tr.y = r.y.saturating_sub(1);\n\n\t\tself.scroll.draw(f, r, &self.theme);\n\n\t\tOk(())\n\t}\n\n\tfn draw_separator(&self, f: &mut Frame, r: Rect) {\n\t\t// Discard self argument because it is not needed.\n\t\tlet _ = self;\n\t\tf.render_widget(\n\t\t\tBlock::default()\n\t\t\t\t.title(strings::POPUP_SUBTITLE_REMOTES)\n\t\t\t\t.border_type(BorderType::Plain)\n\t\t\t\t.borders(Borders::TOP),\n\t\t\tr,\n\t\t);\n\t}\n\n\tfn draw_selected_remote_details(&self, f: &mut Frame, r: Rect) {\n\t\tconst THREE_DOTS: &str = \"...\";\n\t\tconst THREE_DOTS_LENGTH: usize = THREE_DOTS.len(); // \"...\"\n\t\tconst REMOTE_NAME_LABEL: &str = \"name: \";\n\t\tconst REMOTE_NAME_LABEL_LENGTH: usize =\n\t\t\tREMOTE_NAME_LABEL.len();\n\t\tconst REMOTE_URL_LABEL: &str = \"url: \";\n\t\tconst REMOTE_URL_LABEL_LENGTH: usize = REMOTE_URL_LABEL.len();\n\n\t\tlet name_length: usize = (r.width.saturating_sub(1) as usize)\n\t\t\t.saturating_sub(REMOTE_NAME_LABEL_LENGTH);\n\t\tlet url_length: usize = (r.width.saturating_sub(1) as usize)\n\t\t\t.saturating_sub(REMOTE_URL_LABEL_LENGTH);\n\n\t\tlet remote =\n\t\t\tself.remote_names.get(usize::from(self.selection));\n\t\tif let Some(remote) = remote {\n\t\t\tlet mut remote_name = remote.clone();\n\t\t\tif remote_name.len()\n\t\t\t\t> name_length.saturating_sub(THREE_DOTS_LENGTH)\n\t\t\t{\n\t\t\t\tremote_name = remote_name\n\t\t\t\t\t.unicode_truncate(\n\t\t\t\t\t\tname_length.saturating_sub(THREE_DOTS_LENGTH),\n\t\t\t\t\t)\n\t\t\t\t\t.0\n\t\t\t\t\t.to_string();\n\t\t\t\tremote_name += THREE_DOTS;\n\t\t\t}\n\t\t\tlet mut lines = Vec::<Line>::new();\n\t\t\tlines.push(Line::from(Span::styled(\n\t\t\t\tformat!(\n\t\t\t\t\t\"{REMOTE_NAME_LABEL}{remote_name:name_length$}\"\n\t\t\t\t),\n\t\t\t\tself.theme.text(true, false),\n\t\t\t)));\n\t\t\tlet remote_url =\n\t\t\t\tget_remote_url(&self.repo.borrow(), remote);\n\t\t\tif let Ok(Some(mut remote_url)) = remote_url {\n\t\t\t\tif remote_url.len()\n\t\t\t\t\t> url_length.saturating_sub(THREE_DOTS_LENGTH)\n\t\t\t\t{\n\t\t\t\t\tremote_url = remote_url\n\t\t\t\t\t\t.chars()\n\t\t\t\t\t\t.skip(\n\t\t\t\t\t\t\tremote_url.len()\n\t\t\t\t\t\t\t\t- url_length.saturating_sub(\n\t\t\t\t\t\t\t\t\tTHREE_DOTS_LENGTH,\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.collect::<String>();\n\t\t\t\t\tremote_url = format!(\"{THREE_DOTS}{remote_url}\");\n\t\t\t\t}\n\t\t\t\tlines.push(Line::from(Span::styled(\n\t\t\t\t\tformat!(\n\t\t\t\t\t\t\"{REMOTE_URL_LABEL}{remote_url:url_length$}\"\n\t\t\t\t\t),\n\t\t\t\t\tself.theme.text(true, false),\n\t\t\t\t)));\n\t\t\t}\n\t\t\tf.render_widget(\n\t\t\t\tParagraph::new(Text::from(lines))\n\t\t\t\t\t.alignment(Alignment::Left)\n\t\t\t\t\t.wrap(Wrap { trim: true }),\n\t\t\t\tr,\n\t\t\t);\n\t\t}\n\t}\n\n\t///\n\tfn move_selection(&mut self, scroll: ScrollType) -> Result<bool> {\n\t\tlet new_selection = match scroll {\n\t\t\tScrollType::Up => self.selection.saturating_add(1),\n\t\t\tScrollType::Down => self.selection.saturating_sub(1),\n\t\t\tScrollType::PageDown => self\n\t\t\t\t.selection\n\t\t\t\t.saturating_add(self.current_height.get()),\n\t\t\tScrollType::PageUp => self\n\t\t\t\t.selection\n\t\t\t\t.saturating_sub(self.current_height.get()),\n\t\t\tScrollType::Home => 0,\n\t\t\tScrollType::End => {\n\t\t\t\tlet num_branches: u16 =\n\t\t\t\t\tself.remote_names.len().try_into()?;\n\t\t\t\tnum_branches.saturating_sub(1)\n\t\t\t}\n\t\t};\n\n\t\tself.set_selection(new_selection)?;\n\n\t\tOk(true)\n\t}\n\n\tconst fn valid_selection(&self) -> bool {\n\t\t!self.remote_names.is_empty()\n\t\t\t&& self.remote_names.len() >= self.selection as usize\n\t}\n\n\tfn set_selection(&mut self, selection: u16) -> Result<()> {\n\t\tlet num_remotes: u16 = self.remote_names.len().try_into()?;\n\t\tlet num_remotes = num_remotes.saturating_sub(1);\n\n\t\tlet selection = if selection > num_remotes {\n\t\t\tnum_remotes\n\t\t} else {\n\t\t\tselection\n\t\t};\n\n\t\tself.selection = selection;\n\n\t\tOk(())\n\t}\n\n\tpub fn update_remotes(&mut self) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tself.remote_names = get_remotes(&self.repo.borrow())?;\n\t\t\tself.set_selection(self.selection)?;\n\t\t}\n\t\tOk(())\n\t}\n\n\tfn delete_remote(&self) {\n\t\tlet remote_name =\n\t\t\tself.remote_names[self.selection as usize].clone();\n\n\t\tself.queue.push(InternalEvent::ConfirmAction(\n\t\t\tAction::DeleteRemote(remote_name),\n\t\t));\n\t}\n\n\tfn rename_remote(&self) {\n\t\tlet remote_name =\n\t\t\tself.remote_names[self.selection as usize].clone();\n\n\t\tself.queue.push(InternalEvent::RenameRemote(remote_name));\n\t}\n\n\tfn update_remote_url(&self) {\n\t\tlet remote_name =\n\t\t\tself.remote_names[self.selection as usize].clone();\n\t\tlet remote_url =\n\t\t\tget_remote_url(&self.repo.borrow(), &remote_name);\n\t\tif let Ok(Some(url)) = remote_url {\n\t\t\tself.queue.push(InternalEvent::UpdateRemoteUrl(\n\t\t\t\tremote_name,\n\t\t\t\turl,\n\t\t\t));\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/popups/rename_branch.rs",
    "content": "use crate::components::{\n\tvisibility_blocking, CommandBlocking, CommandInfo, Component,\n\tDrawableComponent, EventState, InputType, TextInputComponent,\n};\nuse crate::ui::style::SharedTheme;\nuse crate::{\n\tapp::Environment,\n\tkeys::{key_match, SharedKeyConfig},\n\tqueue::{InternalEvent, NeedsUpdate, Queue},\n\tstrings,\n};\nuse anyhow::Result;\nuse asyncgit::sync::{self, RepoPathRef};\nuse crossterm::event::Event;\nuse easy_cast::Cast;\nuse ratatui::{layout::Rect, widgets::Paragraph, Frame};\n\npub struct RenameBranchPopup {\n\trepo: RepoPathRef,\n\tinput: TextInputComponent,\n\tbranch_ref: Option<String>,\n\tqueue: Queue,\n\tkey_config: SharedKeyConfig,\n\ttheme: SharedTheme,\n}\n\nimpl DrawableComponent for RenameBranchPopup {\n\tfn draw(&self, f: &mut Frame, rect: Rect) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tself.input.draw(f, rect)?;\n\t\t\tself.draw_warnings(f);\n\t\t}\n\t\tOk(())\n\t}\n}\n\nimpl Component for RenameBranchPopup {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.is_visible() || force_all {\n\t\t\tself.input.commands(out, force_all);\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::rename_branch_confirm_msg(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t),\n\t\t\t\ttrue,\n\t\t\t\ttrue,\n\t\t\t));\n\t\t}\n\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(&mut self, ev: &Event) -> Result<EventState> {\n\t\tif self.is_visible() {\n\t\t\tif self.input.event(ev)?.is_consumed() {\n\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t}\n\n\t\t\tif let Event::Key(e) = ev {\n\t\t\t\tif key_match(e, self.key_config.keys.enter) {\n\t\t\t\t\tself.rename_branch();\n\t\t\t\t}\n\n\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t}\n\t\t}\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.input.is_visible()\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.input.hide();\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.input.show()?;\n\n\t\tOk(())\n\t}\n}\n\nimpl RenameBranchPopup {\n\t///\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\trepo: env.repo.clone(),\n\t\t\tqueue: env.queue.clone(),\n\t\t\tinput: TextInputComponent::new(\n\t\t\t\tenv,\n\t\t\t\t&strings::rename_branch_popup_title(&env.key_config),\n\t\t\t\t&strings::rename_branch_popup_msg(&env.key_config),\n\t\t\t\ttrue,\n\t\t\t)\n\t\t\t.with_input_type(InputType::Singleline),\n\t\t\tbranch_ref: None,\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\ttheme: env.theme.clone(),\n\t\t}\n\t}\n\n\t///\n\tpub fn open(\n\t\t&mut self,\n\t\tbranch_ref: String,\n\t\tcur_name: String,\n\t) -> Result<()> {\n\t\tself.branch_ref = None;\n\t\tself.branch_ref = Some(branch_ref);\n\t\tself.input.set_text(cur_name);\n\t\tself.show()?;\n\n\t\tOk(())\n\t}\n\n\t///\n\tpub fn rename_branch(&mut self) {\n\t\tif let Some(br) = &self.branch_ref {\n\t\t\tlet res = sync::rename_branch(\n\t\t\t\t&self.repo.borrow(),\n\t\t\t\tbr,\n\t\t\t\tself.input.get_text(),\n\t\t\t);\n\n\t\t\tmatch res {\n\t\t\t\tOk(()) => {\n\t\t\t\t\tself.queue.push(InternalEvent::Update(\n\t\t\t\t\t\tNeedsUpdate::ALL,\n\t\t\t\t\t));\n\t\t\t\t\tself.hide();\n\t\t\t\t\tself.queue.push(InternalEvent::SelectBranch);\n\t\t\t\t}\n\t\t\t\tErr(e) => {\n\t\t\t\t\tlog::error!(\"create branch: {e}\");\n\t\t\t\t\tself.queue.push(InternalEvent::ShowErrorMsg(\n\t\t\t\t\t\tformat!(\"rename branch error:\\n{e}\"),\n\t\t\t\t\t));\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tlog::error!(\"create branch: No branch selected\");\n\t\t\tself.queue.push(InternalEvent::ShowErrorMsg(\n\t\t\t\t\"rename branch error: No branch selected to rename\"\n\t\t\t\t\t.to_string(),\n\t\t\t));\n\t\t}\n\n\t\tself.input.clear();\n\t}\n\n\tfn draw_warnings(&self, f: &mut Frame) {\n\t\tlet current_text = self.input.get_text();\n\n\t\tif !current_text.is_empty() {\n\t\t\tlet valid = sync::validate_branch_name(current_text)\n\t\t\t\t.unwrap_or_default();\n\n\t\t\tif !valid {\n\t\t\t\tlet msg = strings::branch_name_invalid();\n\t\t\t\tlet msg_length: u16 = msg.len().cast();\n\t\t\t\tlet w = Paragraph::new(msg)\n\t\t\t\t\t.style(self.theme.text_danger());\n\n\t\t\t\tlet rect = {\n\t\t\t\t\tlet mut rect = self.input.get_area();\n\t\t\t\t\trect.y += rect.height.saturating_sub(1);\n\t\t\t\t\trect.height = 1;\n\t\t\t\t\tlet offset =\n\t\t\t\t\t\trect.width.saturating_sub(msg_length + 1);\n\t\t\t\t\trect.width =\n\t\t\t\t\t\trect.width.saturating_sub(offset + 1);\n\t\t\t\t\trect.x += offset;\n\n\t\t\t\t\trect\n\t\t\t\t};\n\n\t\t\t\tf.render_widget(w, rect);\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/popups/rename_remote.rs",
    "content": "use anyhow::Result;\nuse asyncgit::sync::{self, RepoPathRef};\nuse crossterm::event::Event;\nuse easy_cast::Cast;\nuse ratatui::{layout::Rect, widgets::Paragraph, Frame};\n\nuse crate::{\n\tapp::Environment,\n\tcomponents::{\n\t\tvisibility_blocking, CommandBlocking, CommandInfo, Component,\n\t\tDrawableComponent, EventState, InputType, TextInputComponent,\n\t},\n\tkeys::{key_match, SharedKeyConfig},\n\tqueue::{InternalEvent, NeedsUpdate, Queue},\n\tstrings,\n\tui::style::SharedTheme,\n};\n\npub struct RenameRemotePopup {\n\trepo: RepoPathRef,\n\tinput: TextInputComponent,\n\ttheme: SharedTheme,\n\tkey_config: SharedKeyConfig,\n\tqueue: Queue,\n\tinitial_name: Option<String>,\n}\n\nimpl DrawableComponent for RenameRemotePopup {\n\tfn draw(&self, f: &mut Frame, rect: Rect) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tself.input.draw(f, rect)?;\n\t\t\tself.draw_warnings(f);\n\t\t}\n\t\tOk(())\n\t}\n}\n\nimpl Component for RenameRemotePopup {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.is_visible() || force_all {\n\t\t\tself.input.commands(out, force_all);\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::remote_confirm_name_msg(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t),\n\t\t\t\ttrue,\n\t\t\t\ttrue,\n\t\t\t));\n\t\t}\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(&mut self, ev: &Event) -> Result<EventState> {\n\t\tif self.is_visible() {\n\t\t\tif self.input.event(ev)?.is_consumed() {\n\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t}\n\n\t\t\tif let Event::Key(e) = ev {\n\t\t\t\tif key_match(e, self.key_config.keys.enter) {\n\t\t\t\t\tself.rename_remote();\n\t\t\t\t}\n\n\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t}\n\t\t}\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.input.is_visible()\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.input.hide();\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.input.show()?;\n\n\t\tOk(())\n\t}\n}\n\nimpl RenameRemotePopup {\n\t///\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\trepo: env.repo.clone(),\n\t\t\tinput: TextInputComponent::new(\n\t\t\t\tenv,\n\t\t\t\t&strings::rename_remote_popup_title(&env.key_config),\n\t\t\t\t&strings::rename_remote_popup_msg(&env.key_config),\n\t\t\t\ttrue,\n\t\t\t)\n\t\t\t.with_input_type(InputType::Singleline),\n\t\t\ttheme: env.theme.clone(),\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\tqueue: env.queue.clone(),\n\t\t\tinitial_name: None,\n\t\t}\n\t}\n\n\t///\n\tpub fn open(&mut self, cur_name: String) -> Result<()> {\n\t\tself.input.set_text(cur_name.clone());\n\t\tself.initial_name = Some(cur_name);\n\t\tself.show()?;\n\n\t\tOk(())\n\t}\n\n\tfn draw_warnings(&self, f: &mut Frame) {\n\t\tlet current_text = self.input.get_text();\n\n\t\tif !current_text.is_empty() {\n\t\t\tlet valid = sync::validate_remote_name(current_text);\n\n\t\t\tif !valid {\n\t\t\t\tlet msg = strings::branch_name_invalid();\n\t\t\t\tlet msg_length: u16 = msg.len().cast();\n\t\t\t\tlet w = Paragraph::new(msg)\n\t\t\t\t\t.style(self.theme.text_danger());\n\n\t\t\t\tlet rect = {\n\t\t\t\t\tlet mut rect = self.input.get_area();\n\t\t\t\t\trect.y += rect.height.saturating_sub(1);\n\t\t\t\t\trect.height = 1;\n\t\t\t\t\tlet offset =\n\t\t\t\t\t\trect.width.saturating_sub(msg_length + 1);\n\t\t\t\t\trect.width =\n\t\t\t\t\t\trect.width.saturating_sub(offset + 1);\n\t\t\t\t\trect.x += offset;\n\n\t\t\t\t\trect\n\t\t\t\t};\n\n\t\t\t\tf.render_widget(w, rect);\n\t\t\t}\n\t\t}\n\t}\n\n\t///\n\tpub fn rename_remote(&mut self) {\n\t\tif let Some(init_name) = &self.initial_name {\n\t\t\tif init_name != self.input.get_text() {\n\t\t\t\tlet res = sync::rename_remote(\n\t\t\t\t\t&self.repo.borrow(),\n\t\t\t\t\tinit_name,\n\t\t\t\t\tself.input.get_text(),\n\t\t\t\t);\n\t\t\t\tmatch res {\n\t\t\t\t\tOk(()) => {\n\t\t\t\t\t\tself.queue.push(InternalEvent::Update(\n\t\t\t\t\t\t\tNeedsUpdate::ALL | NeedsUpdate::REMOTES,\n\t\t\t\t\t\t));\n\t\t\t\t\t}\n\t\t\t\t\tErr(e) => {\n\t\t\t\t\t\tlog::error!(\"rename remote: {e}\");\n\t\t\t\t\t\tself.queue.push(InternalEvent::ShowErrorMsg(\n\t\t\t\t\t\t\tformat!(\"rename remote error:\\n{e}\"),\n\t\t\t\t\t\t));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tself.input.clear();\n\t\tself.initial_name = None;\n\t\tself.hide();\n\t}\n}\n"
  },
  {
    "path": "src/popups/reset.rs",
    "content": "use crate::components::{\n\tvisibility_blocking, CommandBlocking, CommandInfo, Component,\n\tDrawableComponent, EventState,\n};\nuse crate::{\n\tapp::Environment,\n\tkeys::{key_match, SharedKeyConfig},\n\tqueue::Queue,\n\tstrings, try_or_popup,\n\tui::{self, style::SharedTheme},\n};\nuse anyhow::Result;\nuse asyncgit::{\n\tcached,\n\tsync::{CommitId, RepoPath, ResetType},\n};\nuse crossterm::event::Event;\nuse ratatui::{\n\tlayout::{Alignment, Rect},\n\ttext::{Line, Span},\n\twidgets::{Block, Borders, Clear, Paragraph},\n\tFrame,\n};\n\nconst fn type_to_string(\n\tkind: ResetType,\n) -> (&'static str, &'static str) {\n\tconst RESET_TYPE_DESC_SOFT: &str =\n\t\t\"  🟢 Keep all changes. Stage differences\";\n\tconst RESET_TYPE_DESC_MIXED: &str =\n\t\t\" 🟡 Keep all changes. Unstage differences\";\n\tconst RESET_TYPE_DESC_HARD: &str =\n\t\t\"  🔴 Discard all local changes\";\n\n\tmatch kind {\n\t\tResetType::Soft => (\"Soft\", RESET_TYPE_DESC_SOFT),\n\t\tResetType::Mixed => (\"Mixed\", RESET_TYPE_DESC_MIXED),\n\t\tResetType::Hard => (\"Hard\", RESET_TYPE_DESC_HARD),\n\t}\n}\n\npub struct ResetPopup {\n\tqueue: Queue,\n\trepo: RepoPath,\n\tcommit: Option<CommitId>,\n\tkind: ResetType,\n\tgit_branch_name: cached::BranchName,\n\tvisible: bool,\n\tkey_config: SharedKeyConfig,\n\ttheme: SharedTheme,\n}\n\nimpl ResetPopup {\n\t///\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\tqueue: env.queue.clone(),\n\t\t\trepo: env.repo.borrow().clone(),\n\t\t\tcommit: None,\n\t\t\tkind: ResetType::Soft,\n\t\t\tgit_branch_name: cached::BranchName::new(\n\t\t\t\tenv.repo.clone(),\n\t\t\t),\n\t\t\tvisible: false,\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\ttheme: env.theme.clone(),\n\t\t}\n\t}\n\n\tfn get_text(&self, _width: u16) -> Vec<Line<'_>> {\n\t\tlet mut txt: Vec<Line> = Vec::with_capacity(10);\n\n\t\ttxt.push(Line::from(vec![\n\t\t\tSpan::styled(\n\t\t\t\tString::from(\"Branch: \"),\n\t\t\t\tself.theme.text(true, false),\n\t\t\t),\n\t\t\tSpan::styled(\n\t\t\t\tself.git_branch_name.last().unwrap_or_default(),\n\t\t\t\tself.theme.branch(false, true),\n\t\t\t),\n\t\t]));\n\n\t\ttxt.push(Line::from(vec![\n\t\t\tSpan::styled(\n\t\t\t\tString::from(\"Reset to: \"),\n\t\t\t\tself.theme.text(true, false),\n\t\t\t),\n\t\t\tSpan::styled(\n\t\t\t\tself.commit\n\t\t\t\t\t.map(|c| c.to_string())\n\t\t\t\t\t.unwrap_or_default(),\n\t\t\t\tself.theme.commit_hash(false),\n\t\t\t),\n\t\t]));\n\n\t\tlet (kind_name, kind_desc) = type_to_string(self.kind);\n\n\t\ttxt.push(Line::from(vec![\n\t\t\tSpan::styled(\n\t\t\t\tString::from(\"How: \"),\n\t\t\t\tself.theme.text(true, false),\n\t\t\t),\n\t\t\tSpan::styled(kind_name, self.theme.text(true, true)),\n\t\t\tSpan::styled(kind_desc, self.theme.text(true, false)),\n\t\t]));\n\n\t\ttxt\n\t}\n\n\t///\n\tpub fn open(&mut self, id: CommitId) -> Result<()> {\n\t\tself.show()?;\n\n\t\tself.commit = Some(id);\n\n\t\tOk(())\n\t}\n\n\t///\n\t#[allow(clippy::unnecessary_wraps)]\n\tpub fn update(&mut self) -> Result<()> {\n\t\tself.git_branch_name.lookup().ok();\n\n\t\tOk(())\n\t}\n\n\tfn reset(&mut self) {\n\t\tif let Some(id) = self.commit {\n\t\t\ttry_or_popup!(\n\t\t\t\tself,\n\t\t\t\t\"reset:\",\n\t\t\t\tasyncgit::sync::reset_repo(&self.repo, id, self.kind)\n\t\t\t);\n\t\t}\n\n\t\tself.hide();\n\t}\n\n\tconst fn change_kind(&mut self, incr: bool) {\n\t\tself.kind = if incr {\n\t\t\tmatch self.kind {\n\t\t\t\tResetType::Soft => ResetType::Mixed,\n\t\t\t\tResetType::Mixed => ResetType::Hard,\n\t\t\t\tResetType::Hard => ResetType::Soft,\n\t\t\t}\n\t\t} else {\n\t\t\tmatch self.kind {\n\t\t\t\tResetType::Soft => ResetType::Hard,\n\t\t\t\tResetType::Mixed => ResetType::Soft,\n\t\t\t\tResetType::Hard => ResetType::Mixed,\n\t\t\t}\n\t\t};\n\t}\n}\n\nimpl DrawableComponent for ResetPopup {\n\tfn draw(&self, f: &mut Frame, area: Rect) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tconst SIZE: (u16, u16) = (55, 5);\n\t\t\tlet area =\n\t\t\t\tui::centered_rect_absolute(SIZE.0, SIZE.1, area);\n\n\t\t\tlet width = area.width;\n\n\t\t\tf.render_widget(Clear, area);\n\t\t\tf.render_widget(\n\t\t\t\tParagraph::new(self.get_text(width))\n\t\t\t\t\t.block(\n\t\t\t\t\t\tBlock::default()\n\t\t\t\t\t\t\t.borders(Borders::ALL)\n\t\t\t\t\t\t\t.title(Span::styled(\n\t\t\t\t\t\t\t\t\"Reset\",\n\t\t\t\t\t\t\t\tself.theme.title(true),\n\t\t\t\t\t\t\t))\n\t\t\t\t\t\t\t.border_style(self.theme.block(true)),\n\t\t\t\t\t)\n\t\t\t\t\t.alignment(Alignment::Left),\n\t\t\t\tarea,\n\t\t\t);\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for ResetPopup {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.is_visible() || force_all {\n\t\t\tout.push(\n\t\t\t\tCommandInfo::new(\n\t\t\t\t\tstrings::commands::close_popup(&self.key_config),\n\t\t\t\t\ttrue,\n\t\t\t\t\ttrue,\n\t\t\t\t)\n\t\t\t\t.order(1),\n\t\t\t);\n\n\t\t\tout.push(\n\t\t\t\tCommandInfo::new(\n\t\t\t\t\tstrings::commands::reset_commit(&self.key_config),\n\t\t\t\t\ttrue,\n\t\t\t\t\ttrue,\n\t\t\t\t)\n\t\t\t\t.order(1),\n\t\t\t);\n\n\t\t\tout.push(\n\t\t\t\tCommandInfo::new(\n\t\t\t\t\tstrings::commands::reset_type(&self.key_config),\n\t\t\t\t\ttrue,\n\t\t\t\t\ttrue,\n\t\t\t\t)\n\t\t\t\t.order(1),\n\t\t\t);\n\t\t}\n\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(\n\t\t&mut self,\n\t\tevent: &crossterm::event::Event,\n\t) -> Result<EventState> {\n\t\tif self.is_visible() {\n\t\t\tif let Event::Key(key) = &event {\n\t\t\t\tif key_match(key, self.key_config.keys.exit_popup) {\n\t\t\t\t\tself.hide();\n\t\t\t\t} else if key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.move_down,\n\t\t\t\t) {\n\t\t\t\t\tself.change_kind(true);\n\t\t\t\t} else if key_match(key, self.key_config.keys.move_up)\n\t\t\t\t{\n\t\t\t\t\tself.change_kind(false);\n\t\t\t\t} else if key_match(key, self.key_config.keys.enter) {\n\t\t\t\t\tself.reset();\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn Ok(EventState::Consumed);\n\t\t}\n\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.visible = false;\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "src/popups/revision_files.rs",
    "content": "use crate::components::{\n\tvisibility_blocking, CommandBlocking, CommandInfo, Component,\n\tDrawableComponent, EventState, RevisionFilesComponent,\n};\nuse crate::{\n\tapp::Environment,\n\tkeys::{key_match, SharedKeyConfig},\n\tqueue::{InternalEvent, Queue, StackablePopupOpen},\n\tstrings::{self},\n\tAsyncNotification,\n};\nuse anyhow::Result;\nuse asyncgit::sync::CommitId;\nuse crossterm::event::Event;\nuse ratatui::{layout::Rect, widgets::Clear, Frame};\nuse std::path::Path;\n\n#[derive(Clone, Debug)]\npub struct FileTreeOpen {\n\tpub commit_id: CommitId,\n}\n\nimpl FileTreeOpen {\n\tpub const fn new(commit_id: CommitId) -> Self {\n\t\tSelf { commit_id }\n\t}\n}\n\npub struct RevisionFilesPopup {\n\topen_request: Option<FileTreeOpen>,\n\tvisible: bool,\n\tkey_config: SharedKeyConfig,\n\tfiles: RevisionFilesComponent,\n\tqueue: Queue,\n}\n\nimpl RevisionFilesPopup {\n\t///\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\tfiles: RevisionFilesComponent::new(env, None),\n\t\t\tvisible: false,\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\topen_request: None,\n\t\t\tqueue: env.queue.clone(),\n\t\t}\n\t}\n\n\t///\n\tpub fn open(&mut self, request: FileTreeOpen) -> Result<()> {\n\t\tself.files.set_commit(request.commit_id)?;\n\t\tself.open_request = Some(request);\n\t\tself.show()?;\n\n\t\tOk(())\n\t}\n\n\t///\n\tpub fn update(&mut self, ev: AsyncNotification) -> Result<()> {\n\t\tself.files.update(ev)\n\t}\n\n\t///\n\tpub fn any_work_pending(&self) -> bool {\n\t\tself.files.any_work_pending()\n\t}\n\n\tpub fn file_finder_update(&mut self, file: &Path) {\n\t\tself.files.find_file(file);\n\t}\n\n\tfn hide_stacked(&mut self, stack: bool) {\n\t\tself.hide();\n\n\t\tif stack {\n\t\t\tif let Some(revision) = self.files.revision() {\n\t\t\t\tself.queue.push(InternalEvent::PopupStackPush(\n\t\t\t\t\tStackablePopupOpen::FileTree(FileTreeOpen {\n\t\t\t\t\t\tcommit_id: revision.id,\n\t\t\t\t\t}),\n\t\t\t\t));\n\t\t\t}\n\t\t} else {\n\t\t\tself.queue.push(InternalEvent::PopupStackPop);\n\t\t}\n\t}\n}\n\nimpl DrawableComponent for RevisionFilesPopup {\n\tfn draw(&self, f: &mut Frame, area: Rect) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tf.render_widget(Clear, area);\n\n\t\t\tself.files.draw(f, area)?;\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for RevisionFilesPopup {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.is_visible() || force_all {\n\t\t\tout.push(\n\t\t\t\tCommandInfo::new(\n\t\t\t\t\tstrings::commands::close_popup(&self.key_config),\n\t\t\t\t\ttrue,\n\t\t\t\t\ttrue,\n\t\t\t\t)\n\t\t\t\t.order(1),\n\t\t\t);\n\n\t\t\tself.files.commands(out, force_all);\n\t\t}\n\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(\n\t\t&mut self,\n\t\tevent: &crossterm::event::Event,\n\t) -> Result<EventState> {\n\t\tif self.is_visible() {\n\t\t\tif let Event::Key(key) = event {\n\t\t\t\tif key_match(key, self.key_config.keys.exit_popup) {\n\t\t\t\t\tself.hide_stacked(false);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlet res = self.files.event(event)?;\n\t\t\t//Note: if this made the files hide we need to stack the popup\n\t\t\tif res == EventState::Consumed && !self.files.is_visible()\n\t\t\t{\n\t\t\t\tself.hide_stacked(true);\n\t\t\t}\n\n\t\t\treturn Ok(res);\n\t\t}\n\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.visible = false;\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "src/popups/stashmsg.rs",
    "content": "use crate::components::{\n\tvisibility_blocking, CommandBlocking, CommandInfo, Component,\n\tDrawableComponent, EventState, InputType, TextInputComponent,\n};\nuse crate::{\n\tapp::Environment,\n\tkeys::{key_match, SharedKeyConfig},\n\tqueue::{AppTabs, InternalEvent, Queue},\n\tstrings,\n\ttabs::StashingOptions,\n};\nuse anyhow::Result;\nuse asyncgit::sync::{self, RepoPathRef};\nuse crossterm::event::Event;\nuse ratatui::{layout::Rect, Frame};\n\npub struct StashMsgPopup {\n\trepo: RepoPathRef,\n\toptions: StashingOptions,\n\tinput: TextInputComponent,\n\tqueue: Queue,\n\tkey_config: SharedKeyConfig,\n}\n\nimpl DrawableComponent for StashMsgPopup {\n\tfn draw(&self, f: &mut Frame, rect: Rect) -> Result<()> {\n\t\tself.input.draw(f, rect)?;\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for StashMsgPopup {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.is_visible() || force_all {\n\t\t\tself.input.commands(out, force_all);\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::stashing_confirm_msg(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t),\n\t\t\t\ttrue,\n\t\t\t\ttrue,\n\t\t\t));\n\t\t}\n\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(&mut self, ev: &Event) -> Result<EventState> {\n\t\tif self.is_visible() {\n\t\t\tif self.input.event(ev)?.is_consumed() {\n\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t}\n\n\t\t\tif let Event::Key(e) = ev {\n\t\t\t\tif key_match(e, self.key_config.keys.enter) {\n\t\t\t\t\tlet result = sync::stash_save(\n\t\t\t\t\t\t&self.repo.borrow(),\n\t\t\t\t\t\tif self.input.get_text().is_empty() {\n\t\t\t\t\t\t\tNone\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tSome(self.input.get_text())\n\t\t\t\t\t\t},\n\t\t\t\t\t\tself.options.stash_untracked,\n\t\t\t\t\t\tself.options.keep_index,\n\t\t\t\t\t);\n\t\t\t\t\tmatch result {\n\t\t\t\t\t\tOk(_) => {\n\t\t\t\t\t\t\tself.input.clear();\n\t\t\t\t\t\t\tself.hide();\n\n\t\t\t\t\t\t\tself.queue.push(\n\t\t\t\t\t\t\t\tInternalEvent::TabSwitch(\n\t\t\t\t\t\t\t\t\tAppTabs::Stashlist,\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tErr(e) => {\n\t\t\t\t\t\t\tself.hide();\n\t\t\t\t\t\t\tlog::error!(\n\t\t\t\t\t\t\t\t\"e: {} (options: {:?})\",\n\t\t\t\t\t\t\t\te,\n\t\t\t\t\t\t\t\tself.options\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tself.queue.push(\n                                InternalEvent::ShowErrorMsg(format!(\n                                    \"stash error:\\n{}\\noptions:\\n{:?}\",\n                                    e, self.options\n                                )),\n                            );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// stop key event propagation\n\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t}\n\t\t}\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.input.is_visible()\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.input.hide();\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.input.show()?;\n\n\t\tOk(())\n\t}\n}\n\nimpl StashMsgPopup {\n\t///\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\toptions: StashingOptions::default(),\n\t\t\tqueue: env.queue.clone(),\n\t\t\tinput: TextInputComponent::new(\n\t\t\t\tenv,\n\t\t\t\t&strings::stash_popup_title(&env.key_config),\n\t\t\t\t&strings::stash_popup_msg(&env.key_config),\n\t\t\t\ttrue,\n\t\t\t)\n\t\t\t.with_input_type(InputType::Singleline),\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\trepo: env.repo.clone(),\n\t\t}\n\t}\n\n\t///\n\tpub const fn options(&mut self, options: StashingOptions) {\n\t\tself.options = options;\n\t}\n}\n"
  },
  {
    "path": "src/popups/submodules.rs",
    "content": "use crate::{\n\tapp::Environment,\n\tcomponents::{\n\t\tvisibility_blocking, CommandBlocking, CommandInfo, Component,\n\t\tDrawableComponent, EventState, ScrollType, VerticalScroll,\n\t},\n\tkeys::{key_match, SharedKeyConfig},\n\tqueue::{InternalEvent, NeedsUpdate, Queue},\n\tstrings, try_or_popup,\n\tui::{self, Size},\n};\nuse anyhow::Result;\nuse asyncgit::sync::{\n\tget_submodules, repo_dir, submodule_parent_info,\n\tupdate_submodule, RepoPathRef, SubmoduleInfo,\n\tSubmoduleParentInfo,\n};\nuse crossterm::event::Event;\nuse ratatui::{\n\tlayout::{\n\t\tAlignment, Constraint, Direction, Layout, Margin, Rect,\n\t},\n\ttext::{Line, Span, Text},\n\twidgets::{Block, Borders, Clear, Paragraph},\n\tFrame,\n};\nuse std::cell::Cell;\nuse ui::style::SharedTheme;\nuse unicode_truncate::UnicodeTruncateStr;\n\n///\npub struct SubmodulesListPopup {\n\trepo: RepoPathRef,\n\trepo_path: String,\n\tqueue: Queue,\n\tsubmodules: Vec<SubmoduleInfo>,\n\tsubmodule_parent: Option<SubmoduleParentInfo>,\n\tvisible: bool,\n\tcurrent_height: Cell<u16>,\n\tselection: u16,\n\tscroll: VerticalScroll,\n\ttheme: SharedTheme,\n\tkey_config: SharedKeyConfig,\n}\n\nimpl DrawableComponent for SubmodulesListPopup {\n\tfn draw(&self, f: &mut Frame, rect: Rect) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tconst PERCENT_SIZE: Size = Size::new(80, 80);\n\t\t\tconst MIN_SIZE: Size = Size::new(60, 30);\n\n\t\t\tlet area = ui::centered_rect(\n\t\t\t\tPERCENT_SIZE.width,\n\t\t\t\tPERCENT_SIZE.height,\n\t\t\t\trect,\n\t\t\t);\n\t\t\tlet area = ui::rect_inside(MIN_SIZE, rect.into(), area);\n\t\t\tlet area = area.intersection(rect);\n\n\t\t\tf.render_widget(Clear, area);\n\n\t\t\tf.render_widget(\n\t\t\t\tBlock::default()\n\t\t\t\t\t.title(strings::POPUP_TITLE_SUBMODULES)\n\t\t\t\t\t.border_type(ratatui::widgets::BorderType::Thick)\n\t\t\t\t\t.borders(Borders::ALL),\n\t\t\t\tarea,\n\t\t\t);\n\n\t\t\tlet area = area.inner(Margin {\n\t\t\t\tvertical: 1,\n\t\t\t\thorizontal: 1,\n\t\t\t});\n\n\t\t\tlet chunks_vertical = Layout::default()\n\t\t\t\t.direction(Direction::Vertical)\n\t\t\t\t.constraints(\n\t\t\t\t\t[Constraint::Min(1), Constraint::Length(5)]\n\t\t\t\t\t\t.as_ref(),\n\t\t\t\t)\n\t\t\t\t.split(area);\n\n\t\t\tlet chunks = Layout::default()\n\t\t\t\t.direction(Direction::Horizontal)\n\t\t\t\t.constraints(\n\t\t\t\t\t[Constraint::Min(40), Constraint::Length(60)]\n\t\t\t\t\t\t.as_ref(),\n\t\t\t\t)\n\t\t\t\t.split(chunks_vertical[0]);\n\n\t\t\tself.draw_list(f, chunks[0])?;\n\t\t\tself.draw_info(f, chunks[1]);\n\t\t\tself.draw_local_info(f, chunks_vertical[1]);\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for SubmodulesListPopup {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.visible || force_all {\n\t\t\tif !force_all {\n\t\t\t\tout.clear();\n\t\t\t}\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::scroll(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\ttrue,\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::close_popup(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\ttrue,\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::open_submodule(&self.key_config),\n\t\t\t\tself.can_open_submodule(),\n\t\t\t\ttrue,\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::update_submodule(&self.key_config),\n\t\t\t\tself.is_valid_selection(),\n\t\t\t\ttrue,\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::open_submodule_parent(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t),\n\t\t\t\tself.submodule_parent.is_some(),\n\t\t\t\ttrue,\n\t\t\t));\n\t\t}\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(&mut self, ev: &Event) -> Result<EventState> {\n\t\tif !self.visible {\n\t\t\treturn Ok(EventState::NotConsumed);\n\t\t}\n\n\t\tif let Event::Key(e) = ev {\n\t\t\tif key_match(e, self.key_config.keys.exit_popup) {\n\t\t\t\tself.hide();\n\t\t\t} else if key_match(e, self.key_config.keys.move_down) {\n\t\t\t\treturn self\n\t\t\t\t\t.move_selection(ScrollType::Up)\n\t\t\t\t\t.map(Into::into);\n\t\t\t} else if key_match(e, self.key_config.keys.move_up) {\n\t\t\t\treturn self\n\t\t\t\t\t.move_selection(ScrollType::Down)\n\t\t\t\t\t.map(Into::into);\n\t\t\t} else if key_match(e, self.key_config.keys.page_down) {\n\t\t\t\treturn self\n\t\t\t\t\t.move_selection(ScrollType::PageDown)\n\t\t\t\t\t.map(Into::into);\n\t\t\t} else if key_match(e, self.key_config.keys.page_up) {\n\t\t\t\treturn self\n\t\t\t\t\t.move_selection(ScrollType::PageUp)\n\t\t\t\t\t.map(Into::into);\n\t\t\t} else if key_match(e, self.key_config.keys.home) {\n\t\t\t\treturn self\n\t\t\t\t\t.move_selection(ScrollType::Home)\n\t\t\t\t\t.map(Into::into);\n\t\t\t} else if key_match(e, self.key_config.keys.end) {\n\t\t\t\treturn self\n\t\t\t\t\t.move_selection(ScrollType::End)\n\t\t\t\t\t.map(Into::into);\n\t\t\t} else if key_match(e, self.key_config.keys.enter) {\n\t\t\t\tif let Some(submodule) = self.selected_entry() {\n\t\t\t\t\tif submodule.status.is_in_wd() {\n\t\t\t\t\t\tself.queue.push(InternalEvent::OpenRepo {\n\t\t\t\t\t\t\tpath: submodule.path.clone(),\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if key_match(\n\t\t\t\te,\n\t\t\t\tself.key_config.keys.update_submodule,\n\t\t\t) {\n\t\t\t\tif let Some(submodule) = self.selected_entry() {\n\t\t\t\t\ttry_or_popup!(\n\t\t\t\t\t\tself,\n\t\t\t\t\t\t\"update submodule:\",\n\t\t\t\t\t\tupdate_submodule(\n\t\t\t\t\t\t\t&self.repo.borrow(),\n\t\t\t\t\t\t\t&submodule.name,\n\t\t\t\t\t\t)\n\t\t\t\t\t);\n\n\t\t\t\t\tself.update_submodules()?;\n\n\t\t\t\t\tself.queue.push(InternalEvent::Update(\n\t\t\t\t\t\tNeedsUpdate::ALL,\n\t\t\t\t\t));\n\t\t\t\t}\n\t\t\t} else if key_match(\n\t\t\t\te,\n\t\t\t\tself.key_config.keys.view_submodule_parent,\n\t\t\t) {\n\t\t\t\tif let Some(parent) = &self.submodule_parent {\n\t\t\t\t\tself.queue.push(InternalEvent::OpenRepo {\n\t\t\t\t\t\tpath: parent.parent_gitpath.clone(),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} else if key_match(\n\t\t\t\te,\n\t\t\t\tself.key_config.keys.cmd_bar_toggle,\n\t\t\t) {\n\t\t\t\t//do not consume if its the more key\n\t\t\t\treturn Ok(EventState::NotConsumed);\n\t\t\t}\n\t\t}\n\n\t\tOk(EventState::Consumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.visible = false;\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\n\t\tOk(())\n\t}\n}\n\nimpl SubmodulesListPopup {\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\tsubmodules: Vec::new(),\n\t\t\tsubmodule_parent: None,\n\t\t\tscroll: VerticalScroll::new(),\n\t\t\tqueue: env.queue.clone(),\n\t\t\tselection: 0,\n\t\t\tvisible: false,\n\t\t\ttheme: env.theme.clone(),\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\tcurrent_height: Cell::new(0),\n\t\t\trepo: env.repo.clone(),\n\t\t\trepo_path: String::new(),\n\t\t}\n\t}\n\n\t///\n\tpub fn open(&mut self) -> Result<()> {\n\t\tself.show()?;\n\t\tself.update_submodules()?;\n\n\t\tOk(())\n\t}\n\n\t///\n\tpub fn update_submodules(&mut self) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tself.submodules = get_submodules(&self.repo.borrow())?;\n\n\t\t\tself.submodule_parent =\n\t\t\t\tsubmodule_parent_info(&self.repo.borrow())?;\n\n\t\t\tself.repo_path = repo_dir(&self.repo.borrow())\n\t\t\t\t.map(|e| e.to_string_lossy().to_string())\n\t\t\t\t.unwrap_or_default();\n\n\t\t\tself.set_selection(self.selection)?;\n\t\t}\n\t\tOk(())\n\t}\n\n\tfn selected_entry(&self) -> Option<&SubmoduleInfo> {\n\t\tself.submodules.get(self.selection as usize)\n\t}\n\n\tfn is_valid_selection(&self) -> bool {\n\t\tself.selected_entry().is_some()\n\t}\n\n\tfn can_open_submodule(&self) -> bool {\n\t\tself.selected_entry().is_some_and(|s| s.status.is_in_wd())\n\t}\n\n\t//TODO: dedup this almost identical with BranchListComponent\n\tfn move_selection(&mut self, scroll: ScrollType) -> Result<bool> {\n\t\tlet new_selection = match scroll {\n\t\t\tScrollType::Up => self.selection.saturating_add(1),\n\t\t\tScrollType::Down => self.selection.saturating_sub(1),\n\t\t\tScrollType::PageDown => self\n\t\t\t\t.selection\n\t\t\t\t.saturating_add(self.current_height.get()),\n\t\t\tScrollType::PageUp => self\n\t\t\t\t.selection\n\t\t\t\t.saturating_sub(self.current_height.get()),\n\t\t\tScrollType::Home => 0,\n\t\t\tScrollType::End => {\n\t\t\t\tlet count: u16 = self.submodules.len().try_into()?;\n\t\t\t\tcount.saturating_sub(1)\n\t\t\t}\n\t\t};\n\n\t\tself.set_selection(new_selection)?;\n\n\t\tOk(true)\n\t}\n\n\tfn set_selection(&mut self, selection: u16) -> Result<()> {\n\t\tlet num_entries: u16 = self.submodules.len().try_into()?;\n\t\tlet num_entries = num_entries.saturating_sub(1);\n\n\t\tlet selection = if selection > num_entries {\n\t\t\tnum_entries\n\t\t} else {\n\t\t\tselection\n\t\t};\n\n\t\tself.selection = selection;\n\n\t\tOk(())\n\t}\n\n\tfn get_text(\n\t\t&self,\n\t\ttheme: &SharedTheme,\n\t\twidth_available: u16,\n\t\theight: usize,\n\t) -> Text<'_> {\n\t\tconst THREE_DOTS: &str = \"...\";\n\t\tconst THREE_DOTS_LENGTH: usize = THREE_DOTS.len(); // \"...\"\n\t\tconst COMMIT_HASH_LENGTH: usize = 8;\n\n\t\tlet mut txt = Vec::with_capacity(3);\n\n\t\tlet name_length: usize = (width_available as usize)\n\t\t\t.saturating_sub(COMMIT_HASH_LENGTH)\n\t\t\t.saturating_sub(THREE_DOTS_LENGTH);\n\n\t\tfor (i, submodule) in self\n\t\t\t.submodules\n\t\t\t.iter()\n\t\t\t.skip(self.scroll.get_top())\n\t\t\t.take(height)\n\t\t\t.enumerate()\n\t\t{\n\t\t\tlet mut module_path = submodule\n\t\t\t\t.path\n\t\t\t\t.as_os_str()\n\t\t\t\t.to_string_lossy()\n\t\t\t\t.to_string();\n\n\t\t\tif module_path.len() > name_length {\n\t\t\t\tmodule_path.unicode_truncate(\n\t\t\t\t\tname_length.saturating_sub(THREE_DOTS_LENGTH),\n\t\t\t\t);\n\t\t\t\tmodule_path += THREE_DOTS;\n\t\t\t}\n\n\t\t\tlet selected = (self.selection as usize\n\t\t\t\t- self.scroll.get_top())\n\t\t\t\t== i;\n\n\t\t\tlet span_hash = Span::styled(\n\t\t\t\tformat!(\n\t\t\t\t\t\"{} \",\n\t\t\t\t\tsubmodule\n\t\t\t\t\t\t.head_id\n\t\t\t\t\t\t.unwrap_or_default()\n\t\t\t\t\t\t.get_short_string()\n\t\t\t\t),\n\t\t\t\ttheme.commit_hash(selected),\n\t\t\t);\n\n\t\t\tlet span_name = Span::styled(\n\t\t\t\tformat!(\"{module_path:name_length$} \"),\n\t\t\t\ttheme.text(true, selected),\n\t\t\t);\n\n\t\t\ttxt.push(Line::from(vec![span_name, span_hash]));\n\t\t}\n\n\t\tText::from(txt)\n\t}\n\n\tfn get_info_text(&self, theme: &SharedTheme) -> Text<'_> {\n\t\tself.selected_entry().map_or_else(\n\t\t\tText::default,\n\t\t\t|submodule| {\n\t\t\t\tlet span_title_path =\n\t\t\t\t\tSpan::styled(\"Path:\", theme.text(false, false));\n\t\t\t\tlet span_path = Span::styled(\n\t\t\t\t\tsubmodule.path.to_string_lossy(),\n\t\t\t\t\ttheme.text(true, false),\n\t\t\t\t);\n\n\t\t\t\tlet span_title_commit =\n\t\t\t\t\tSpan::styled(\"Commit:\", theme.text(false, false));\n\t\t\t\tlet span_commit = Span::styled(\n\t\t\t\t\tsubmodule.id.unwrap_or_default().to_string(),\n\t\t\t\t\ttheme.commit_hash(false),\n\t\t\t\t);\n\n\t\t\t\tlet span_title_url =\n\t\t\t\t\tSpan::styled(\"Url:\", theme.text(false, false));\n\t\t\t\tlet span_url = Span::styled(\n\t\t\t\t\tsubmodule.url.clone().unwrap_or_default(),\n\t\t\t\t\ttheme.text(true, false),\n\t\t\t\t);\n\n\t\t\t\tlet span_title_status =\n\t\t\t\t\tSpan::styled(\"Status:\", theme.text(false, false));\n\t\t\t\tlet span_status = Span::styled(\n\t\t\t\t\tformat!(\"{:?}\", submodule.status),\n\t\t\t\t\ttheme.text(true, false),\n\t\t\t\t);\n\n\t\t\t\tText::from(vec![\n\t\t\t\t\tLine::from(vec![span_title_path]),\n\t\t\t\t\tLine::from(vec![span_path]),\n\t\t\t\t\tLine::from(vec![]),\n\t\t\t\t\tLine::from(vec![span_title_commit]),\n\t\t\t\t\tLine::from(vec![span_commit]),\n\t\t\t\t\tLine::from(vec![]),\n\t\t\t\t\tLine::from(vec![span_title_url]),\n\t\t\t\t\tLine::from(vec![span_url]),\n\t\t\t\t\tLine::from(vec![]),\n\t\t\t\t\tLine::from(vec![span_title_status]),\n\t\t\t\t\tLine::from(vec![span_status]),\n\t\t\t\t])\n\t\t\t},\n\t\t)\n\t}\n\n\tfn get_local_info_text(&self, theme: &SharedTheme) -> Text<'_> {\n\t\tlet mut spans = vec![\n\t\t\tLine::from(vec![Span::styled(\n\t\t\t\t\"Current:\",\n\t\t\t\ttheme.text(false, false),\n\t\t\t)]),\n\t\t\tLine::from(vec![Span::styled(\n\t\t\t\tself.repo_path.clone(),\n\t\t\t\ttheme.text(true, false),\n\t\t\t)]),\n\t\t\tLine::from(vec![Span::styled(\n\t\t\t\t\"Parent:\",\n\t\t\t\ttheme.text(false, false),\n\t\t\t)]),\n\t\t];\n\n\t\tif let Some(parent_info) = &self.submodule_parent {\n\t\t\tspans.push(Line::from(vec![Span::styled(\n\t\t\t\tparent_info.parent_gitpath.to_string_lossy(),\n\t\t\t\ttheme.text(true, false),\n\t\t\t)]));\n\t\t}\n\n\t\tText::from(spans)\n\t}\n\n\tfn draw_list(&self, f: &mut Frame, r: Rect) -> Result<()> {\n\t\tlet height_in_lines = r.height as usize;\n\t\tself.current_height.set(height_in_lines.try_into()?);\n\n\t\tself.scroll.update(\n\t\t\tself.selection as usize,\n\t\t\tself.submodules.len(),\n\t\t\theight_in_lines,\n\t\t);\n\n\t\tf.render_widget(\n\t\t\tParagraph::new(self.get_text(\n\t\t\t\t&self.theme,\n\t\t\t\tr.width.saturating_add(1),\n\t\t\t\theight_in_lines,\n\t\t\t))\n\t\t\t.block(Block::default().borders(Borders::RIGHT))\n\t\t\t.alignment(Alignment::Left),\n\t\t\tr,\n\t\t);\n\n\t\tlet mut r = r;\n\t\tr.height += 2;\n\t\tr.y = r.y.saturating_sub(1);\n\n\t\tself.scroll.draw(f, r, &self.theme);\n\n\t\tOk(())\n\t}\n\n\tfn draw_info(&self, f: &mut Frame, r: Rect) {\n\t\tf.render_widget(\n\t\t\tParagraph::new(self.get_info_text(&self.theme))\n\t\t\t\t.alignment(Alignment::Left),\n\t\t\tr,\n\t\t);\n\t}\n\n\tfn draw_local_info(&self, f: &mut Frame, r: Rect) {\n\t\tf.render_widget(\n\t\t\tParagraph::new(self.get_local_info_text(&self.theme))\n\t\t\t\t.block(Block::default().borders(Borders::TOP))\n\t\t\t\t.alignment(Alignment::Left),\n\t\t\tr,\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "src/popups/tag_commit.rs",
    "content": "use crate::components::{\n\tvisibility_blocking, CommandBlocking, CommandInfo, Component,\n\tDrawableComponent, EventState, InputType, TextInputComponent,\n};\nuse crate::{\n\tapp::Environment,\n\tkeys::{key_match, SharedKeyConfig},\n\tqueue::{InternalEvent, NeedsUpdate, Queue},\n\tstrings, try_or_popup,\n};\nuse anyhow::Result;\nuse asyncgit::sync::{\n\tself, get_config_string, CommitId, RepoPathRef,\n};\nuse crossterm::event::Event;\nuse ratatui::{layout::Rect, Frame};\n\nenum Mode {\n\tName,\n\tAnnotation { tag_name: String },\n}\n\npub struct TagCommitPopup {\n\trepo: RepoPathRef,\n\tmode: Mode,\n\tinput: TextInputComponent,\n\tcommit_id: Option<CommitId>,\n\tqueue: Queue,\n\tkey_config: SharedKeyConfig,\n}\n\nimpl DrawableComponent for TagCommitPopup {\n\tfn draw(&self, f: &mut Frame, rect: Rect) -> Result<()> {\n\t\tself.input.draw(f, rect)?;\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for TagCommitPopup {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.is_visible() || force_all {\n\t\t\tself.input.commands(out, force_all);\n\n\t\t\tlet is_annotation_mode =\n\t\t\t\tmatches!(self.mode, Mode::Annotation { .. });\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::tag_commit_confirm_msg(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t\tis_annotation_mode,\n\t\t\t\t),\n\t\t\t\tself.is_valid_tag(),\n\t\t\t\ttrue,\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::tag_annotate_msg(&self.key_config),\n\t\t\t\tself.is_valid_tag(),\n\t\t\t\tmatches!(self.mode, Mode::Name),\n\t\t\t));\n\t\t}\n\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(&mut self, ev: &Event) -> Result<EventState> {\n\t\tif self.is_visible() {\n\t\t\tif let Event::Key(e) = ev {\n\t\t\t\tlet is_annotation_mode =\n\t\t\t\t\tmatches!(self.mode, Mode::Annotation { .. });\n\n\t\t\t\tif !is_annotation_mode\n\t\t\t\t\t&& key_match(e, self.key_config.keys.enter)\n\t\t\t\t\t&& self.is_valid_tag()\n\t\t\t\t{\n\t\t\t\t\ttry_or_popup!(self, \"tag error:\", self.tag());\n\t\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t\t}\n\t\t\t\tif is_annotation_mode\n\t\t\t\t\t&& key_match(e, self.key_config.keys.commit)\n\t\t\t\t{\n\t\t\t\t\ttry_or_popup!(self, \"tag error:\", self.tag());\n\t\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t\t} else if key_match(\n\t\t\t\t\te,\n\t\t\t\t\tself.key_config.keys.tag_annotate,\n\t\t\t\t) && self.is_valid_tag()\n\t\t\t\t{\n\t\t\t\t\tself.start_annotate_mode();\n\t\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tself.input.event(ev)?;\n\t\t\treturn Ok(EventState::Consumed);\n\t\t}\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.input.is_visible()\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.input.hide();\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.mode = Mode::Name;\n\t\tself.input.set_input_type(InputType::Singleline);\n\t\tself.input.set_title(strings::tag_popup_name_title());\n\t\tself.input.set_default_msg(strings::tag_popup_name_msg());\n\t\tself.input.show()?;\n\n\t\tOk(())\n\t}\n}\n\nimpl TagCommitPopup {\n\t///\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\tqueue: env.queue.clone(),\n\t\t\tinput: TextInputComponent::new(\n\t\t\t\tenv,\n\t\t\t\t&strings::tag_popup_name_title(),\n\t\t\t\t&strings::tag_popup_name_msg(),\n\t\t\t\ttrue,\n\t\t\t)\n\t\t\t.with_input_type(InputType::Singleline),\n\t\t\tcommit_id: None,\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\trepo: env.repo.clone(),\n\t\t\tmode: Mode::Name,\n\t\t}\n\t}\n\n\t///\n\tpub fn open(&mut self, id: CommitId) -> Result<()> {\n\t\tself.commit_id = Some(id);\n\t\tself.show()?;\n\n\t\tOk(())\n\t}\n\n\tfn is_valid_tag(&self) -> bool {\n\t\t!self.input.get_text().is_empty()\n\t}\n\n\tfn tag_info(&self) -> (String, Option<String>) {\n\t\tmatch &self.mode {\n\t\t\tMode::Name => (self.input.get_text().into(), None),\n\t\t\tMode::Annotation { tag_name } => {\n\t\t\t\t(tag_name.clone(), Some(self.input.get_text().into()))\n\t\t\t}\n\t\t}\n\t}\n\n\tpub fn tag(&mut self) -> Result<()> {\n\t\tlet gpgsign =\n\t\t\tget_config_string(&self.repo.borrow(), \"tag.gpgsign\")\n\t\t\t\t.ok()\n\t\t\t\t.flatten()\n\t\t\t\t.and_then(|val| val.parse::<bool>().ok())\n\t\t\t\t.unwrap_or_default();\n\n\t\tanyhow::ensure!(!gpgsign, \"config tag.gpgsign=true detected.\\ngpg signing not supported.\\ndeactivate in your repo/gitconfig to be able to tag without signing.\");\n\n\t\tlet (tag_name, tag_annotation) = self.tag_info();\n\n\t\tif let Some(commit_id) = self.commit_id {\n\t\t\tlet result = sync::tag_commit(\n\t\t\t\t&self.repo.borrow(),\n\t\t\t\t&commit_id,\n\t\t\t\t&tag_name,\n\t\t\t\ttag_annotation.as_deref(),\n\t\t\t);\n\t\t\tmatch result {\n\t\t\t\tOk(_) => {\n\t\t\t\t\tself.input.clear();\n\t\t\t\t\tself.hide();\n\n\t\t\t\t\tself.queue.push(InternalEvent::Update(\n\t\t\t\t\t\tNeedsUpdate::ALL,\n\t\t\t\t\t));\n\t\t\t\t}\n\t\t\t\tErr(e) => {\n\t\t\t\t\t// go back to tag name if something goes wrong\n\t\t\t\t\tself.input.set_text(tag_name);\n\t\t\t\t\tself.hide();\n\n\t\t\t\t\tlog::error!(\"e: {e}\");\n\t\t\t\t\tself.queue.push(InternalEvent::ShowErrorMsg(\n\t\t\t\t\t\tformat!(\"tag error:\\n{e}\"),\n\t\t\t\t\t));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn start_annotate_mode(&mut self) {\n\t\tlet tag_name: String = self.input.get_text().into();\n\n\t\tself.input.clear();\n\t\tself.input.set_input_type(InputType::Multiline);\n\t\tself.input.set_title(strings::tag_popup_annotation_title(\n\t\t\t&tag_name,\n\t\t));\n\t\tself.input\n\t\t\t.set_default_msg(strings::tag_popup_annotation_msg());\n\t\tself.mode = Mode::Annotation { tag_name };\n\t}\n}\n"
  },
  {
    "path": "src/popups/taglist.rs",
    "content": "use crate::components::{\n\ttime_to_string, visibility_blocking, CommandBlocking,\n\tCommandInfo, Component, DrawableComponent, EventState,\n};\nuse crate::{\n\tapp::Environment,\n\tcomponents::ScrollType,\n\tkeys::{key_match, SharedKeyConfig},\n\tqueue::{Action, InternalEvent, Queue},\n\tstrings,\n\tui::{self, Size},\n\tAsyncNotification,\n};\nuse anyhow::Result;\nuse asyncgit::{\n\tasyncjob::AsyncSingleJob,\n\tremote_tags::AsyncRemoteTagsJob,\n\tsync::cred::{\n\t\textract_username_password, need_username_password,\n\t\tBasicAuthCredential,\n\t},\n\tsync::{\n\t\tself, get_tags_with_metadata, RepoPathRef, TagWithMetadata,\n\t},\n\tAsyncGitNotification,\n};\n\nuse crossterm::event::Event;\nuse ratatui::{\n\tlayout::{Constraint, Margin, Rect},\n\ttext::Span,\n\twidgets::{\n\t\tBlock, BorderType, Borders, Cell, Clear, Row, Table,\n\t\tTableState,\n\t},\n\tFrame,\n};\nuse ui::style::SharedTheme;\n\n///\npub struct TagListPopup {\n\trepo: RepoPathRef,\n\ttheme: SharedTheme,\n\tqueue: Queue,\n\ttags: Option<Vec<TagWithMetadata>>,\n\tvisible: bool,\n\ttable_state: std::cell::Cell<TableState>,\n\tcurrent_height: std::cell::Cell<usize>,\n\tmissing_remote_tags: Option<Vec<String>>,\n\thas_remotes: bool,\n\tbasic_credential: Option<BasicAuthCredential>,\n\tasync_remote_tags: AsyncSingleJob<AsyncRemoteTagsJob>,\n\tkey_config: SharedKeyConfig,\n}\n\nimpl DrawableComponent for TagListPopup {\n\tfn draw(&self, f: &mut Frame, rect: Rect) -> Result<()> {\n\t\tif self.visible {\n\t\t\tconst PERCENT_SIZE: Size = Size::new(80, 50);\n\t\t\tconst MIN_SIZE: Size = Size::new(60, 20);\n\n\t\t\tlet area = ui::centered_rect(\n\t\t\t\tPERCENT_SIZE.width,\n\t\t\t\tPERCENT_SIZE.height,\n\t\t\t\tf.area(),\n\t\t\t);\n\t\t\tlet area =\n\t\t\t\tui::rect_inside(MIN_SIZE, f.area().into(), area);\n\t\t\tlet area = area.intersection(rect);\n\n\t\t\tlet tag_name_width =\n\t\t\t\tself.tags.as_ref().map_or(0, |tags| {\n\t\t\t\t\ttags.iter()\n\t\t\t\t\t\t.fold(0, |acc, tag| acc.max(tag.name.len()))\n\t\t\t\t});\n\n\t\t\tlet constraints = [\n\t\t\t\t// symbol if tag is not yet on remote and can be pushed\n\t\t\t\tConstraint::Length(1),\n\t\t\t\t// tag name\n\t\t\t\tConstraint::Length(tag_name_width.try_into()?),\n\t\t\t\t// commit date\n\t\t\t\tConstraint::Length(10),\n\t\t\t\t// author width\n\t\t\t\tConstraint::Length(19),\n\t\t\t\t// attachment\n\t\t\t\tConstraint::Length(1),\n\t\t\t\t// commit id\n\t\t\t\tConstraint::Percentage(100),\n\t\t\t];\n\n\t\t\tlet rows = self.get_rows();\n\t\t\tlet number_of_rows = rows.len();\n\n\t\t\tlet table = Table::new(rows, constraints)\n\t\t\t\t.column_spacing(1)\n\t\t\t\t.row_highlight_style(self.theme.text(true, true))\n\t\t\t\t.block(\n\t\t\t\t\tBlock::default()\n\t\t\t\t\t\t.borders(Borders::ALL)\n\t\t\t\t\t\t.title(Span::styled(\n\t\t\t\t\t\t\tstrings::title_tags(),\n\t\t\t\t\t\t\tself.theme.title(true),\n\t\t\t\t\t\t))\n\t\t\t\t\t\t.border_style(self.theme.block(true))\n\t\t\t\t\t\t.border_type(BorderType::Thick),\n\t\t\t\t);\n\n\t\t\tlet mut table_state = self.table_state.take();\n\n\t\t\tf.render_widget(Clear, area);\n\t\t\tf.render_stateful_widget(table, area, &mut table_state);\n\n\t\t\tlet area = area.inner(Margin {\n\t\t\t\tvertical: 1,\n\t\t\t\thorizontal: 0,\n\t\t\t});\n\n\t\t\tui::draw_scrollbar(\n\t\t\t\tf,\n\t\t\t\tarea,\n\t\t\t\t&self.theme,\n\t\t\t\tnumber_of_rows,\n\t\t\t\ttable_state.selected().unwrap_or(0),\n\t\t\t\tui::Orientation::Vertical,\n\t\t\t);\n\n\t\t\tself.table_state.set(table_state);\n\t\t\tself.current_height.set(area.height.into());\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for TagListPopup {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.visible || force_all {\n\t\t\tif !force_all {\n\t\t\t\tout.clear();\n\t\t\t}\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::scroll(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\ttrue,\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::close_popup(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\ttrue,\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::delete_tag_popup(&self.key_config),\n\t\t\t\tself.valid_selection(),\n\t\t\t\ttrue,\n\t\t\t));\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::select_tag(&self.key_config),\n\t\t\t\tself.valid_selection(),\n\t\t\t\ttrue,\n\t\t\t));\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::push_tags(&self.key_config),\n\t\t\t\tself.has_remotes,\n\t\t\t\ttrue,\n\t\t\t));\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::show_tag_annotation(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t),\n\t\t\t\tself.can_show_annotation(),\n\t\t\t\ttrue,\n\t\t\t));\n\t\t}\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(&mut self, event: &Event) -> Result<EventState> {\n\t\tif self.visible {\n\t\t\tif let Event::Key(key) = event {\n\t\t\t\tif key_match(key, self.key_config.keys.exit_popup) {\n\t\t\t\t\tself.hide();\n\t\t\t\t} else if key_match(key, self.key_config.keys.move_up)\n\t\t\t\t{\n\t\t\t\t\tself.move_selection(ScrollType::Up);\n\t\t\t\t} else if key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.move_down,\n\t\t\t\t) {\n\t\t\t\t\tself.move_selection(ScrollType::Down);\n\t\t\t\t} else if key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.shift_up,\n\t\t\t\t) || key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.home,\n\t\t\t\t) {\n\t\t\t\t\tself.move_selection(ScrollType::Home);\n\t\t\t\t} else if key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.shift_down,\n\t\t\t\t) || key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.end,\n\t\t\t\t) {\n\t\t\t\t\tself.move_selection(ScrollType::End);\n\t\t\t\t} else if key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.page_down,\n\t\t\t\t) {\n\t\t\t\t\tself.move_selection(ScrollType::PageDown);\n\t\t\t\t} else if key_match(key, self.key_config.keys.page_up)\n\t\t\t\t{\n\t\t\t\t\tself.move_selection(ScrollType::PageUp);\n\t\t\t\t} else if key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.move_right,\n\t\t\t\t) && self.can_show_annotation()\n\t\t\t\t{\n\t\t\t\t\tself.show_annotation();\n\t\t\t\t} else if key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.delete_tag,\n\t\t\t\t) {\n\t\t\t\t\treturn self.selected_tag().map_or(\n\t\t\t\t\t\tOk(EventState::NotConsumed),\n\t\t\t\t\t\t|tag| {\n\t\t\t\t\t\t\tself.queue.push(\n\t\t\t\t\t\t\t\tInternalEvent::ConfirmAction(\n\t\t\t\t\t\t\t\t\tAction::DeleteTag(\n\t\t\t\t\t\t\t\t\t\ttag.name.clone(),\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t\t\t},\n\t\t\t\t\t);\n\t\t\t\t} else if key_match(\n\t\t\t\t\tkey,\n\t\t\t\t\tself.key_config.keys.select_tag,\n\t\t\t\t) {\n\t\t\t\t\treturn self.selected_tag().map_or(\n\t\t\t\t\t\tOk(EventState::NotConsumed),\n\t\t\t\t\t\t|tag| {\n\t\t\t\t\t\t\tself.queue.push(\n\t\t\t\t\t\t\t\tInternalEvent::SelectCommitInRevlog(\n\t\t\t\t\t\t\t\t\ttag.commit_id,\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t\t\t},\n\t\t\t\t\t);\n\t\t\t\t} else if key_match(key, self.key_config.keys.push)\n\t\t\t\t\t&& self.has_remotes\n\t\t\t\t{\n\t\t\t\t\tself.queue.push(InternalEvent::PushTags);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tOk(EventState::Consumed)\n\t\t} else {\n\t\t\tOk(EventState::NotConsumed)\n\t\t}\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.visible = false;\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\n\t\tOk(())\n\t}\n}\n\nimpl TagListPopup {\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\ttheme: env.theme.clone(),\n\t\t\tqueue: env.queue.clone(),\n\t\t\ttags: None,\n\t\t\tvisible: false,\n\t\t\thas_remotes: false,\n\t\t\ttable_state: std::cell::Cell::new(TableState::default()),\n\t\t\tcurrent_height: std::cell::Cell::new(0),\n\t\t\tbasic_credential: None,\n\t\t\tmissing_remote_tags: None,\n\t\t\tasync_remote_tags: AsyncSingleJob::new(\n\t\t\t\tenv.sender_git.clone(),\n\t\t\t),\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\trepo: env.repo.clone(),\n\t\t}\n\t}\n\n\t///\n\tpub fn open(&mut self) -> Result<()> {\n\t\tself.table_state.get_mut().select(Some(0));\n\t\tself.show()?;\n\n\t\tself.has_remotes =\n\t\t\tsync::get_branches_info(&self.repo.borrow(), false)\n\t\t\t\t.is_ok_and(|branches| !branches.is_empty());\n\n\t\tlet basic_credential = if self.has_remotes {\n\t\t\tif need_username_password(&self.repo.borrow())? {\n\t\t\t\tlet credential =\n\t\t\t\t\textract_username_password(&self.repo.borrow())?;\n\n\t\t\t\tif credential.is_complete() {\n\t\t\t\t\tSome(credential)\n\t\t\t\t} else {\n\t\t\t\t\tNone\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tNone\n\t\t\t}\n\t\t} else {\n\t\t\tNone\n\t\t};\n\n\t\tself.basic_credential = basic_credential;\n\n\t\tself.update_tags()?;\n\t\tself.update_missing_remote_tags();\n\n\t\tOk(())\n\t}\n\n\t///\n\tpub fn update(&mut self, ev: AsyncNotification) {\n\t\tif matches!(\n\t\t\tev,\n\t\t\tAsyncNotification::Git(AsyncGitNotification::RemoteTags)\n\t\t) {\n\t\t\tif let Some(job) = self.async_remote_tags.take_last() {\n\t\t\t\tif let Some(Ok(missing_remote_tags)) = job.result() {\n\t\t\t\t\tself.missing_remote_tags =\n\t\t\t\t\t\tSome(missing_remote_tags);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if matches!(\n\t\t\tev,\n\t\t\tAsyncNotification::Git(AsyncGitNotification::PushTags)\n\t\t) {\n\t\t\tself.update_missing_remote_tags();\n\t\t}\n\t}\n\n\t///\n\tpub fn any_work_pending(&self) -> bool {\n\t\tself.async_remote_tags.is_pending()\n\t}\n\n\t/// fetch list of tags\n\tpub fn update_tags(&mut self) -> Result<()> {\n\t\tlet tags = get_tags_with_metadata(&self.repo.borrow())?;\n\n\t\tself.tags = Some(tags);\n\n\t\tOk(())\n\t}\n\n\tpub fn update_missing_remote_tags(&self) {\n\t\tif self.has_remotes {\n\t\t\tself.async_remote_tags.spawn(AsyncRemoteTagsJob::new(\n\t\t\t\tself.repo.borrow().clone(),\n\t\t\t\tself.basic_credential.clone(),\n\t\t\t));\n\t\t}\n\t}\n\n\t///\n\tfn move_selection(&self, scroll_type: ScrollType) -> bool {\n\t\tlet mut table_state = self.table_state.take();\n\n\t\tlet old_selection = table_state.selected().unwrap_or(0);\n\t\tlet max_selection =\n\t\t\tself.tags.as_ref().map_or(0, |tags| tags.len() - 1);\n\n\t\tlet new_selection = match scroll_type {\n\t\t\tScrollType::Up => old_selection.saturating_sub(1),\n\t\t\tScrollType::Down => {\n\t\t\t\told_selection.saturating_add(1).min(max_selection)\n\t\t\t}\n\t\t\tScrollType::Home => 0,\n\t\t\tScrollType::End => max_selection,\n\t\t\tScrollType::PageUp => old_selection.saturating_sub(\n\t\t\t\tself.current_height.get().saturating_sub(1),\n\t\t\t),\n\t\t\tScrollType::PageDown => old_selection\n\t\t\t\t.saturating_add(\n\t\t\t\t\tself.current_height.get().saturating_sub(1),\n\t\t\t\t)\n\t\t\t\t.min(max_selection),\n\t\t};\n\n\t\tlet needs_update = new_selection != old_selection;\n\n\t\ttable_state.select(Some(new_selection));\n\t\tself.table_state.set(table_state);\n\n\t\tneeds_update\n\t}\n\n\tfn show_annotation(&self) {\n\t\tif let Some(tag) = self.selected_tag() {\n\t\t\tif let Some(annotation) = &tag.annotation {\n\t\t\t\tself.queue.push(InternalEvent::ShowInfoMsg(\n\t\t\t\t\tannotation.clone(),\n\t\t\t\t));\n\t\t\t}\n\t\t}\n\t}\n\n\tfn can_show_annotation(&self) -> bool {\n\t\tself.selected_tag()\n\t\t\t.and_then(|t| t.annotation.as_ref())\n\t\t\t.is_some()\n\t}\n\n\t///\n\tfn get_rows(&self) -> Vec<Row<'_>> {\n\t\tself.tags.as_ref().map_or_else(Vec::new, |tags| {\n\t\t\ttags.iter().map(|tag| self.get_row(tag)).collect()\n\t\t})\n\t}\n\n\t///\n\tfn get_row(&self, tag: &TagWithMetadata) -> Row<'_> {\n\t\tconst UPSTREAM_SYMBOL: &str = \"\\u{2191}\";\n\t\tconst ATTACHMENT_SYMBOL: &str = \"@\";\n\t\tconst EMPTY_SYMBOL: &str = \" \";\n\n\t\tlet is_tag_missing_on_remote = self\n\t\t\t.missing_remote_tags\n\t\t\t.as_ref()\n\t\t\t.is_some_and(|missing_remote_tags| {\n\t\t\t\tlet remote_tag = format!(\"refs/tags/{}\", tag.name);\n\n\t\t\t\tmissing_remote_tags.contains(&remote_tag)\n\t\t\t});\n\n\t\tlet has_remote_str = if is_tag_missing_on_remote {\n\t\t\tUPSTREAM_SYMBOL\n\t\t} else {\n\t\t\tEMPTY_SYMBOL\n\t\t};\n\n\t\tlet has_attachment_str = if tag.annotation.is_some() {\n\t\t\tATTACHMENT_SYMBOL\n\t\t} else {\n\t\t\tEMPTY_SYMBOL\n\t\t};\n\n\t\tlet cells: Vec<Cell> = vec![\n\t\t\tCell::from(has_remote_str)\n\t\t\t\t.style(self.theme.commit_author(false)),\n\t\t\tCell::from(tag.name.clone())\n\t\t\t\t.style(self.theme.text(true, false)),\n\t\t\tCell::from(time_to_string(tag.time, true))\n\t\t\t\t.style(self.theme.commit_time(false)),\n\t\t\tCell::from(tag.author.clone())\n\t\t\t\t.style(self.theme.commit_author(false)),\n\t\t\tCell::from(has_attachment_str)\n\t\t\t\t.style(self.theme.text_danger()),\n\t\t\tCell::from(tag.message.clone())\n\t\t\t\t.style(self.theme.text(true, false)),\n\t\t];\n\n\t\tRow::new(cells)\n\t}\n\n\tfn valid_selection(&self) -> bool {\n\t\tself.selected_tag().is_some()\n\t}\n\n\tfn selected_tag(&self) -> Option<&TagWithMetadata> {\n\t\tself.tags.as_ref().and_then(|tags| {\n\t\t\tlet table_state = self.table_state.take();\n\n\t\t\tlet tag = table_state\n\t\t\t\t.selected()\n\t\t\t\t.and_then(|selected| tags.get(selected));\n\n\t\t\tself.table_state.set(table_state);\n\n\t\t\ttag\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "src/popups/update_remote_url.rs",
    "content": "use anyhow::Result;\nuse asyncgit::sync::{self, RepoPathRef};\nuse crossterm::event::Event;\n\nuse crate::{\n\tapp::Environment,\n\tcomponents::{\n\t\tvisibility_blocking, CommandBlocking, CommandInfo, Component,\n\t\tDrawableComponent, EventState, InputType, TextInputComponent,\n\t},\n\tkeys::{key_match, SharedKeyConfig},\n\tqueue::{InternalEvent, NeedsUpdate, Queue},\n\tstrings,\n};\n\npub struct UpdateRemoteUrlPopup {\n\trepo: RepoPathRef,\n\tinput: TextInputComponent,\n\tkey_config: SharedKeyConfig,\n\tqueue: Queue,\n\tremote_name: Option<String>,\n\tinitial_url: Option<String>,\n}\n\nimpl DrawableComponent for UpdateRemoteUrlPopup {\n\tfn draw(\n\t\t&self,\n\t\tf: &mut ratatui::Frame,\n\t\trect: ratatui::prelude::Rect,\n\t) -> anyhow::Result<()> {\n\t\tif self.is_visible() {\n\t\t\tself.input.draw(f, rect)?;\n\t\t}\n\t\tOk(())\n\t}\n}\n\nimpl Component for UpdateRemoteUrlPopup {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<crate::components::CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.is_visible() || force_all {\n\t\t\tself.input.commands(out, force_all);\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::remote_confirm_url_msg(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t),\n\t\t\t\ttrue,\n\t\t\t\ttrue,\n\t\t\t));\n\t\t}\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(&mut self, ev: &Event) -> Result<EventState> {\n\t\tif self.is_visible() {\n\t\t\tif self.input.event(ev)?.is_consumed() {\n\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t}\n\n\t\t\tif let Event::Key(e) = ev {\n\t\t\t\tif key_match(e, self.key_config.keys.enter) {\n\t\t\t\t\tself.update_remote_url();\n\t\t\t\t}\n\n\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t}\n\t\t}\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.input.is_visible()\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.input.hide();\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.input.show()?;\n\n\t\tOk(())\n\t}\n}\n\nimpl UpdateRemoteUrlPopup {\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\trepo: env.repo.clone(),\n\t\t\tinput: TextInputComponent::new(\n\t\t\t\tenv,\n\t\t\t\t&strings::update_remote_url_popup_title(\n\t\t\t\t\t&env.key_config,\n\t\t\t\t),\n\t\t\t\t&strings::update_remote_url_popup_msg(\n\t\t\t\t\t&env.key_config,\n\t\t\t\t),\n\t\t\t\ttrue,\n\t\t\t)\n\t\t\t.with_input_type(InputType::Singleline),\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\tqueue: env.queue.clone(),\n\t\t\tinitial_url: None,\n\t\t\tremote_name: None,\n\t\t}\n\t}\n\n\t///\n\tpub fn open(\n\t\t&mut self,\n\t\tremote_name: String,\n\t\tcur_url: String,\n\t) -> Result<()> {\n\t\tself.input.set_text(cur_url.clone());\n\t\tself.remote_name = Some(remote_name);\n\t\tself.initial_url = Some(cur_url);\n\t\tself.show()?;\n\n\t\tOk(())\n\t}\n\n\t///\n\tpub fn update_remote_url(&mut self) {\n\t\tif let Some(remote_name) = &self.remote_name {\n\t\t\tlet res = sync::update_remote_url(\n\t\t\t\t&self.repo.borrow(),\n\t\t\t\tremote_name,\n\t\t\t\tself.input.get_text(),\n\t\t\t);\n\t\t\tmatch res {\n\t\t\t\tOk(()) => {\n\t\t\t\t\tself.queue.push(InternalEvent::Update(\n\t\t\t\t\t\tNeedsUpdate::ALL | NeedsUpdate::REMOTES,\n\t\t\t\t\t));\n\t\t\t\t}\n\t\t\t\tErr(e) => {\n\t\t\t\t\tlog::error!(\"update remote url: {e}\");\n\t\t\t\t\tself.queue.push(InternalEvent::ShowErrorMsg(\n\t\t\t\t\t\tformat!(\"update remote url error:\\n{e}\"),\n\t\t\t\t\t));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tself.input.clear();\n\t\tself.initial_url = None;\n\t\tself.hide();\n\t}\n}\n"
  },
  {
    "path": "src/queue.rs",
    "content": "use crate::{\n\tcomponents::FuzzyFinderTarget,\n\tpopups::{\n\t\tAppOption, BlameFileOpen, FileRevOpen, FileTreeOpen,\n\t\tInspectCommitOpen,\n\t},\n\ttabs::StashingOptions,\n};\nuse asyncgit::{\n\tsync::{\n\t\tdiff::DiffLinePosition, BranchInfo, CommitId,\n\t\tLogFilterSearchOptions,\n\t},\n\tPushType,\n};\nuse bitflags::bitflags;\nuse std::{\n\tcell::RefCell, collections::VecDeque, path::PathBuf, rc::Rc,\n};\n\nbitflags! {\n\t/// flags defining what part of the app need to update\n\tpub struct NeedsUpdate: u32 {\n\t\t/// app::update\n\t\tconst ALL = 0b001;\n\t\t/// diff may have changed (app::update_diff)\n\t\tconst DIFF = 0b010;\n\t\t/// commands might need updating (app::update_commands)\n\t\tconst COMMANDS = 0b100;\n\t\t/// branches have changed\n\t\tconst BRANCHES = 0b1000;\n\t\t/// Remotes have changed\n\t\tconst REMOTES = 0b1001;\n\t}\n}\n\n/// data of item that is supposed to be reset\npub struct ResetItem {\n\t/// path to the item (folder/file)\n\tpub path: String,\n}\n\n///\npub enum Action {\n\tReset(ResetItem),\n\tResetHunk(String, u64),\n\tResetLines(String, Vec<DiffLinePosition>),\n\tStashDrop(Vec<CommitId>),\n\tStashPop(CommitId),\n\tDeleteLocalBranch(String),\n\tDeleteRemoteBranch(String),\n\tDeleteTag(String),\n\tDeleteRemoteTag(String, String),\n\tDeleteRemote(String),\n\tForcePush(String, bool),\n\tPullMerge { incoming: usize, rebase: bool },\n\tAbortMerge,\n\tAbortRebase,\n\tAbortRevert,\n\tUndoCommit,\n}\n\n#[derive(Debug)]\npub enum StackablePopupOpen {\n\t///\n\tBlameFile(BlameFileOpen),\n\t///\n\tFileRevlog(FileRevOpen),\n\t///\n\tFileTree(FileTreeOpen),\n\t///\n\tInspectCommit(InspectCommitOpen),\n\t///\n\tCompareCommits(InspectCommitOpen),\n}\n\npub enum AppTabs {\n\tStatus,\n\tLog,\n\tFiles,\n\tStashing,\n\tStashlist,\n}\n\n///\npub enum InternalEvent {\n\t///\n\tConfirmAction(Action),\n\t///\n\tConfirmedAction(Action),\n\t///\n\tShowErrorMsg(String),\n\t///\n\tShowInfoMsg(String),\n\t///\n\tUpdate(NeedsUpdate),\n\t///\n\tStatusLastFileMoved,\n\t/// open commit msg input\n\tOpenCommit,\n\t///\n\tPopupStashing(StashingOptions),\n\t///\n\tTabSwitchStatus,\n\t///\n\tTabSwitch(AppTabs),\n\t///\n\tSelectCommitInRevlog(CommitId),\n\t///\n\tTagCommit(CommitId),\n\t///\n\tTags,\n\t///\n\tCreateBranch,\n\t///\n\tRenameRemote(String),\n\t///\n\tUpdateRemoteUrl(String, String),\n\t///\n\tRenameBranch(String, String),\n\t///\n\tSelectBranch,\n\t///\n\tOpenExternalEditor(Option<String>),\n\t///\n\tPush(String, PushType, bool, bool),\n\t///\n\tPull(String),\n\t///\n\tPushTags,\n\t///\n\tOptionSwitched(AppOption),\n\t///\n\tOpenFuzzyFinder(Vec<String>, FuzzyFinderTarget),\n\t///\n\tOpenLogSearchPopup,\n\t///\n\tFuzzyFinderChanged(usize, String, FuzzyFinderTarget),\n\t///\n\tFetchRemotes,\n\t///\n\tOpenPopup(StackablePopupOpen),\n\t///\n\tPopupStackPop,\n\t///\n\tPopupStackPush(StackablePopupOpen),\n\t///\n\tViewSubmodules,\n\t///\n\tViewRemotes,\n\t///\n\tCreateRemote,\n\t///\n\tOpenRepo { path: PathBuf },\n\t///\n\tOpenResetPopup(CommitId),\n\t///\n\tRewordCommit(CommitId),\n\t///\n\tCommitSearch(LogFilterSearchOptions),\n\t///\n\tOpenGotoLinePopup(usize),\n\t///\n\tGotoLine(usize),\n\t///\n\tCheckoutOption(BranchInfo),\n}\n\n/// single threaded simple queue for components to communicate with each other\n#[derive(Clone, Default)]\npub struct Queue {\n\tdata: Rc<RefCell<VecDeque<InternalEvent>>>,\n}\n\nimpl Queue {\n\tpub fn new() -> Self {\n\t\tSelf {\n\t\t\tdata: Rc::new(RefCell::new(VecDeque::new())),\n\t\t}\n\t}\n\n\tpub fn push(&self, ev: InternalEvent) {\n\t\tself.data.borrow_mut().push_back(ev);\n\t}\n\n\tpub fn pop(&self) -> Option<InternalEvent> {\n\t\tself.data.borrow_mut().pop_front()\n\t}\n\n\tpub fn clear(&self) {\n\t\tself.data.borrow_mut().clear();\n\t}\n}\n"
  },
  {
    "path": "src/spinner.rs",
    "content": "use ratatui::{\n\tbackend::{Backend, CrosstermBackend},\n\tTerminal,\n};\nuse std::{cell::Cell, char, io};\n\n// static SPINNER_CHARS: &[char] = &['◢', '◣', '◤', '◥'];\n// static SPINNER_CHARS: &[char] = &['⢹', '⢺', '⢼', '⣸', '⣇', '⡧', '⡗', '⡏'];\nstatic SPINNER_CHARS: &[char] =\n\t&['⣷', '⣯', '⣟', '⡿', '⢿', '⣻', '⣽', '⣾'];\n\n///\npub struct Spinner {\n\tidx: usize,\n\tactive: bool,\n\tlast_char: Cell<char>,\n}\n\nimpl Default for Spinner {\n\tfn default() -> Self {\n\t\tSelf {\n\t\t\tidx: 0,\n\t\t\tactive: false,\n\t\t\tlast_char: Cell::new(' '),\n\t\t}\n\t}\n}\n\nimpl Spinner {\n\t/// increment spinner graphic by one\n\tpub fn update(&mut self) {\n\t\tself.idx += 1;\n\t\tself.idx %= SPINNER_CHARS.len();\n\t}\n\n\t///\n\tpub const fn set_state(&mut self, active: bool) {\n\t\tself.active = active;\n\t}\n\n\t/// draws or removes spinner char depending on `pending` state\n\tpub fn draw(\n\t\t&self,\n\t\tterminal: &mut Terminal<CrosstermBackend<io::Stdout>>,\n\t) -> io::Result<()> {\n\t\tlet idx = self.idx;\n\n\t\tlet char_to_draw =\n\t\t\tif self.active { SPINNER_CHARS[idx] } else { ' ' };\n\n\t\tif self.last_char.get() != char_to_draw {\n\t\t\tself.last_char.set(char_to_draw);\n\n\t\t\tlet c = ratatui::buffer::Cell::default()\n\t\t\t\t.set_char(char_to_draw)\n\t\t\t\t.clone();\n\n\t\t\tterminal\n\t\t\t\t.backend_mut()\n\t\t\t\t.draw(vec![(0_u16, 0_u16, &c)].into_iter())?;\n\n\t\t\tBackend::flush(terminal.backend_mut())?;\n\t\t}\n\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "src/string_utils.rs",
    "content": "use unicode_segmentation::UnicodeSegmentation;\nuse unicode_width::UnicodeWidthStr;\n\n///\npub fn trim_length_left(s: &str, width: usize) -> &str {\n\tlet len = s.len();\n\tif len > width {\n\t\tfor i in len - width..len {\n\t\t\tif s.is_char_boundary(i) {\n\t\t\t\treturn &s[i..];\n\t\t\t}\n\t\t}\n\t}\n\n\ts\n}\n\n//TODO: allow customize tabsize\npub fn tabs_to_spaces(input: String) -> String {\n\tif input.contains('\\t') {\n\t\tinput.replace('\\t', \"  \")\n\t} else {\n\t\tinput\n\t}\n}\n\n/// This function will return a str slice which start at specified offset.\n/// As src is a unicode str, start offset has to be calculated with each character.\npub fn trim_offset(src: &str, mut offset: usize) -> &str {\n\tlet mut start = 0;\n\tfor c in UnicodeSegmentation::graphemes(src, true) {\n\t\tlet w = c.width();\n\t\tif w <= offset {\n\t\t\toffset -= w;\n\t\t\tstart += c.len();\n\t\t} else {\n\t\t\tbreak;\n\t\t}\n\t}\n\t&src[start..]\n}\n\n#[cfg(test)]\nmod test {\n\tuse pretty_assertions::assert_eq;\n\n\tuse crate::string_utils::trim_length_left;\n\n\t#[test]\n\tfn test_trim() {\n\t\tassert_eq!(trim_length_left(\"👍foo\", 3), \"foo\");\n\t\tassert_eq!(trim_length_left(\"👍foo\", 4), \"foo\");\n\t}\n}\n"
  },
  {
    "path": "src/strings.rs",
    "content": "use std::borrow::Cow;\n\nuse asyncgit::sync::CommitId;\nuse unicode_truncate::UnicodeTruncateStr;\nuse unicode_width::UnicodeWidthStr;\n\nuse crate::keys::SharedKeyConfig;\n\npub mod order {\n\tpub const RARE_ACTION: i8 = 30;\n\tpub const NAV: i8 = 20;\n\tpub const AVERAGE: i8 = 10;\n\tpub const PRIORITY: i8 = 1;\n}\n\npub static PUSH_POPUP_MSG: &str = \"Push\";\npub static FORCE_PUSH_POPUP_MSG: &str = \"Force Push\";\npub static PULL_POPUP_MSG: &str = \"Pull\";\npub static FETCH_POPUP_MSG: &str = \"Fetch\";\npub static PUSH_POPUP_PROGRESS_NONE: &str = \"preparing...\";\npub static PUSH_POPUP_STATES_ADDING: &str = \"adding objects (1/3)\";\npub static PUSH_POPUP_STATES_DELTAS: &str = \"deltas (2/3)\";\npub static PUSH_POPUP_STATES_PUSHING: &str = \"pushing (3/3)\";\npub static PUSH_POPUP_STATES_TRANSFER: &str = \"transfer\";\npub static PUSH_POPUP_STATES_DONE: &str = \"done\";\n\npub static PUSH_TAGS_POPUP_MSG: &str = \"Push Tags\";\npub static PUSH_TAGS_STATES_FETCHING: &str = \"fetching\";\npub static PUSH_TAGS_STATES_PUSHING: &str = \"pushing\";\npub static PUSH_TAGS_STATES_DONE: &str = \"done\";\n\npub static POPUP_TITLE_SUBMODULES: &str = \"Submodules\";\npub static POPUP_TITLE_REMOTES: &str = \"Remotes\";\npub static POPUP_SUBTITLE_REMOTES: &str = \"Details\";\npub static POPUP_TITLE_FUZZY_FIND: &str = \"Fuzzy Finder\";\npub static POPUP_TITLE_LOG_SEARCH: &str = \"Search\";\n\npub static POPUP_FAIL_COPY: &str = \"Failed to copy text\";\npub static POPUP_SUCCESS_COPY: &str = \"Copied Text\";\npub static POPUP_COMMIT_SHA_INVALID: &str = \"Invalid commit sha\";\n\npub mod symbol {\n\tpub const CHECKMARK: &str = \"\\u{2713}\"; //✓\n\tpub const SPACE: &str = \"\\u{02FD}\"; //˽\n\tpub const EMPTY_SPACE: &str = \" \";\n\tpub const FOLDER_ICON_COLLAPSED: &str = \"\\u{25b8}\"; //▸\n\tpub const FOLDER_ICON_EXPANDED: &str = \"\\u{25be}\"; //▾\n\tpub const EMPTY_STR: &str = \"\";\n\tpub const ELLIPSIS: char = '\\u{2026}'; // …\n}\n\npub fn title_branches() -> String {\n\t\"Branches\".to_string()\n}\npub fn title_tags() -> String {\n\t\"Tags\".to_string()\n}\npub fn title_status(_key_config: &SharedKeyConfig) -> String {\n\t\"Unstaged Changes\".to_string()\n}\npub fn title_diff(_key_config: &SharedKeyConfig) -> String {\n\t\"Diff: \".to_string()\n}\npub fn title_index(_key_config: &SharedKeyConfig) -> String {\n\t\"Staged Changes\".to_string()\n}\npub fn tab_status(key_config: &SharedKeyConfig) -> String {\n\tformat!(\n\t\t\"Status [{}]\",\n\t\tkey_config.get_hint(key_config.keys.tab_status)\n\t)\n}\npub fn tab_log(key_config: &SharedKeyConfig) -> String {\n\tformat!(\"Log [{}]\", key_config.get_hint(key_config.keys.tab_log))\n}\npub fn tab_files(key_config: &SharedKeyConfig) -> String {\n\tformat!(\n\t\t\"Files [{}]\",\n\t\tkey_config.get_hint(key_config.keys.tab_files)\n\t)\n}\npub fn tab_stashing(key_config: &SharedKeyConfig) -> String {\n\tformat!(\n\t\t\"Stashing [{}]\",\n\t\tkey_config.get_hint(key_config.keys.tab_stashing)\n\t)\n}\npub fn tab_stashes(key_config: &SharedKeyConfig) -> String {\n\tformat!(\n\t\t\"Stashes [{}]\",\n\t\tkey_config.get_hint(key_config.keys.tab_stashes)\n\t)\n}\npub fn tab_divider(_key_config: &SharedKeyConfig) -> String {\n\t\" | \".to_string()\n}\npub fn cmd_splitter(_key_config: &SharedKeyConfig) -> String {\n\t\" \".to_string()\n}\npub fn msg_opening_editor(_key_config: &SharedKeyConfig) -> String {\n\t\"opening editor...\".to_string()\n}\npub fn msg_title_error(_key_config: &SharedKeyConfig) -> String {\n\t\"Error\".to_string()\n}\npub fn msg_title_info(_key_config: &SharedKeyConfig) -> String {\n\t\"Info\".to_string()\n}\npub fn commit_title() -> String {\n\t\"Commit\".to_string()\n}\npub fn commit_reword_title() -> String {\n\t\"Reword Commit\".to_string()\n}\n\npub fn commit_title_merge() -> String {\n\t\"Commit (Merge)\".to_string()\n}\npub fn commit_title_revert() -> String {\n\t\"Commit (Revert)\".to_string()\n}\npub fn commit_title_amend() -> String {\n\t\"Commit (Amend)\".to_string()\n}\npub fn commit_msg(_key_config: &SharedKeyConfig) -> String {\n\t\"type commit message..\".to_string()\n}\npub fn commit_first_line_warning(count: usize) -> String {\n\tformat!(\"[subject length: {count}]\")\n}\npub const fn branch_name_invalid() -> &'static str {\n\t\"[invalid name]\"\n}\npub fn commit_editor_msg(_key_config: &SharedKeyConfig) -> String {\n\tr\"\n# Edit your commit message\n# Lines starting with '#' will be ignored\"\n\t\t.to_string()\n}\npub fn stash_popup_title(_key_config: &SharedKeyConfig) -> String {\n\t\"Stash\".to_string()\n}\npub fn stash_popup_msg(_key_config: &SharedKeyConfig) -> String {\n\t\"type name (optional)\".to_string()\n}\npub fn confirm_title_reset() -> String {\n\t\"Reset\".to_string()\n}\npub fn confirm_title_undo_commit() -> String {\n\t\"Undo commit\".to_string()\n}\npub fn confirm_title_stashdrop(\n\t_key_config: &SharedKeyConfig,\n\tmultiple: bool,\n) -> String {\n\tformat!(\"Drop Stash{}\", if multiple { \"es\" } else { \"\" })\n}\npub fn confirm_title_stashpop(\n\t_key_config: &SharedKeyConfig,\n) -> String {\n\t\"Pop\".to_string()\n}\npub fn confirm_title_merge(\n\t_key_config: &SharedKeyConfig,\n\trebase: bool,\n) -> String {\n\tif rebase {\n\t\t\"Merge (via rebase)\".to_string()\n\t} else {\n\t\t\"Merge (via commit)\".to_string()\n\t}\n}\npub fn confirm_msg_merge(\n\t_key_config: &SharedKeyConfig,\n\tincoming: usize,\n\trebase: bool,\n) -> String {\n\tif rebase {\n\t\tformat!(\"Rebase onto {incoming} incoming commits?\")\n\t} else {\n\t\tformat!(\"Merge of {incoming} incoming commits?\")\n\t}\n}\n\npub fn confirm_title_abortmerge() -> String {\n\t\"Abort merge?\".to_string()\n}\npub fn confirm_title_abortrevert() -> String {\n\t\"Abort revert?\".to_string()\n}\npub fn confirm_msg_revertchanges() -> String {\n\t\"This will revert all uncommitted changes. Are you sure?\"\n\t\t.to_string()\n}\npub fn confirm_title_abortrebase() -> String {\n\t\"Abort rebase?\".to_string()\n}\npub fn confirm_msg_abortrebase() -> String {\n\t\"This will revert all uncommitted changes. Are you sure?\"\n\t\t.to_string()\n}\npub fn confirm_msg_reset() -> String {\n\t\"confirm file reset?\".to_string()\n}\npub fn confirm_msg_reset_lines(lines: usize) -> String {\n\tformat!(\n\t\t\"are you sure you want to discard {lines} selected lines?\"\n\t)\n}\npub fn confirm_msg_undo_commit() -> String {\n\t\"confirm undo last commit?\".to_string()\n}\npub fn confirm_msg_stashdrop(\n\t_key_config: &SharedKeyConfig,\n\tids: &[CommitId],\n) -> String {\n\tformat!(\n\t\t\"Sure you want to drop following {}stash{}?\\n\\n{}\",\n\t\tif ids.len() > 1 {\n\t\t\tformat!(\"{} \", ids.len())\n\t\t} else {\n\t\t\tString::default()\n\t\t},\n\t\tif ids.len() > 1 { \"es\" } else { \"\" },\n\t\tids.iter()\n\t\t\t.map(CommitId::get_short_string)\n\t\t\t.collect::<Vec<_>>()\n\t\t\t.join(\", \")\n\t)\n}\npub fn confirm_msg_stashpop(_key_config: &SharedKeyConfig) -> String {\n\t\"The stash will be applied and removed from the stash list. Confirm stash pop?\"\n        .to_string()\n}\npub fn confirm_msg_resethunk(\n\t_key_config: &SharedKeyConfig,\n) -> String {\n\t\"confirm reset hunk?\".to_string()\n}\npub fn confirm_title_delete_branch(\n\t_key_config: &SharedKeyConfig,\n) -> String {\n\t\"Delete Branch\".to_string()\n}\npub fn confirm_msg_delete_branch(\n\t_key_config: &SharedKeyConfig,\n\tbranch_ref: &str,\n) -> String {\n\tformat!(\"Confirm deleting branch: '{branch_ref}' ?\")\n}\npub fn confirm_title_delete_remote_branch(\n\t_key_config: &SharedKeyConfig,\n) -> String {\n\t\"Delete Remote Branch\".to_string()\n}\npub fn confirm_title_delete_remote(\n\t_key_config: &SharedKeyConfig,\n) -> String {\n\t\"Delete Remote\".to_string()\n}\npub fn confirm_msg_delete_remote(\n\t_key_config: &SharedKeyConfig,\n\tremote_name: &str,\n) -> String {\n\tformat!(\"Confirm deleting remote \\\"{remote_name}\\\"\")\n}\npub fn confirm_msg_delete_remote_branch(\n\t_key_config: &SharedKeyConfig,\n\tbranch_ref: &str,\n) -> String {\n\tformat!(\"Confirm deleting remote branch: '{branch_ref}' ?\")\n}\npub fn confirm_title_delete_tag(\n\t_key_config: &SharedKeyConfig,\n) -> String {\n\t\"Delete Tag\".to_string()\n}\npub fn confirm_msg_delete_tag(\n\t_key_config: &SharedKeyConfig,\n\ttag_name: &str,\n) -> String {\n\tformat!(\"Confirm deleting Tag: '{tag_name}' ?\")\n}\npub fn confirm_title_delete_tag_remote() -> String {\n\t\"Delete Tag (remote)\".to_string()\n}\npub fn confirm_msg_delete_tag_remote(remote_name: &str) -> String {\n\tformat!(\"Confirm deleting tag on remote '{remote_name}'?\")\n}\npub fn confirm_title_force_push(\n\t_key_config: &SharedKeyConfig,\n) -> String {\n\t\"Force Push\".to_string()\n}\npub fn confirm_msg_force_push(\n\t_key_config: &SharedKeyConfig,\n\tbranch_ref: &str,\n) -> String {\n\tformat!(\n        \"Confirm force push to branch '{branch_ref}' ?  This may rewrite history.\"\n    )\n}\npub fn log_title(_key_config: &SharedKeyConfig) -> String {\n\t\"Commit\".to_string()\n}\npub fn file_log_title(\n\tfile_path: &str,\n\tselected: usize,\n\trevisions: usize,\n) -> String {\n\tformat!(\"Revisions of '{file_path}' ({selected}/{revisions})\")\n}\npub fn blame_title(_key_config: &SharedKeyConfig) -> String {\n\t\"Blame\".to_string()\n}\npub fn tag_popup_name_title() -> String {\n\t\"Tag\".to_string()\n}\npub fn tag_popup_name_msg() -> String {\n\t\"type tag name\".to_string()\n}\npub fn tag_popup_annotation_title(name: &str) -> String {\n\tformat!(\"Tag Annotation ({name})\")\n}\npub fn tag_popup_annotation_msg() -> String {\n\t\"type tag annotation\".to_string()\n}\npub fn stashlist_title(_key_config: &SharedKeyConfig) -> String {\n\t\"Stashes\".to_string()\n}\npub fn help_title(_key_config: &SharedKeyConfig) -> String {\n\t\"Help: all commands\".to_string()\n}\npub fn stashing_files_title(_key_config: &SharedKeyConfig) -> String {\n\t\"Files to Stash\".to_string()\n}\npub fn stashing_options_title(\n\t_key_config: &SharedKeyConfig,\n) -> String {\n\t\"Options\".to_string()\n}\npub fn loading_text(_key_config: &SharedKeyConfig) -> String {\n\t\"Loading ...\".to_string()\n}\npub fn create_branch_popup_title(\n\t_key_config: &SharedKeyConfig,\n) -> String {\n\t\"Branch\".to_string()\n}\npub fn create_branch_popup_msg(\n\t_key_config: &SharedKeyConfig,\n) -> String {\n\t\"type branch name\".to_string()\n}\npub fn rename_remote_popup_title(\n\t_key_config: &SharedKeyConfig,\n) -> String {\n\t\"Rename remote\".to_string()\n}\npub fn rename_remote_popup_msg(\n\t_key_config: &SharedKeyConfig,\n) -> String {\n\t\"new remote name\".to_string()\n}\npub fn update_remote_url_popup_title(\n\t_key_config: &SharedKeyConfig,\n) -> String {\n\t\"Update url\".to_string()\n}\npub fn update_remote_url_popup_msg(\n\t_key_config: &SharedKeyConfig,\n) -> String {\n\t\"new remote url\".to_string()\n}\npub fn create_remote_popup_title_name(\n\t_key_config: &SharedKeyConfig,\n) -> String {\n\t\"Remote name\".to_string()\n}\npub fn create_remote_popup_title_url(\n\t_key_config: &SharedKeyConfig,\n) -> String {\n\t\"Remote url\".to_string()\n}\npub fn create_remote_popup_msg_name(\n\t_key_config: &SharedKeyConfig,\n) -> String {\n\t\"type remote name\".to_string()\n}\npub fn create_remote_popup_msg_url(\n\t_key_config: &SharedKeyConfig,\n) -> String {\n\t\"type remote url\".to_string()\n}\npub const fn remote_name_invalid() -> &'static str {\n\t\"[invalid name]\"\n}\npub fn username_popup_title(_key_config: &SharedKeyConfig) -> String {\n\t\"Username\".to_string()\n}\npub fn username_popup_msg(_key_config: &SharedKeyConfig) -> String {\n\t\"type username\".to_string()\n}\npub fn password_popup_title(_key_config: &SharedKeyConfig) -> String {\n\t\"Password\".to_string()\n}\npub fn password_popup_msg(_key_config: &SharedKeyConfig) -> String {\n\t\"type password\".to_string()\n}\n\npub fn rename_branch_popup_title(\n\t_key_config: &SharedKeyConfig,\n) -> String {\n\t\"Rename Branch\".to_string()\n}\npub fn rename_branch_popup_msg(\n\t_key_config: &SharedKeyConfig,\n) -> String {\n\t\"new branch name\".to_string()\n}\n\npub fn copy_success(s: &str) -> String {\n\tformat!(\"{POPUP_SUCCESS_COPY} \\\"{s}\\\"\")\n}\n\npub fn ellipsis_trim_start(s: &str, width: usize) -> Cow<'_, str> {\n\tif s.width() <= width {\n\t\tCow::Borrowed(s)\n\t} else {\n\t\tCow::Owned(format!(\n\t\t\t\"[{}]{}\",\n\t\t\tsymbol::ELLIPSIS,\n\t\t\ts.unicode_truncate_start(\n\t\t\t\twidth.saturating_sub(3 /* front indicator */)\n\t\t\t)\n\t\t\t.0\n\t\t))\n\t}\n}\n\n#[derive(PartialEq, Eq, Clone, Copy)]\npub enum CheckoutOptions {\n\tKeepLocalChanges,\n\tDiscardAllLocalChagnes,\n}\n\nimpl CheckoutOptions {\n\tpub const fn previous(self) -> Self {\n\t\tmatch self {\n\t\t\tSelf::KeepLocalChanges => Self::DiscardAllLocalChagnes,\n\t\t\tSelf::DiscardAllLocalChagnes => Self::KeepLocalChanges,\n\t\t}\n\t}\n\n\tpub const fn next(self) -> Self {\n\t\tmatch self {\n\t\t\tSelf::KeepLocalChanges => Self::DiscardAllLocalChagnes,\n\t\t\tSelf::DiscardAllLocalChagnes => Self::KeepLocalChanges,\n\t\t}\n\t}\n\n\tpub const fn to_string_pair(\n\t\tself,\n\t) -> (&'static str, &'static str) {\n\t\tconst CHECKOUT_OPTION_UNCHANGE: &str =\n\t\t\t\" 🟡 Keep local changes\";\n\t\tconst CHECKOUT_OPTION_DISCARD: &str =\n\t\t\t\" 🔴 Discard all local changes\";\n\n\t\tmatch self {\n\t\t\tSelf::KeepLocalChanges => {\n\t\t\t\t(\"Don't change\", CHECKOUT_OPTION_UNCHANGE)\n\t\t\t}\n\t\t\tSelf::DiscardAllLocalChagnes => {\n\t\t\t\t(\"Discard\", CHECKOUT_OPTION_DISCARD)\n\t\t\t}\n\t\t}\n\t}\n}\n\npub mod commit {\n\tuse crate::keys::SharedKeyConfig;\n\n\tpub fn details_author() -> String {\n\t\t\"Author: \".to_string()\n\t}\n\tpub fn details_committer() -> String {\n\t\t\"Committer: \".to_string()\n\t}\n\tpub fn details_sha() -> String {\n\t\t\"Sha: \".to_string()\n\t}\n\tpub fn details_date() -> String {\n\t\t\"Date: \".to_string()\n\t}\n\tpub fn details_tags() -> String {\n\t\t\"Tags: \".to_string()\n\t}\n\tpub fn details_message() -> String {\n\t\t\"Subject: \".to_string()\n\t}\n\tpub fn details_info_title(\n\t\t_key_config: &SharedKeyConfig,\n\t) -> String {\n\t\t\"Info\".to_string()\n\t}\n\tpub fn compare_details_info_title(\n\t\told: bool,\n\t\thash: &str,\n\t) -> String {\n\t\tformat!(\"{}: {hash}\", if old { \"Old\" } else { \"New\" })\n\t}\n\tpub fn details_message_title(\n\t\t_key_config: &SharedKeyConfig,\n\t) -> String {\n\t\t\"Message\".to_string()\n\t}\n\tpub fn details_files_title(\n\t\t_key_config: &SharedKeyConfig,\n\t) -> String {\n\t\t\"Files:\".to_string()\n\t}\n}\n\npub mod commands {\n\tuse crate::components::CommandText;\n\tuse crate::keys::SharedKeyConfig;\n\n\tstatic CMD_GROUP_GENERAL: &str = \"-- General --\";\n\tstatic CMD_GROUP_DIFF: &str = \"-- Diff --\";\n\tstatic CMD_GROUP_CHANGES: &str = \"-- Changes --\";\n\tstatic CMD_GROUP_COMMIT_POPUP: &str = \"-- Commit Popup --\";\n\tstatic CMD_GROUP_STASHING: &str = \"-- Stashing --\";\n\tstatic CMD_GROUP_STASHES: &str = \"-- Stashes --\";\n\tstatic CMD_GROUP_LOG: &str = \"-- Log --\";\n\tstatic CMD_GROUP_BRANCHES: &str = \"-- Branches --\";\n\n\tpub fn toggle_tabs(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Next [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.tab_toggle)\n\t\t\t),\n\t\t\t\"switch to next tab\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\tpub fn find_file(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Find [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.file_find)\n\t\t\t),\n\t\t\t\"find file in tree\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\tpub fn find_branch(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Find [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.branch_find)\n\t\t\t),\n\t\t\t\"find branch in list\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\tpub fn toggle_tabs_direct(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Tab [{}{}{}{}{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.tab_status),\n\t\t\t\tkey_config.get_hint(key_config.keys.tab_log),\n\t\t\t\tkey_config.get_hint(key_config.keys.tab_files),\n\t\t\t\tkey_config.get_hint(key_config.keys.tab_stashing),\n\t\t\t\tkey_config.get_hint(key_config.keys.tab_stashes),\n\t\t\t),\n\t\t\t\"switch top level tabs directly\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\tpub fn options_popup(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Options [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.open_options),\n\t\t\t),\n\t\t\t\"open options popup\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\tpub fn help_open(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Help [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.open_help)\n\t\t\t),\n\t\t\t\"open this help screen\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\tpub fn navigate_commit_message(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Nav [{}{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.move_up),\n\t\t\t\tkey_config.get_hint(key_config.keys.move_down)\n\t\t\t),\n\t\t\t\"navigate commit message\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\tpub fn navigate_tree(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Nav [{}{}{}{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.move_up),\n\t\t\t\tkey_config.get_hint(key_config.keys.move_down),\n\t\t\t\tkey_config.get_hint(key_config.keys.move_right),\n\t\t\t\tkey_config.get_hint(key_config.keys.move_left)\n\t\t\t),\n\t\t\t\"navigate tree view, collapse, expand\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\tpub fn scroll(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Scroll [{}{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.move_up),\n\t\t\t\tkey_config.get_hint(key_config.keys.move_down)\n\t\t\t),\n\t\t\t\"scroll up or down in focused view\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\tpub fn commit_list_mark(\n\t\tkey_config: &SharedKeyConfig,\n\t\tmarked: bool,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"{} [{}]\",\n\t\t\t\tif marked { \"Unmark\" } else { \"Mark\" },\n\t\t\t\tkey_config.get_hint(key_config.keys.log_mark_commit),\n\t\t\t),\n\t\t\t\"mark multiple commits\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\tpub fn copy(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Copy [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.copy),\n\t\t\t),\n\t\t\t\"copy selected lines to clipboard\",\n\t\t\tCMD_GROUP_DIFF,\n\t\t)\n\t}\n\tpub fn copy_hash(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Copy Hash [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.copy),\n\t\t\t),\n\t\t\t\"copy selected commit hash to clipboard\",\n\t\t\tCMD_GROUP_LOG,\n\t\t)\n\t}\n\tpub fn copy_path(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Copy Path [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.copy),\n\t\t\t),\n\t\t\t\"copy selected file path to clipboard\",\n\t\t\tCMD_GROUP_LOG,\n\t\t)\n\t}\n\tpub fn push_tags(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Push Tags [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.push),\n\t\t\t),\n\t\t\t\"push tags to remote\",\n\t\t\tCMD_GROUP_LOG,\n\t\t)\n\t}\n\tpub fn toggle_option(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Toggle Option [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.log_mark_commit),\n\t\t\t),\n\t\t\t\"toggle search option selected\",\n\t\t\tCMD_GROUP_LOG,\n\t\t)\n\t}\n\tpub fn show_tag_annotation(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Annotation [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.move_right),\n\t\t\t),\n\t\t\t\"show tag annotation\",\n\t\t\tCMD_GROUP_LOG,\n\t\t)\n\t}\n\tpub fn diff_hunk_next(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Next hunk [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.diff_hunk_next),\n\t\t\t),\n\t\t\t\"move cursor to next hunk\",\n\t\t\tCMD_GROUP_DIFF,\n\t\t)\n\t}\n\tpub fn diff_hunk_prev(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Prev hunk [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.diff_hunk_prev),\n\t\t\t),\n\t\t\t\"move cursor to prev hunk\",\n\t\t\tCMD_GROUP_DIFF,\n\t\t)\n\t}\n\tpub fn diff_home_end(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Jump up/down [{},{},{},{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.home),\n\t\t\t\tkey_config.get_hint(key_config.keys.end),\n\t\t\t\tkey_config.get_hint(key_config.keys.move_up),\n\t\t\t\tkey_config.get_hint(key_config.keys.move_down)\n\t\t\t),\n\t\t\t\"scroll to top or bottom of diff\",\n\t\t\tCMD_GROUP_DIFF,\n\t\t)\n\t}\n\tpub fn diff_hunk_add(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Add hunk [{}]\",\n\t\t\t\tkey_config\n\t\t\t\t\t.get_hint(key_config.keys.stage_unstage_item),\n\t\t\t),\n\t\t\t\"adds selected hunk to stage\",\n\t\t\tCMD_GROUP_DIFF,\n\t\t)\n\t}\n\tpub fn diff_hunk_revert(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Reset hunk [{}]\",\n\t\t\t\tkey_config\n\t\t\t\t\t.get_hint(key_config.keys.status_reset_item),\n\t\t\t),\n\t\t\t\"reverts selected hunk\",\n\t\t\tCMD_GROUP_DIFF,\n\t\t)\n\t}\n\tpub fn diff_lines_revert(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Reset lines [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.diff_reset_lines),\n\t\t\t),\n\t\t\t\"resets selected lines\",\n\t\t\tCMD_GROUP_DIFF,\n\t\t)\n\t}\n\tpub fn diff_lines_stage(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Stage lines [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.diff_stage_lines),\n\t\t\t),\n\t\t\t\"stage selected lines\",\n\t\t\tCMD_GROUP_DIFF,\n\t\t)\n\t}\n\tpub fn diff_lines_unstage(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Unstage lines [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.diff_stage_lines),\n\t\t\t),\n\t\t\t\"unstage selected lines\",\n\t\t\tCMD_GROUP_DIFF,\n\t\t)\n\t}\n\tpub fn diff_hunk_remove(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Remove hunk [{}]\",\n\t\t\t\tkey_config\n\t\t\t\t\t.get_hint(key_config.keys.stage_unstage_item),\n\t\t\t),\n\t\t\t\"removes selected hunk from stage\",\n\t\t\tCMD_GROUP_DIFF,\n\t\t)\n\t}\n\tpub fn close_fuzzy_finder(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Close [{}{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.exit_popup),\n\t\t\t\tkey_config.get_hint(key_config.keys.enter),\n\t\t\t),\n\t\t\t\"close fuzzy finder\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\tpub fn close_popup(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Close [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.exit_popup),\n\t\t\t),\n\t\t\t\"close overlay (e.g commit, help)\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\tpub fn scroll_popup(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Scroll [{}{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.popup_down),\n\t\t\t\tkey_config.get_hint(key_config.keys.popup_up),\n\t\t\t),\n\t\t\t\"scroll up or down in popup\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\tpub fn close_msg(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Close [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.enter),\n\t\t\t),\n\t\t\t\"close msg popup (e.g msg)\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t\t.hide_help()\n\t}\n\tpub fn validate_msg(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Validate [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.enter),\n\t\t\t),\n\t\t\t\"validate msg\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t\t.hide_help()\n\t}\n\n\tpub fn abort_merge(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Abort merge [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.abort_merge),\n\t\t\t),\n\t\t\t\"abort ongoing merge\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\n\tpub fn abort_revert(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Abort revert [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.abort_merge),\n\t\t\t),\n\t\t\t\"abort ongoing revert\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\n\tpub fn view_submodules(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Submodules [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.view_submodules),\n\t\t\t),\n\t\t\t\"open submodule view\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\n\tpub fn view_remotes(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Remotes [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.view_remotes)\n\t\t\t),\n\t\t\t\"open remotes view\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\n\tpub fn update_remote_name(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Edit name [{}]\",\n\t\t\t\tkey_config\n\t\t\t\t\t.get_hint(key_config.keys.update_remote_name)\n\t\t\t),\n\t\t\t\"updates a remote name\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\n\tpub fn update_remote_url(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Edit url [{}]\",\n\t\t\t\tkey_config\n\t\t\t\t\t.get_hint(key_config.keys.update_remote_url)\n\t\t\t),\n\t\t\t\"updates a remote url\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\n\tpub fn create_remote(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Add [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.add_remote)\n\t\t\t),\n\t\t\t\"creates a new remote\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\n\tpub fn delete_remote_popup(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Remove [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.delete_remote),\n\t\t\t),\n\t\t\t\"remove a remote\",\n\t\t\tCMD_GROUP_BRANCHES,\n\t\t)\n\t}\n\n\tpub fn remote_confirm_name_msg(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Confirm name [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.enter),\n\t\t\t),\n\t\t\t\"confirm remote name\",\n\t\t\tCMD_GROUP_BRANCHES,\n\t\t)\n\t\t.hide_help()\n\t}\n\n\tpub fn remote_confirm_url_msg(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Confirm url [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.enter),\n\t\t\t),\n\t\t\t\"confirm remote url\",\n\t\t\tCMD_GROUP_BRANCHES,\n\t\t)\n\t\t.hide_help()\n\t}\n\n\tpub fn open_submodule(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Open [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.enter),\n\t\t\t),\n\t\t\t\"open submodule\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\n\tpub fn open_submodule_parent(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Open Parent [{}]\",\n\t\t\t\tkey_config\n\t\t\t\t\t.get_hint(key_config.keys.view_submodule_parent),\n\t\t\t),\n\t\t\t\"open submodule parent repo\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\n\tpub fn update_submodule(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Update [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.update_submodule),\n\t\t\t),\n\t\t\t\"update submodule\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\n\tpub fn continue_rebase(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Continue rebase [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.rebase_branch),\n\t\t\t),\n\t\t\t\"continue ongoing rebase\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\n\tpub fn abort_rebase(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Abort rebase [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.abort_merge),\n\t\t\t),\n\t\t\t\"abort ongoing rebase\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\n\tpub fn select_staging(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"To stage [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.toggle_workarea),\n\t\t\t),\n\t\t\t\"focus/select staging area\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\tpub fn select_unstaged(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"To unstaged [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.toggle_workarea),\n\t\t\t),\n\t\t\t\"focus/select unstaged area\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\tpub fn undo_commit(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Undo Commit [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.undo_commit),\n\t\t\t),\n\t\t\t\"undo last commit\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\tpub fn commit_open(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Commit [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.open_commit),\n\t\t\t),\n\t\t\t\"open commit popup (available in non-empty stage)\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\tpub fn commit_open_editor(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Open editor [{}]\",\n\t\t\t\tkey_config\n\t\t\t\t\t.get_hint(key_config.keys.open_commit_editor),\n\t\t\t),\n\t\t\t\"open commit editor (available in commit popup)\",\n\t\t\tCMD_GROUP_COMMIT_POPUP,\n\t\t)\n\t}\n\tpub fn commit_next_msg_from_history(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Previous Msg [{}]\",\n\t\t\t\tkey_config\n\t\t\t\t\t.get_hint(key_config.keys.commit_history_next),\n\t\t\t),\n\t\t\t\"use previous commit message from history\",\n\t\t\tCMD_GROUP_COMMIT_POPUP,\n\t\t)\n\t}\n\tpub fn commit_submit(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Do Commit [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.commit),\n\t\t\t),\n\t\t\t\"commit (available when commit message is non-empty)\",\n\t\t\tCMD_GROUP_COMMIT_POPUP,\n\t\t)\n\t\t.hide_help()\n\t}\n\tpub fn newline(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"New line [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.newline),\n\t\t\t),\n\t\t\t\"create line break\",\n\t\t\tCMD_GROUP_COMMIT_POPUP,\n\t\t)\n\t\t.hide_help()\n\t}\n\tpub fn toggle_verify(\n\t\tkey_config: &SharedKeyConfig,\n\t\tcurrent_verify: bool,\n\t) -> CommandText {\n\t\tlet verb = if current_verify { \"disable\" } else { \"enable\" };\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"{} hooks [{}]\",\n\t\t\t\tverb,\n\t\t\t\tkey_config.get_hint(key_config.keys.toggle_verify),\n\t\t\t),\n\t\t\t\"toggle running on commit hooks (available in commit popup)\",\n\t\t\tCMD_GROUP_COMMIT_POPUP,\n\t\t)\n\t}\n\n\tpub fn commit_amend(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Amend [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.commit_amend),\n\t\t\t),\n\t\t\t\"amend last commit (available in commit popup)\",\n\t\t\tCMD_GROUP_COMMIT_POPUP,\n\t\t)\n\t}\n\tpub fn commit_signoff(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Sign-off [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.toggle_signoff),\n\t\t\t),\n\t\t\t\"sign-off commit (-s option)\",\n\t\t\tCMD_GROUP_COMMIT_POPUP,\n\t\t)\n\t}\n\tpub fn edit_item(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Edit [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.edit_file),\n\t\t\t),\n\t\t\t\"edit the currently selected file in an external editor\",\n\t\t\tCMD_GROUP_CHANGES,\n\t\t)\n\t}\n\tpub fn stage_item(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Stage [{}]\",\n\t\t\t\tkey_config\n\t\t\t\t\t.get_hint(key_config.keys.stage_unstage_item),\n\t\t\t),\n\t\t\t\"stage currently selected file or entire path\",\n\t\t\tCMD_GROUP_CHANGES,\n\t\t)\n\t}\n\tpub fn stage_all(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Stage All [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.status_stage_all),\n\t\t\t),\n\t\t\t\"stage all changes (in unstaged files)\",\n\t\t\tCMD_GROUP_CHANGES,\n\t\t)\n\t}\n\tpub fn unstage_item(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Unstage [{}]\",\n\t\t\t\tkey_config\n\t\t\t\t\t.get_hint(key_config.keys.stage_unstage_item),\n\t\t\t),\n\t\t\t\"unstage currently selected file or entire path\",\n\t\t\tCMD_GROUP_CHANGES,\n\t\t)\n\t}\n\tpub fn unstage_all(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Unstage all [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.status_stage_all),\n\t\t\t),\n\t\t\t\"unstage all files (in staged files)\",\n\t\t\tCMD_GROUP_CHANGES,\n\t\t)\n\t}\n\tpub fn reset_item(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Reset [{}]\",\n\t\t\t\tkey_config\n\t\t\t\t\t.get_hint(key_config.keys.status_reset_item),\n\t\t\t),\n\t\t\t\"revert changes in selected file or entire path\",\n\t\t\tCMD_GROUP_CHANGES,\n\t\t)\n\t}\n\tpub fn ignore_item(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Ignore [{}]\",\n\t\t\t\tkey_config\n\t\t\t\t\t.get_hint(key_config.keys.status_ignore_file),\n\t\t\t),\n\t\t\t\"Add file or path to .gitignore\",\n\t\t\tCMD_GROUP_CHANGES,\n\t\t)\n\t}\n\n\tpub fn diff_focus_left(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Back [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.move_left),\n\t\t\t),\n\t\t\t\"view and select changed files\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\tpub fn diff_focus_right(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Diff [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.move_right),\n\t\t\t),\n\t\t\t\"inspect file diff\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\tpub fn quit(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Quit [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.exit),\n\t\t\t),\n\t\t\t\"quit gitui application\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\tpub fn confirm_action(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Confirm [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.enter),\n\t\t\t),\n\t\t\t\"confirm action\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\tpub fn stashing_save(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Save [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.stashing_save),\n\t\t\t),\n\t\t\t\"opens stash name input popup\",\n\t\t\tCMD_GROUP_STASHING,\n\t\t)\n\t}\n\tpub fn stashing_toggle_indexed(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Toggle Staged [{}]\",\n\t\t\t\tkey_config\n\t\t\t\t\t.get_hint(key_config.keys.stashing_toggle_index),\n\t\t\t),\n\t\t\t\"toggle including staged files into stash\",\n\t\t\tCMD_GROUP_STASHING,\n\t\t)\n\t}\n\tpub fn stashing_toggle_untracked(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Toggle Untracked [{}]\",\n\t\t\t\tkey_config.get_hint(\n\t\t\t\t\tkey_config.keys.stashing_toggle_untracked\n\t\t\t\t),\n\t\t\t),\n\t\t\t\"toggle including untracked files into stash\",\n\t\t\tCMD_GROUP_STASHING,\n\t\t)\n\t}\n\tpub fn stashing_confirm_msg(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Stash [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.enter),\n\t\t\t),\n\t\t\t\"save files to stash\",\n\t\t\tCMD_GROUP_STASHING,\n\t\t)\n\t}\n\tpub fn stashlist_apply(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Apply [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.stash_apply),\n\t\t\t),\n\t\t\t\"apply selected stash\",\n\t\t\tCMD_GROUP_STASHES,\n\t\t)\n\t}\n\tpub fn stashlist_drop(\n\t\tkey_config: &SharedKeyConfig,\n\t\tmarked: usize,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Drop{} [{}]\",\n\t\t\t\tif marked == 0 {\n\t\t\t\t\tString::default()\n\t\t\t\t} else {\n\t\t\t\t\tformat!(\" {marked}\")\n\t\t\t\t},\n\t\t\t\tkey_config.get_hint(key_config.keys.stash_drop),\n\t\t\t),\n\t\t\t\"drop selected stash\",\n\t\t\tCMD_GROUP_STASHES,\n\t\t)\n\t}\n\tpub fn stashlist_pop(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Pop [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.enter),\n\t\t\t),\n\t\t\t\"pop selected stash\",\n\t\t\tCMD_GROUP_STASHES,\n\t\t)\n\t}\n\tpub fn stashlist_inspect(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Inspect [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.stash_open),\n\t\t\t),\n\t\t\t\"open stash commit details (allows to diff files)\",\n\t\t\tCMD_GROUP_STASHES,\n\t\t)\n\t}\n\tpub fn log_details_toggle(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Details [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.enter),\n\t\t\t),\n\t\t\t\"open details of selected commit\",\n\t\t\tCMD_GROUP_LOG,\n\t\t)\n\t}\n\n\tpub fn commit_details_open(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Inspect [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.move_right),\n\t\t\t),\n\t\t\t\"inspect selected commit in detail\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\n\tpub fn blame_file(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Blame [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.blame),\n\t\t\t),\n\t\t\t\"open blame view of selected file\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\tpub fn open_file_history(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"History [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.file_history),\n\t\t\t),\n\t\t\t\"open history of selected file\",\n\t\t\tCMD_GROUP_LOG,\n\t\t)\n\t}\n\tpub fn open_line_number_popup(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Go to [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.goto_line),\n\t\t\t),\n\t\t\t\"go to a given line number in the blame view\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\tpub fn log_tag_commit(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Tag [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.log_tag_commit),\n\t\t\t),\n\t\t\t\"tag commit\",\n\t\t\tCMD_GROUP_LOG,\n\t\t)\n\t}\n\tpub fn log_checkout_commit(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Checkout [{}]\",\n\t\t\t\tkey_config\n\t\t\t\t\t.get_hint(key_config.keys.log_checkout_commit),\n\t\t\t),\n\t\t\t\"checkout commit\",\n\t\t\tCMD_GROUP_LOG,\n\t\t)\n\t}\n\tpub fn inspect_file_tree(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Files [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.open_file_tree),\n\t\t\t),\n\t\t\t\"inspect file tree at specific revision\",\n\t\t\tCMD_GROUP_LOG,\n\t\t)\n\t}\n\tpub fn revert_commit(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Revert [{}]\",\n\t\t\t\tkey_config\n\t\t\t\t\t.get_hint(key_config.keys.status_reset_item),\n\t\t\t),\n\t\t\t\"revert commit\",\n\t\t\tCMD_GROUP_LOG,\n\t\t)\n\t}\n\tpub fn log_reset_commit(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Reset [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.log_reset_commit),\n\t\t\t),\n\t\t\t\"reset to commit\",\n\t\t\tCMD_GROUP_LOG,\n\t\t)\n\t}\n\tpub fn log_reword_commit(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Reword [{}]\",\n\t\t\t\tkey_config\n\t\t\t\t\t.get_hint(key_config.keys.log_reword_commit),\n\t\t\t),\n\t\t\t\"reword commit message\",\n\t\t\tCMD_GROUP_LOG,\n\t\t)\n\t}\n\tpub fn log_find_commit(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Find [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.file_find),\n\t\t\t),\n\t\t\t\"start commit search\",\n\t\t\tCMD_GROUP_LOG,\n\t\t)\n\t}\n\tpub fn log_close_search(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Exit Search [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.exit_popup),\n\t\t\t),\n\t\t\t\"leave search mode\",\n\t\t\tCMD_GROUP_LOG,\n\t\t)\n\t}\n\n\tpub fn reset_commit(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Confirm [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.enter),\n\t\t\t),\n\t\t\t\"confirm reset\",\n\t\t\tCMD_GROUP_LOG,\n\t\t)\n\t}\n\n\tpub fn reset_branch(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Reset [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.reset_branch),\n\t\t\t),\n\t\t\t\"confirm reset\",\n\t\t\tCMD_GROUP_BRANCHES,\n\t\t)\n\t}\n\n\tpub fn reset_type(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Change Type [{}{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.move_up),\n\t\t\t\tkey_config.get_hint(key_config.keys.move_down)\n\t\t\t),\n\t\t\t\"change reset type\",\n\t\t\tCMD_GROUP_LOG,\n\t\t)\n\t}\n\tpub fn tag_commit_confirm_msg(\n\t\tkey_config: &SharedKeyConfig,\n\t\tis_annotation_mode: bool,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Tag [{}]\",\n\t\t\t\tkey_config.get_hint(if is_annotation_mode {\n\t\t\t\t\tkey_config.keys.commit\n\t\t\t\t} else {\n\t\t\t\t\tkey_config.keys.enter\n\t\t\t\t}),\n\t\t\t),\n\t\t\t\"tag commit\",\n\t\t\tCMD_GROUP_LOG,\n\t\t)\n\t}\n\n\tpub fn tag_annotate_msg(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Annotate [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.tag_annotate),\n\t\t\t),\n\t\t\t\"annotate tag\",\n\t\t\tCMD_GROUP_LOG,\n\t\t)\n\t}\n\n\tpub fn create_branch_confirm_msg(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Create Branch [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.enter),\n\t\t\t),\n\t\t\t\"create branch\",\n\t\t\tCMD_GROUP_BRANCHES,\n\t\t)\n\t\t.hide_help()\n\t}\n\tpub fn open_branch_create_popup(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Create [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.create_branch),\n\t\t\t),\n\t\t\t\"open create branch popup\",\n\t\t\tCMD_GROUP_BRANCHES,\n\t\t)\n\t}\n\tpub fn rename_branch_confirm_msg(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Rename Branch [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.enter),\n\t\t\t),\n\t\t\t\"rename branch\",\n\t\t\tCMD_GROUP_BRANCHES,\n\t\t)\n\t\t.hide_help()\n\t}\n\tpub fn rename_branch_popup(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Rename Branch [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.rename_branch),\n\t\t\t),\n\t\t\t\"rename branch\",\n\t\t\tCMD_GROUP_BRANCHES,\n\t\t)\n\t}\n\tpub fn delete_branch_popup(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Delete [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.delete_branch),\n\t\t\t),\n\t\t\t\"delete a branch\",\n\t\t\tCMD_GROUP_BRANCHES,\n\t\t)\n\t}\n\tpub fn merge_branch_popup(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Merge [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.merge_branch),\n\t\t\t),\n\t\t\t\"merge a branch\",\n\t\t\tCMD_GROUP_BRANCHES,\n\t\t)\n\t}\n\n\tpub fn branch_popup_rebase(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Rebase [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.rebase_branch),\n\t\t\t),\n\t\t\t\"rebase a branch\",\n\t\t\tCMD_GROUP_BRANCHES,\n\t\t)\n\t}\n\n\tpub fn compare_with_head(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Compare [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.compare_commits),\n\t\t\t),\n\t\t\t\"compare with head\",\n\t\t\tCMD_GROUP_BRANCHES,\n\t\t)\n\t}\n\n\tpub fn compare_commits(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Compare Commits [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.compare_commits),\n\t\t\t),\n\t\t\t\"compare two marked commits\",\n\t\t\tCMD_GROUP_LOG,\n\t\t)\n\t}\n\n\tpub fn select_branch_popup(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Checkout [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.enter),\n\t\t\t),\n\t\t\t\"checkout branch\",\n\t\t\tCMD_GROUP_BRANCHES,\n\t\t)\n\t}\n\tpub fn toggle_branch_popup(\n\t\tkey_config: &SharedKeyConfig,\n\t\tlocal: bool,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"{} [{}]\",\n\t\t\t\tif local { \"Remote\" } else { \"Local\" },\n\t\t\t\tkey_config.get_hint(key_config.keys.tab_toggle),\n\t\t\t),\n\t\t\t\"toggle branch type (remote/local)\",\n\t\t\tCMD_GROUP_BRANCHES,\n\t\t)\n\t}\n\tpub fn open_branch_select_popup(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Branches [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.select_branch),\n\t\t\t),\n\t\t\t\"open branch popup\",\n\t\t\tCMD_GROUP_BRANCHES,\n\t\t)\n\t}\n\n\tpub fn open_tags_popup(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Tags [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.tags),\n\t\t\t),\n\t\t\t\"open tags popup\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\tpub fn delete_tag_popup(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Delete [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.delete_tag),\n\t\t\t),\n\t\t\t\"delete a tag\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\tpub fn select_tag(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Select commit [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.select_tag),\n\t\t\t),\n\t\t\t\"Select commit in revlog\",\n\t\t\tCMD_GROUP_LOG,\n\t\t)\n\t}\n\n\tpub fn status_push(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Push [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.push),\n\t\t\t),\n\t\t\t\"push to origin\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\tpub fn status_force_push(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Force Push [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.force_push),\n\t\t\t),\n\t\t\t\"force push to origin\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\n\tpub fn status_fetch(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Fetch [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.fetch),\n\t\t\t),\n\t\t\t\"fetch\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\tpub fn status_pull(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Pull [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.pull),\n\t\t\t),\n\t\t\t\"fetch/merge\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n\n\tpub fn fetch_remotes(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Fetch [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.fetch),\n\t\t\t),\n\t\t\t\"fetch/prune\",\n\t\t\tCMD_GROUP_BRANCHES,\n\t\t)\n\t}\n\n\tpub fn find_commit_sha(\n\t\tkey_config: &SharedKeyConfig,\n\t) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Search Hash [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.find_commit_sha),\n\t\t\t),\n\t\t\t\"find commit from sha\",\n\t\t\tCMD_GROUP_LOG,\n\t\t)\n\t}\n\n\tpub fn goto_line(key_config: &SharedKeyConfig) -> CommandText {\n\t\tCommandText::new(\n\t\t\tformat!(\n\t\t\t\t\"Go To [{}]\",\n\t\t\t\tkey_config.get_hint(key_config.keys.enter),\n\t\t\t),\n\t\t\t\"Go to the given line\",\n\t\t\tCMD_GROUP_GENERAL,\n\t\t)\n\t}\n}\n"
  },
  {
    "path": "src/tabs/files.rs",
    "content": "use std::path::{Path, PathBuf};\n\nuse crate::{\n\tapp::Environment,\n\tcomponents::{\n\t\tvisibility_blocking, CommandBlocking, CommandInfo, Component,\n\t\tDrawableComponent, EventState, RevisionFilesComponent,\n\t},\n\tAsyncNotification,\n};\nuse anyhow::Result;\nuse asyncgit::sync::{self, RepoPathRef};\n\npub struct FilesTab {\n\trepo: RepoPathRef,\n\tvisible: bool,\n\tfiles: RevisionFilesComponent,\n}\n\nimpl FilesTab {\n\t///\n\tpub fn new(\n\t\tenv: &Environment,\n\t\tselect_file: Option<PathBuf>,\n\t) -> Self {\n\t\tSelf {\n\t\t\tvisible: false,\n\t\t\tfiles: RevisionFilesComponent::new(env, select_file),\n\t\t\trepo: env.repo.clone(),\n\t\t}\n\t}\n\n\t///\n\tpub fn update(&mut self) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tif let Ok(head) = sync::get_head(&self.repo.borrow()) {\n\t\t\t\tself.files.set_commit(head)?;\n\t\t\t}\n\t\t}\n\n\t\tOk(())\n\t}\n\n\t///\n\tpub fn anything_pending(&self) -> bool {\n\t\tself.files.any_work_pending()\n\t}\n\n\t///\n\tpub fn update_async(\n\t\t&mut self,\n\t\tev: AsyncNotification,\n\t) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tself.files.update(ev)?;\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tpub fn file_finder_update(&mut self, file: &Path) {\n\t\tself.files.find_file(file);\n\t}\n}\n\nimpl DrawableComponent for FilesTab {\n\tfn draw(\n\t\t&self,\n\t\tf: &mut ratatui::Frame,\n\t\trect: ratatui::layout::Rect,\n\t) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tself.files.draw(f, rect)?;\n\t\t}\n\t\tOk(())\n\t}\n}\n\nimpl Component for FilesTab {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.visible || force_all {\n\t\t\treturn self.files.commands(out, force_all);\n\t\t}\n\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(\n\t\t&mut self,\n\t\tev: &crossterm::event::Event,\n\t) -> Result<EventState> {\n\t\tif self.visible {\n\t\t\treturn self.files.event(ev);\n\t\t}\n\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.visible = false;\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\t\tself.update()?;\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "src/tabs/mod.rs",
    "content": "/*!\nThe tabs module contains a struct for each of the tabs visible in the\nui:\n\n- [`Status`]: Stage changes, push, pull\n- [`Revlog`]: Revision log (think git log)\n- [`FilesTab`]: See content of any file at HEAD. Blame\n- [`Stashing`]: Managing one stash\n- [`StashList`]: Managing all stashes\n\nMany of the tabs can expand to show more details. This is done via\nEnter or right-arrow. To close again, press ESC.\n*/\n\nmod files;\nmod revlog;\nmod stashing;\nmod stashlist;\nmod status;\n\npub use files::FilesTab;\npub use revlog::Revlog;\npub use stashing::{Stashing, StashingOptions};\npub use stashlist::StashList;\npub use status::Status;\n"
  },
  {
    "path": "src/tabs/revlog.rs",
    "content": "use crate::{\n\tapp::Environment,\n\tcomponents::{\n\t\tvisibility_blocking, CommandBlocking, CommandInfo,\n\t\tCommitDetailsComponent, CommitList, Component,\n\t\tDrawableComponent, EventState,\n\t},\n\tkeys::{key_match, SharedKeyConfig},\n\tpopups::{FileTreeOpen, InspectCommitOpen},\n\tqueue::{InternalEvent, Queue, StackablePopupOpen},\n\tstrings::{self, order},\n\ttry_or_popup,\n\tui::style::{SharedTheme, Theme},\n};\nuse anyhow::Result;\nuse asyncgit::{\n\tasyncjob::AsyncSingleJob,\n\tsync::{\n\t\tself, filter_commit_by_search, CommitId, LogFilterSearch,\n\t\tLogFilterSearchOptions, RepoPathRef,\n\t},\n\tAsyncBranchesJob, AsyncCommitFilterJob, AsyncGitNotification,\n\tAsyncLog, AsyncTags, CommitFilesParams, FetchStatus,\n\tProgressPercent,\n};\nuse crossbeam_channel::Sender;\nuse crossterm::event::Event;\nuse indexmap::IndexSet;\nuse ratatui::{\n\tlayout::{Alignment, Constraint, Direction, Layout, Rect},\n\ttext::Span,\n\twidgets::{Block, Borders, Paragraph},\n\tFrame,\n};\nuse std::{\n\trc::Rc,\n\tsync::{\n\t\tatomic::{AtomicBool, Ordering},\n\t\tArc,\n\t},\n\ttime::Duration,\n};\nuse sync::CommitTags;\n\nstruct LogSearchResult {\n\toptions: LogFilterSearchOptions,\n\tduration: Duration,\n}\n\n//TODO: deserves its own component\nenum LogSearch {\n\tOff,\n\tSearching(\n\t\tAsyncSingleJob<AsyncCommitFilterJob>,\n\t\tLogFilterSearchOptions,\n\t\tOption<ProgressPercent>,\n\t\tArc<AtomicBool>,\n\t),\n\tResults(LogSearchResult),\n}\n\n///\npub struct Revlog {\n\trepo: RepoPathRef,\n\tcommit_details: CommitDetailsComponent,\n\tlist: CommitList,\n\tgit_log: AsyncLog,\n\tsearch: LogSearch,\n\tgit_tags: AsyncTags,\n\tgit_local_branches: AsyncSingleJob<AsyncBranchesJob>,\n\tgit_remote_branches: AsyncSingleJob<AsyncBranchesJob>,\n\tqueue: Queue,\n\tvisible: bool,\n\tkey_config: SharedKeyConfig,\n\tsender: Sender<AsyncGitNotification>,\n\ttheme: SharedTheme,\n}\n\nimpl Revlog {\n\t///\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\trepo: env.repo.clone(),\n\t\t\tqueue: env.queue.clone(),\n\t\t\tcommit_details: CommitDetailsComponent::new(env),\n\t\t\tlist: CommitList::new(\n\t\t\t\tenv,\n\t\t\t\t&strings::log_title(&env.key_config),\n\t\t\t),\n\t\t\tgit_log: AsyncLog::new(\n\t\t\t\tenv.repo.borrow().clone(),\n\t\t\t\t&env.sender_git,\n\t\t\t\tNone,\n\t\t\t),\n\t\t\tsearch: LogSearch::Off,\n\t\t\tgit_tags: AsyncTags::new(\n\t\t\t\tenv.repo.borrow().clone(),\n\t\t\t\t&env.sender_git,\n\t\t\t),\n\t\t\tgit_local_branches: AsyncSingleJob::new(\n\t\t\t\tenv.sender_git.clone(),\n\t\t\t),\n\t\t\tgit_remote_branches: AsyncSingleJob::new(\n\t\t\t\tenv.sender_git.clone(),\n\t\t\t),\n\t\t\tvisible: false,\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\tsender: env.sender_git.clone(),\n\t\t\ttheme: env.theme.clone(),\n\t\t}\n\t}\n\n\t///\n\tpub fn any_work_pending(&self) -> bool {\n\t\tself.git_log.is_pending()\n\t\t\t|| self.is_search_pending()\n\t\t\t|| self.git_tags.is_pending()\n\t\t\t|| self.git_local_branches.is_pending()\n\t\t\t|| self.git_remote_branches.is_pending()\n\t\t\t|| self.commit_details.any_work_pending()\n\t}\n\n\tconst fn is_search_pending(&self) -> bool {\n\t\tmatches!(self.search, LogSearch::Searching(_, _, _, _))\n\t}\n\n\t///\n\tpub fn update(&mut self) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tif self.git_log.fetch()? == FetchStatus::Started {\n\t\t\t\tself.list.clear();\n\t\t\t}\n\n\t\t\tself.list\n\t\t\t\t.refresh_extend_data(self.git_log.extract_items()?);\n\n\t\t\tself.git_tags.request(Duration::from_secs(3), false)?;\n\n\t\t\tif self.commit_details.is_visible() {\n\t\t\t\tlet commit = self.selected_commit();\n\t\t\t\tlet tags = self.selected_commit_tags(commit.as_ref());\n\n\t\t\t\tself.commit_details.set_commits(\n\t\t\t\t\tcommit.map(CommitFilesParams::from),\n\t\t\t\t\ttags.as_ref(),\n\t\t\t\t)?;\n\t\t\t}\n\t\t}\n\n\t\tOk(())\n\t}\n\n\t///\n\tpub fn update_git(\n\t\t&mut self,\n\t\tev: AsyncGitNotification,\n\t) -> Result<()> {\n\t\tif self.visible {\n\t\t\tmatch ev {\n\t\t\t\tAsyncGitNotification::CommitFiles\n\t\t\t\t| AsyncGitNotification::Log => self.update()?,\n\t\t\t\tAsyncGitNotification::CommitFilter => {\n\t\t\t\t\tself.update_search_state();\n\t\t\t\t}\n\t\t\t\tAsyncGitNotification::Tags => {\n\t\t\t\t\tif let Some(tags) = self.git_tags.last()? {\n\t\t\t\t\t\tself.list.set_tags(tags);\n\t\t\t\t\t\tself.update()?;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tAsyncGitNotification::Branches => {\n\t\t\t\t\tif let Some(local_branches) =\n\t\t\t\t\t\tself.git_local_branches.take_last()\n\t\t\t\t\t{\n\t\t\t\t\t\tif let Some(Ok(local_branches)) =\n\t\t\t\t\t\t\tlocal_branches.result()\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tself.list\n\t\t\t\t\t\t\t\t.set_local_branches(local_branches);\n\t\t\t\t\t\t\tself.update()?;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif let Some(remote_branches) =\n\t\t\t\t\t\tself.git_remote_branches.take_last()\n\t\t\t\t\t{\n\t\t\t\t\t\tif let Some(Ok(remote_branches)) =\n\t\t\t\t\t\t\tremote_branches.result()\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tself.list\n\t\t\t\t\t\t\t\t.set_remote_branches(remote_branches);\n\t\t\t\t\t\t\tself.update()?;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t_ => (),\n\t\t\t}\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn selected_commit(&self) -> Option<CommitId> {\n\t\tself.list.selected_entry().map(|e| e.id)\n\t}\n\n\tfn selected_commit_tags(\n\t\t&self,\n\t\tcommit: Option<&CommitId>,\n\t) -> Option<CommitTags> {\n\t\tlet tags = self.list.tags();\n\n\t\tcommit.and_then(|commit| {\n\t\t\ttags.and_then(|tags| tags.get(commit).cloned())\n\t\t})\n\t}\n\n\t///\n\tpub fn select_commit(&mut self, id: CommitId) -> Result<()> {\n\t\tself.list.select_commit(id)\n\t}\n\n\tfn revert_commit(&self) -> Result<()> {\n\t\tif let Some(c) = self.selected_commit() {\n\t\t\tsync::revert_commit(&self.repo.borrow(), c)?;\n\t\t\tself.queue.push(InternalEvent::TabSwitchStatus);\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn inspect_commit(&self) {\n\t\tif let Some(commit_id) = self.selected_commit() {\n\t\t\tlet tags =\n\t\t\t\tself.selected_commit_tags(Some(commit_id).as_ref());\n\t\t\tself.queue.push(InternalEvent::OpenPopup(\n\t\t\t\tStackablePopupOpen::InspectCommit(\n\t\t\t\t\tInspectCommitOpen::new_with_tags(commit_id, tags),\n\t\t\t\t),\n\t\t\t));\n\t\t}\n\t}\n\n\tpub fn search(&mut self, options: LogFilterSearchOptions) {\n\t\tif !self.can_start_search() {\n\t\t\treturn;\n\t\t}\n\n\t\tif matches!(\n\t\t\tself.search,\n\t\t\tLogSearch::Off | LogSearch::Results(_)\n\t\t) {\n\t\t\tlog::info!(\"start search: {options:?}\");\n\n\t\t\tlet filter = filter_commit_by_search(\n\t\t\t\tLogFilterSearch::new(options.clone()),\n\t\t\t);\n\n\t\t\tlet cancellation_flag = Arc::new(AtomicBool::new(false));\n\n\t\t\tlet job = AsyncSingleJob::new(self.sender.clone());\n\t\t\tjob.spawn(AsyncCommitFilterJob::new(\n\t\t\t\tself.repo.borrow().clone(),\n\t\t\t\tself.list.copy_items(),\n\t\t\t\tfilter,\n\t\t\t\tArc::clone(&cancellation_flag),\n\t\t\t));\n\n\t\t\tself.search = LogSearch::Searching(\n\t\t\t\tjob,\n\t\t\t\toptions,\n\t\t\t\tNone,\n\t\t\t\tArc::clone(&cancellation_flag),\n\t\t\t);\n\n\t\t\tself.list.set_highlighting(None);\n\t\t}\n\t}\n\n\tfn cancel_search(&mut self) -> bool {\n\t\tif let LogSearch::Searching(_, _, _, cancellation_flag) =\n\t\t\t&self.search\n\t\t{\n\t\t\tcancellation_flag.store(true, Ordering::Relaxed);\n\t\t\tself.list.set_highlighting(None);\n\t\t\treturn true;\n\t\t}\n\n\t\tfalse\n\t}\n\n\tfn update_search_state(&mut self) {\n\t\tmatch &mut self.search {\n\t\t\tLogSearch::Off | LogSearch::Results(_) => (),\n\t\t\tLogSearch::Searching(\n\t\t\t\tsearch,\n\t\t\t\toptions,\n\t\t\t\tprogress,\n\t\t\t\tcancel,\n\t\t\t) => {\n\t\t\t\tif search.is_pending() {\n\t\t\t\t\t//update progress\n\t\t\t\t\t*progress = search.progress();\n\t\t\t\t} else if let Some(search) = search\n\t\t\t\t\t.take_last()\n\t\t\t\t\t.and_then(|search| search.result())\n\t\t\t\t{\n\t\t\t\t\tmatch search {\n\t\t\t\t\t\tOk(search) => {\n\t\t\t\t\t\t\tlet was_aborted =\n\t\t\t\t\t\t\t\tcancel.load(Ordering::Relaxed);\n\n\t\t\t\t\t\t\tself.search = if was_aborted {\n\t\t\t\t\t\t\t\tLogSearch::Off\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tself.list.set_highlighting(Some(\n\t\t\t\t\t\t\t\t\tRc::new(\n\t\t\t\t\t\t\t\t\t\tsearch\n\t\t\t\t\t\t\t\t\t\t\t.result\n\t\t\t\t\t\t\t\t\t\t\t.into_iter()\n\t\t\t\t\t\t\t\t\t\t\t.collect::<IndexSet<_>>(),\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t));\n\n\t\t\t\t\t\t\t\tLogSearch::Results(LogSearchResult {\n\t\t\t\t\t\t\t\t\toptions: options.clone(),\n\t\t\t\t\t\t\t\t\tduration: search.duration,\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t\tErr(err) => {\n\t\t\t\t\t\t\tself.queue.push(\n\t\t\t\t\t\t\t\tInternalEvent::ShowErrorMsg(format!(\n\t\t\t\t\t\t\t\t\t\"search error: {err}\",\n\t\t\t\t\t\t\t\t)),\n\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t\tself.search = LogSearch::Off;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tconst fn is_in_search_mode(&self) -> bool {\n\t\t!matches!(self.search, LogSearch::Off)\n\t}\n\n\tfn draw_search(&self, f: &mut Frame, area: Rect) {\n\t\tlet (text, title) = match &self.search {\n\t\t\tLogSearch::Searching(_, options, progress, _) => (\n\t\t\t\tformat!(\"'{}'\", options.search_pattern.clone()),\n\t\t\t\tformat!(\n\t\t\t\t\t\"({}%)\",\n\t\t\t\t\tprogress\n\t\t\t\t\t\t.map(|progress| progress.progress)\n\t\t\t\t\t\t.unwrap_or_default()\n\t\t\t\t),\n\t\t\t),\n\t\t\tLogSearch::Results(results) => {\n\t\t\t\tlet info = self.list.highlighted_selection_info();\n\n\t\t\t\t(\n\t\t\t\t\tformat!(\n\t\t\t\t\t\t\"'{}' (duration: {:?})\",\n\t\t\t\t\t\tresults.options.search_pattern.clone(),\n\t\t\t\t\t\tresults.duration,\n\t\t\t\t\t),\n\t\t\t\t\tformat!(\n\t\t\t\t\t\t\"({}/{})\",\n\t\t\t\t\t\t(info.0 + 1).min(info.1),\n\t\t\t\t\t\tinfo.1\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t}\n\t\t\tLogSearch::Off => (String::new(), String::new()),\n\t\t};\n\n\t\tf.render_widget(\n\t\t\tParagraph::new(text)\n\t\t\t\t.block(\n\t\t\t\t\tBlock::default()\n\t\t\t\t\t\t.title(Span::styled(\n\t\t\t\t\t\t\tformat!(\n\t\t\t\t\t\t\t\t\"{} {}\",\n\t\t\t\t\t\t\t\tstrings::POPUP_TITLE_LOG_SEARCH,\n\t\t\t\t\t\t\t\ttitle\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tself.theme.title(true),\n\t\t\t\t\t\t))\n\t\t\t\t\t\t.borders(Borders::ALL)\n\t\t\t\t\t\t.border_style(Theme::attention_block()),\n\t\t\t\t)\n\t\t\t\t.alignment(Alignment::Left),\n\t\t\tarea,\n\t\t);\n\t}\n\n\tconst fn can_close_search(&self) -> bool {\n\t\tself.is_in_search_mode() && !self.is_search_pending()\n\t}\n\n\tfn can_start_search(&self) -> bool {\n\t\t!self.git_log.is_pending() && !self.is_search_pending()\n\t}\n}\n\nimpl DrawableComponent for Revlog {\n\tfn draw(&self, f: &mut Frame, area: Rect) -> Result<()> {\n\t\tlet area = if self.is_in_search_mode() {\n\t\t\tLayout::default()\n\t\t\t\t.direction(Direction::Vertical)\n\t\t\t\t.constraints(\n\t\t\t\t\t[Constraint::Min(1), Constraint::Length(3)]\n\t\t\t\t\t\t.as_ref(),\n\t\t\t\t)\n\t\t\t\t.split(area)\n\t\t} else {\n\t\t\tRc::new([area])\n\t\t};\n\n\t\tlet chunks = Layout::default()\n\t\t\t.direction(Direction::Horizontal)\n\t\t\t.constraints(\n\t\t\t\t[\n\t\t\t\t\tConstraint::Percentage(60),\n\t\t\t\t\tConstraint::Percentage(40),\n\t\t\t\t]\n\t\t\t\t.as_ref(),\n\t\t\t)\n\t\t\t.split(area[0]);\n\n\t\tif self.commit_details.is_visible() {\n\t\t\tself.list.draw(f, chunks[0])?;\n\t\t\tself.commit_details.draw(f, chunks[1])?;\n\t\t} else {\n\t\t\tself.list.draw(f, area[0])?;\n\t\t}\n\n\t\tif self.is_in_search_mode() {\n\t\t\tself.draw_search(f, area[1]);\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for Revlog {\n\t//TODO: cleanup\n\t#[allow(clippy::too_many_lines)]\n\tfn event(&mut self, ev: &Event) -> Result<EventState> {\n\t\tif self.visible {\n\t\t\tlet event_used = self.list.event(ev)?;\n\n\t\t\tif event_used.is_consumed() {\n\t\t\t\tself.update()?;\n\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t} else if let Event::Key(k) = ev {\n\t\t\t\tif key_match(k, self.key_config.keys.enter) {\n\t\t\t\t\tself.commit_details.toggle_visible()?;\n\t\t\t\t\tself.update()?;\n\t\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.exit_popup,\n\t\t\t\t) {\n\t\t\t\t\tif self.is_search_pending() {\n\t\t\t\t\t\tself.cancel_search();\n\t\t\t\t\t} else if self.can_close_search() {\n\t\t\t\t\t\tself.list.set_highlighting(None);\n\t\t\t\t\t\tself.search = LogSearch::Off;\n\t\t\t\t\t}\n\t\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t\t} else if key_match(k, self.key_config.keys.copy) {\n\t\t\t\t\ttry_or_popup!(\n\t\t\t\t\t\tself,\n\t\t\t\t\t\tstrings::POPUP_FAIL_COPY,\n\t\t\t\t\t\tself.list.copy_commit_hash()\n\t\t\t\t\t);\n\t\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t\t} else if key_match(k, self.key_config.keys.push) {\n\t\t\t\t\tself.queue.push(InternalEvent::PushTags);\n\t\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.log_tag_commit,\n\t\t\t\t) {\n\t\t\t\t\treturn self.selected_commit().map_or(\n\t\t\t\t\t\tOk(EventState::NotConsumed),\n\t\t\t\t\t\t|id| {\n\t\t\t\t\t\t\tself.queue\n\t\t\t\t\t\t\t\t.push(InternalEvent::TagCommit(id));\n\t\t\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t\t\t},\n\t\t\t\t\t);\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.move_right,\n\t\t\t\t) && self.commit_details.is_visible()\n\t\t\t\t{\n\t\t\t\t\tself.inspect_commit();\n\t\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.select_branch,\n\t\t\t\t) && !self.is_search_pending()\n\t\t\t\t{\n\t\t\t\t\tself.queue.push(InternalEvent::SelectBranch);\n\t\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.status_reset_item,\n\t\t\t\t) && !self.is_search_pending()\n\t\t\t\t{\n\t\t\t\t\ttry_or_popup!(\n\t\t\t\t\t\tself,\n\t\t\t\t\t\t\"revert error:\",\n\t\t\t\t\t\tself.revert_commit()\n\t\t\t\t\t);\n\n\t\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.open_file_tree,\n\t\t\t\t) && !self.is_search_pending()\n\t\t\t\t{\n\t\t\t\t\treturn self.selected_commit().map_or(\n\t\t\t\t\t\tOk(EventState::NotConsumed),\n\t\t\t\t\t\t|id| {\n\t\t\t\t\t\t\tself.queue.push(\n\t\t\t\t\t\t\t\tInternalEvent::OpenPopup(\n\t\t\t\t\t\t\t\t\tStackablePopupOpen::FileTree(\n\t\t\t\t\t\t\t\t\t\tFileTreeOpen::new(id),\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t\t\t},\n\t\t\t\t\t);\n\t\t\t\t} else if key_match(k, self.key_config.keys.tags) {\n\t\t\t\t\tself.queue.push(InternalEvent::Tags);\n\t\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.log_reset_commit,\n\t\t\t\t) && !self.is_search_pending()\n\t\t\t\t{\n\t\t\t\t\treturn self.selected_commit().map_or(\n\t\t\t\t\t\tOk(EventState::NotConsumed),\n\t\t\t\t\t\t|id| {\n\t\t\t\t\t\t\tself.queue.push(\n\t\t\t\t\t\t\t\tInternalEvent::OpenResetPopup(id),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t\t\t},\n\t\t\t\t\t);\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.log_reword_commit,\n\t\t\t\t) && !self.is_search_pending()\n\t\t\t\t{\n\t\t\t\t\treturn self.selected_commit().map_or(\n\t\t\t\t\t\tOk(EventState::NotConsumed),\n\t\t\t\t\t\t|id| {\n\t\t\t\t\t\t\tself.queue.push(\n\t\t\t\t\t\t\t\tInternalEvent::RewordCommit(id),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t\t\t},\n\t\t\t\t\t);\n\t\t\t\t} else if key_match(k, self.key_config.keys.log_find)\n\t\t\t\t\t&& self.can_start_search()\n\t\t\t\t{\n\t\t\t\t\tself.queue\n\t\t\t\t\t\t.push(InternalEvent::OpenLogSearchPopup);\n\t\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.compare_commits,\n\t\t\t\t) && self.list.marked_count() > 0\n\t\t\t\t\t&& !self.is_search_pending()\n\t\t\t\t{\n\t\t\t\t\tif self.list.marked_count() == 1 {\n\t\t\t\t\t\t// compare against head\n\t\t\t\t\t\tself.queue.push(InternalEvent::OpenPopup(\n\t\t\t\t\t\t\tStackablePopupOpen::CompareCommits(\n\t\t\t\t\t\t\t\tInspectCommitOpen::new(\n\t\t\t\t\t\t\t\t\tself.list.marked_commits()[0],\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t));\n\t\t\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t\t\t} else if self.list.marked_count() == 2 {\n\t\t\t\t\t\t//compare two marked commits\n\t\t\t\t\t\tlet marked = self.list.marked_commits();\n\t\t\t\t\t\tself.queue.push(InternalEvent::OpenPopup(\n\t\t\t\t\t\t\tStackablePopupOpen::CompareCommits(\n\t\t\t\t\t\t\t\tInspectCommitOpen {\n\t\t\t\t\t\t\t\t\tcommit_id: marked[0],\n\t\t\t\t\t\t\t\t\tcompare_id: Some(marked[1]),\n\t\t\t\t\t\t\t\t\ttags: None,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t));\n\t\t\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.visible || force_all {\n\t\t\tself.list.commands(out, force_all);\n\t\t}\n\n\t\tout.push(\n\t\t\tCommandInfo::new(\n\t\t\t\tstrings::commands::log_close_search(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\t(self.visible\n\t\t\t\t\t&& (self.can_close_search()\n\t\t\t\t\t\t|| self.is_search_pending()))\n\t\t\t\t\t|| force_all,\n\t\t\t)\n\t\t\t.order(order::PRIORITY),\n\t\t);\n\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::log_details_toggle(&self.key_config),\n\t\t\ttrue,\n\t\t\tself.visible,\n\t\t));\n\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::commit_details_open(&self.key_config),\n\t\t\ttrue,\n\t\t\t(self.visible && self.commit_details.is_visible())\n\t\t\t\t|| force_all,\n\t\t));\n\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::open_branch_select_popup(\n\t\t\t\t&self.key_config,\n\t\t\t),\n\t\t\ttrue,\n\t\t\t(self.visible && !self.is_search_pending()) || force_all,\n\t\t));\n\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::compare_with_head(&self.key_config),\n\t\t\tself.list.marked_count() == 1,\n\t\t\t(self.visible\n\t\t\t\t&& !self.is_search_pending()\n\t\t\t\t&& self.list.marked_count() <= 1)\n\t\t\t\t|| force_all,\n\t\t));\n\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::compare_commits(&self.key_config),\n\t\t\ttrue,\n\t\t\t(self.visible\n\t\t\t\t&& !self.is_search_pending()\n\t\t\t\t&& self.list.marked_count() == 2)\n\t\t\t\t|| force_all,\n\t\t));\n\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::copy_hash(&self.key_config),\n\t\t\tself.selected_commit().is_some(),\n\t\t\tself.visible || force_all,\n\t\t));\n\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::log_tag_commit(&self.key_config),\n\t\t\tself.selected_commit().is_some(),\n\t\t\tself.visible || force_all,\n\t\t));\n\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::log_checkout_commit(&self.key_config),\n\t\t\tself.selected_commit().is_some(),\n\t\t\tself.visible || force_all,\n\t\t));\n\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::open_tags_popup(&self.key_config),\n\t\t\ttrue,\n\t\t\tself.visible || force_all,\n\t\t));\n\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::push_tags(&self.key_config),\n\t\t\ttrue,\n\t\t\tself.visible || force_all,\n\t\t));\n\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::inspect_file_tree(&self.key_config),\n\t\t\tself.selected_commit().is_some(),\n\t\t\t(self.visible && !self.is_search_pending()) || force_all,\n\t\t));\n\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::revert_commit(&self.key_config),\n\t\t\tself.selected_commit().is_some(),\n\t\t\t(self.visible && !self.is_search_pending()) || force_all,\n\t\t));\n\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::log_reset_commit(&self.key_config),\n\t\t\tself.selected_commit().is_some(),\n\t\t\t(self.visible && !self.is_search_pending()) || force_all,\n\t\t));\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::log_reword_commit(&self.key_config),\n\t\t\tself.selected_commit().is_some(),\n\t\t\t(self.visible && !self.is_search_pending()) || force_all,\n\t\t));\n\t\tout.push(CommandInfo::new(\n\t\t\tstrings::commands::log_find_commit(&self.key_config),\n\t\t\tself.can_start_search(),\n\t\t\tself.visible || force_all,\n\t\t));\n\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.visible = false;\n\t\tself.git_log.set_background();\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\n\t\tself.git_local_branches.spawn(AsyncBranchesJob::new(\n\t\t\tself.repo.borrow().clone(),\n\t\t\ttrue,\n\t\t));\n\n\t\tself.git_remote_branches.spawn(AsyncBranchesJob::new(\n\t\t\tself.repo.borrow().clone(),\n\t\t\tfalse,\n\t\t));\n\n\t\tself.update()?;\n\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "src/tabs/stashing.rs",
    "content": "use crate::{\n\taccessors,\n\tapp::Environment,\n\tcomponents::{\n\t\tcommand_pump, event_pump, visibility_blocking,\n\t\tCommandBlocking, CommandInfo, Component, DrawableComponent,\n\t\tEventState, StatusTreeComponent,\n\t},\n\tkeys::{key_match, SharedKeyConfig},\n\tqueue::{InternalEvent, Queue},\n\tstrings,\n\tui::style::SharedTheme,\n};\nuse anyhow::Result;\nuse asyncgit::{\n\tsync::{self, status::StatusType, RepoPathRef},\n\tAsyncGitNotification, AsyncStatus, StatusParams,\n};\nuse crossterm::event::Event;\nuse ratatui::{\n\tlayout::{Alignment, Constraint, Direction, Layout},\n\ttext::{Line, Span},\n\twidgets::{Block, Borders, Paragraph},\n};\nuse std::borrow::Cow;\n\n#[derive(Default, Clone, Copy, Debug)]\npub struct StashingOptions {\n\tpub stash_untracked: bool,\n\tpub keep_index: bool,\n}\n\npub struct Stashing {\n\trepo: RepoPathRef,\n\tindex: StatusTreeComponent,\n\tvisible: bool,\n\toptions: StashingOptions,\n\ttheme: SharedTheme,\n\tgit_status: AsyncStatus,\n\tqueue: Queue,\n\tkey_config: SharedKeyConfig,\n}\n\nimpl Stashing {\n\taccessors!(self, [index]);\n\n\t///\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\trepo: env.repo.clone(),\n\t\t\tindex: StatusTreeComponent::new(\n\t\t\t\tenv,\n\t\t\t\t&strings::stashing_files_title(&env.key_config),\n\t\t\t\ttrue,\n\t\t\t),\n\t\t\tvisible: false,\n\t\t\toptions: StashingOptions {\n\t\t\t\tkeep_index: false,\n\t\t\t\tstash_untracked: true,\n\t\t\t},\n\t\t\ttheme: env.theme.clone(),\n\t\t\tgit_status: AsyncStatus::new(\n\t\t\t\tenv.repo.borrow().clone(),\n\t\t\t\tenv.sender_git.clone(),\n\t\t\t),\n\t\t\tqueue: env.queue.clone(),\n\t\t\tkey_config: env.key_config.clone(),\n\t\t}\n\t}\n\n\t///\n\tpub fn update(&self) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tself.git_status\n\t\t\t\t//TODO: support options\n\t\t\t\t.fetch(&StatusParams::new(StatusType::Both, None))?;\n\t\t}\n\n\t\tOk(())\n\t}\n\n\t///\n\tpub fn anything_pending(&self) -> bool {\n\t\tself.git_status.is_pending()\n\t}\n\n\t///\n\tpub fn update_git(\n\t\t&mut self,\n\t\tev: AsyncGitNotification,\n\t) -> Result<()> {\n\t\tif self.is_visible() && ev == AsyncGitNotification::Status {\n\t\t\tlet status = self.git_status.last()?;\n\t\t\tself.index.show()?;\n\t\t\tself.index.update(&status.items)?;\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn get_option_text(&self) -> Vec<Line<'_>> {\n\t\tlet bracket_open = Span::raw(Cow::from(\"[\"));\n\t\tlet bracket_close = Span::raw(Cow::from(\"]\"));\n\t\tlet option_on =\n\t\t\tSpan::styled(Cow::from(\"x\"), self.theme.option(true));\n\n\t\tlet option_off =\n\t\t\tSpan::styled(Cow::from(\"_\"), self.theme.option(false));\n\n\t\tvec![\n\t\t\tLine::from(vec![\n\t\t\t\tbracket_open.clone(),\n\t\t\t\tif self.options.stash_untracked {\n\t\t\t\t\toption_on.clone()\n\t\t\t\t} else {\n\t\t\t\t\toption_off.clone()\n\t\t\t\t},\n\t\t\t\tbracket_close.clone(),\n\t\t\t\tSpan::raw(Cow::from(\" stash untracked\")),\n\t\t\t]),\n\t\t\tLine::from(vec![\n\t\t\t\tbracket_open,\n\t\t\t\tif self.options.keep_index {\n\t\t\t\t\toption_on\n\t\t\t\t} else {\n\t\t\t\t\toption_off\n\t\t\t\t},\n\t\t\t\tbracket_close,\n\t\t\t\tSpan::raw(Cow::from(\" keep index\")),\n\t\t\t]),\n\t\t]\n\t}\n}\n\nimpl DrawableComponent for Stashing {\n\tfn draw(\n\t\t&self,\n\t\tf: &mut ratatui::Frame,\n\t\trect: ratatui::layout::Rect,\n\t) -> Result<()> {\n\t\tlet chunks = Layout::default()\n\t\t\t.direction(Direction::Horizontal)\n\t\t\t.constraints(\n\t\t\t\t[Constraint::Min(1), Constraint::Length(22)].as_ref(),\n\t\t\t)\n\t\t\t.split(rect);\n\n\t\tlet right_chunks = Layout::default()\n\t\t\t.direction(Direction::Vertical)\n\t\t\t.constraints(\n\t\t\t\t[Constraint::Length(4), Constraint::Min(1)].as_ref(),\n\t\t\t)\n\t\t\t.split(chunks[1]);\n\n\t\tf.render_widget(\n\t\t\tParagraph::new(self.get_option_text())\n\t\t\t\t.block(Block::default().borders(Borders::ALL).title(\n\t\t\t\t\tstrings::stashing_options_title(&self.key_config),\n\t\t\t\t))\n\t\t\t\t.alignment(Alignment::Left),\n\t\t\tright_chunks[0],\n\t\t);\n\n\t\tself.index.draw(f, chunks[0])?;\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for Stashing {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.visible || force_all {\n\t\t\tcommand_pump(\n\t\t\t\tout,\n\t\t\t\tforce_all,\n\t\t\t\tself.components().as_slice(),\n\t\t\t);\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::stashing_save(&self.key_config),\n\t\t\t\tself.visible && !self.index.is_empty(),\n\t\t\t\tself.visible || force_all,\n\t\t\t));\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::stashing_toggle_indexed(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t),\n\t\t\t\tself.visible,\n\t\t\t\tself.visible || force_all,\n\t\t\t));\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::stashing_toggle_untracked(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t),\n\t\t\t\tself.visible,\n\t\t\t\tself.visible || force_all,\n\t\t\t));\n\t\t}\n\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(\n\t\t&mut self,\n\t\tev: &crossterm::event::Event,\n\t) -> Result<EventState> {\n\t\tif self.visible {\n\t\t\tif event_pump(ev, self.components_mut().as_mut_slice())?\n\t\t\t\t.is_consumed()\n\t\t\t{\n\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t}\n\n\t\t\tif let Event::Key(k) = ev {\n\t\t\t\treturn if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.stashing_save,\n\t\t\t\t) && !self.index.is_empty()\n\t\t\t\t{\n\t\t\t\t\tself.queue.push(InternalEvent::PopupStashing(\n\t\t\t\t\t\tself.options,\n\t\t\t\t\t));\n\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.stashing_toggle_index,\n\t\t\t\t) {\n\t\t\t\t\tself.options.keep_index =\n\t\t\t\t\t\t!self.options.keep_index;\n\t\t\t\t\tself.update()?;\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.stashing_toggle_untracked,\n\t\t\t\t) {\n\t\t\t\t\tself.options.stash_untracked =\n\t\t\t\t\t\t!self.options.stash_untracked;\n\t\t\t\t\tself.update()?;\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else {\n\t\t\t\t\tOk(EventState::NotConsumed)\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.visible = false;\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tlet config_untracked_files =\n\t\t\tsync::untracked_files_config(&self.repo.borrow())?;\n\n\t\tself.options.stash_untracked =\n\t\t\t!config_untracked_files.include_none();\n\n\t\tself.index.show()?;\n\t\tself.visible = true;\n\t\tself.update()?;\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "src/tabs/stashlist.rs",
    "content": "use crate::{\n\tapp::Environment,\n\tcomponents::{\n\t\tvisibility_blocking, CommandBlocking, CommandInfo,\n\t\tCommitList, Component, DrawableComponent, EventState,\n\t},\n\tkeys::{key_match, SharedKeyConfig},\n\tpopups::InspectCommitOpen,\n\tqueue::{Action, InternalEvent, Queue, StackablePopupOpen},\n\tstrings,\n};\nuse anyhow::Result;\nuse asyncgit::sync::{self, CommitId, RepoPath, RepoPathRef};\nuse crossterm::event::Event;\n\npub struct StashList {\n\trepo: RepoPathRef,\n\tlist: CommitList,\n\tvisible: bool,\n\tqueue: Queue,\n\tkey_config: SharedKeyConfig,\n}\n\nimpl StashList {\n\t///\n\tpub fn new(env: &Environment) -> Self {\n\t\tSelf {\n\t\t\tvisible: false,\n\t\t\tlist: CommitList::new(\n\t\t\t\tenv,\n\t\t\t\t&strings::stashlist_title(&env.key_config),\n\t\t\t),\n\t\t\tqueue: env.queue.clone(),\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\trepo: env.repo.clone(),\n\t\t}\n\t}\n\n\t///\n\tpub fn update(&mut self) -> Result<()> {\n\t\tif self.is_visible() {\n\t\t\tlet stashes = sync::get_stashes(&self.repo.borrow())?;\n\t\t\tself.list.set_commits(stashes.into_iter().collect());\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn apply_stash(&self) {\n\t\tif let Some(e) = self.list.selected_entry() {\n\t\t\tmatch sync::stash_apply(&self.repo.borrow(), e.id, false)\n\t\t\t{\n\t\t\t\tOk(()) => {\n\t\t\t\t\tself.queue.push(InternalEvent::TabSwitchStatus);\n\t\t\t\t}\n\t\t\t\tErr(e) => {\n\t\t\t\t\tself.queue.push(InternalEvent::ShowErrorMsg(\n\t\t\t\t\t\tformat!(\"stash apply error:\\n{e}\"),\n\t\t\t\t\t));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfn drop_stash(&self) {\n\t\tif self.list.marked_count() > 0 {\n\t\t\tself.queue.push(InternalEvent::ConfirmAction(\n\t\t\t\tAction::StashDrop(self.list.marked_commits()),\n\t\t\t));\n\t\t} else if let Some(e) = self.list.selected_entry() {\n\t\t\tself.queue.push(InternalEvent::ConfirmAction(\n\t\t\t\tAction::StashDrop(vec![e.id]),\n\t\t\t));\n\t\t}\n\t}\n\n\tfn pop_stash(&self) {\n\t\tif let Some(e) = self.list.selected_entry() {\n\t\t\tself.queue.push(InternalEvent::ConfirmAction(\n\t\t\t\tAction::StashPop(e.id),\n\t\t\t));\n\t\t}\n\t}\n\n\tfn inspect(&self) {\n\t\tif let Some(e) = self.list.selected_entry() {\n\t\t\tself.queue.push(InternalEvent::OpenPopup(\n\t\t\t\tStackablePopupOpen::InspectCommit(\n\t\t\t\t\tInspectCommitOpen::new(e.id),\n\t\t\t\t),\n\t\t\t));\n\t\t}\n\t}\n\n\t/// Called when a pending stash action has been confirmed\n\tpub fn action_confirmed(\n\t\t&mut self,\n\t\trepo: &RepoPath,\n\t\taction: &Action,\n\t) -> Result<()> {\n\t\tmatch action {\n\t\t\tAction::StashDrop(ids) => self.drop(repo, ids)?,\n\t\t\tAction::StashPop(id) => self.pop(repo, *id)?,\n\t\t\t_ => (),\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn drop(\n\t\t&mut self,\n\t\trepo: &RepoPath,\n\t\tids: &[CommitId],\n\t) -> Result<()> {\n\t\tfor id in ids {\n\t\t\tsync::stash_drop(repo, *id)?;\n\t\t}\n\n\t\tself.list.clear_marked();\n\t\tself.update()?;\n\n\t\tOk(())\n\t}\n\n\tfn pop(&mut self, repo: &RepoPath, id: CommitId) -> Result<()> {\n\t\tsync::stash_pop(repo, id)?;\n\n\t\tself.list.clear_marked();\n\t\tself.update()?;\n\n\t\tself.queue.push(InternalEvent::TabSwitchStatus);\n\n\t\tOk(())\n\t}\n}\n\nimpl DrawableComponent for StashList {\n\tfn draw(\n\t\t&self,\n\t\tf: &mut ratatui::Frame,\n\t\trect: ratatui::layout::Rect,\n\t) -> Result<()> {\n\t\tself.list.draw(f, rect)?;\n\n\t\tOk(())\n\t}\n}\n\nimpl Component for StashList {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tif self.visible || force_all {\n\t\t\tself.list.commands(out, force_all);\n\n\t\t\tlet selection_valid =\n\t\t\t\tself.list.selected_entry().is_some();\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::stashlist_pop(&self.key_config),\n\t\t\t\tselection_valid,\n\t\t\t\ttrue,\n\t\t\t));\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::stashlist_apply(&self.key_config),\n\t\t\t\tselection_valid,\n\t\t\t\ttrue,\n\t\t\t));\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::stashlist_drop(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t\tself.list.marked_count(),\n\t\t\t\t),\n\t\t\t\tselection_valid,\n\t\t\t\ttrue,\n\t\t\t));\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::stashlist_inspect(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t),\n\t\t\t\tselection_valid,\n\t\t\t\ttrue,\n\t\t\t));\n\t\t}\n\n\t\tvisibility_blocking(self)\n\t}\n\n\tfn event(\n\t\t&mut self,\n\t\tev: &crossterm::event::Event,\n\t) -> Result<EventState> {\n\t\tif self.is_visible() {\n\t\t\tif self.list.event(ev)?.is_consumed() {\n\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t}\n\n\t\t\tif let Event::Key(k) = ev {\n\t\t\t\tif key_match(k, self.key_config.keys.enter) {\n\t\t\t\t\tself.pop_stash();\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.stash_apply,\n\t\t\t\t) {\n\t\t\t\t\tself.apply_stash();\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.stash_drop,\n\t\t\t\t) {\n\t\t\t\t\tself.drop_stash();\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.stash_open,\n\t\t\t\t) {\n\t\t\t\t\tself.inspect();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.visible = false;\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\t\tself.update()?;\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "src/tabs/status.rs",
    "content": "use crate::{\n\taccessors,\n\tapp::Environment,\n\tcomponents::{\n\t\tcommand_pump, event_pump, visibility_blocking,\n\t\tChangesComponent, CommandBlocking, CommandInfo, Component,\n\t\tDiffComponent, DrawableComponent, EventState,\n\t\tFileTreeItemKind,\n\t},\n\tkeys::{key_match, SharedKeyConfig},\n\toptions::SharedOptions,\n\tqueue::{Action, InternalEvent, NeedsUpdate, Queue, ResetItem},\n\tstrings, try_or_popup,\n\tui::style::Theme,\n};\nuse anyhow::Result;\nuse asyncgit::{\n\tcached,\n\tsync::{\n\t\tself, status::StatusType, RepoPath, RepoPathRef, RepoState,\n\t},\n\tsync::{BranchCompare, CommitId},\n\tAsyncDiff, AsyncGitNotification, AsyncStatus, DiffParams,\n\tDiffType, PushType, StatusItem, StatusParams,\n};\nuse crossterm::event::Event;\nuse itertools::Itertools;\nuse ratatui::{\n\tlayout::{Alignment, Constraint, Direction, Layout},\n\tstyle::{Color, Style},\n\twidgets::{Block, BorderType, Borders, Paragraph},\n};\n\n/// what part of the screen is focused\n#[derive(PartialEq)]\nenum Focus {\n\tWorkDir,\n\tDiff,\n\tStage,\n}\n\n/// focus can toggle between workdir and stage\nimpl Focus {\n\tconst fn toggled_focus(&self) -> Self {\n\t\tmatch self {\n\t\t\tSelf::WorkDir => Self::Stage,\n\t\t\tSelf::Stage => Self::WorkDir,\n\t\t\tSelf::Diff => Self::Diff,\n\t\t}\n\t}\n}\n\n/// which target are we showing a diff against\n#[derive(PartialEq, Copy, Clone)]\nenum DiffTarget {\n\tStage,\n\tWorkingDir,\n}\n\nstruct RemoteStatus {\n\thas_remote_for_fetch: bool,\n\thas_remote_for_push: bool,\n}\n\npub struct Status {\n\trepo: RepoPathRef,\n\tvisible: bool,\n\tfocus: Focus,\n\tdiff_target: DiffTarget,\n\tindex: ChangesComponent,\n\tindex_wd: ChangesComponent,\n\tdiff: DiffComponent,\n\tremotes: RemoteStatus,\n\tgit_diff: AsyncDiff,\n\tgit_state: RepoState,\n\tgit_status_workdir: AsyncStatus,\n\tgit_status_stage: AsyncStatus,\n\tgit_branch_state: Option<BranchCompare>,\n\tgit_branch_name: cached::BranchName,\n\tqueue: Queue,\n\tgit_action_executed: bool,\n\toptions: SharedOptions,\n\tkey_config: SharedKeyConfig,\n}\n\nimpl DrawableComponent for Status {\n\tfn draw(\n\t\t&self,\n\t\tf: &mut ratatui::Frame,\n\t\trect: ratatui::layout::Rect,\n\t) -> Result<()> {\n\t\tlet repo_unclean = self.repo_state_unclean();\n\t\tlet rects = if repo_unclean {\n\t\t\tLayout::default()\n\t\t\t\t.direction(Direction::Vertical)\n\t\t\t\t.constraints(\n\t\t\t\t\t[Constraint::Min(1), Constraint::Length(3)]\n\t\t\t\t\t\t.as_ref(),\n\t\t\t\t)\n\t\t\t\t.split(rect)\n\t\t} else {\n\t\t\tstd::rc::Rc::new([rect])\n\t\t};\n\n\t\tlet chunks = Layout::default()\n\t\t\t.direction(Direction::Horizontal)\n\t\t\t.constraints(\n\t\t\t\tif self.focus == Focus::Diff {\n\t\t\t\t\t[\n\t\t\t\t\t\tConstraint::Percentage(0),\n\t\t\t\t\t\tConstraint::Percentage(100),\n\t\t\t\t\t]\n\t\t\t\t} else {\n\t\t\t\t\t[\n\t\t\t\t\t\tConstraint::Percentage(50),\n\t\t\t\t\t\tConstraint::Percentage(50),\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t\t.as_ref(),\n\t\t\t)\n\t\t\t.split(rects[0]);\n\n\t\tlet left_chunks = Layout::default()\n\t\t\t.direction(Direction::Vertical)\n\t\t\t.constraints(\n\t\t\t\tif self.diff_target == DiffTarget::WorkingDir {\n\t\t\t\t\t[\n\t\t\t\t\t\tConstraint::Percentage(60),\n\t\t\t\t\t\tConstraint::Percentage(40),\n\t\t\t\t\t]\n\t\t\t\t} else {\n\t\t\t\t\t[\n\t\t\t\t\t\tConstraint::Percentage(40),\n\t\t\t\t\t\tConstraint::Percentage(60),\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t\t.as_ref(),\n\t\t\t)\n\t\t\t.split(chunks[0]);\n\n\t\tself.index_wd.draw(f, left_chunks[0])?;\n\t\tself.index.draw(f, left_chunks[1])?;\n\t\tself.diff.draw(f, chunks[1])?;\n\t\tself.draw_branch_state(f, &left_chunks);\n\n\t\tif repo_unclean {\n\t\t\tself.draw_repo_state(f, rects[1]);\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl Status {\n\taccessors!(self, [index, index_wd, diff]);\n\n\t///\n\tpub fn new(env: &Environment) -> Self {\n\t\tlet repo_clone = env.repo.borrow().clone();\n\t\tSelf {\n\t\t\tqueue: env.queue.clone(),\n\t\t\tvisible: true,\n\t\t\tremotes: RemoteStatus {\n\t\t\t\thas_remote_for_fetch: false,\n\t\t\t\thas_remote_for_push: false,\n\t\t\t},\n\t\t\tgit_state: RepoState::Clean,\n\t\t\tfocus: Focus::WorkDir,\n\t\t\tdiff_target: DiffTarget::WorkingDir,\n\t\t\tindex_wd: ChangesComponent::new(\n\t\t\t\tenv,\n\t\t\t\t&strings::title_status(&env.key_config),\n\t\t\t\ttrue,\n\t\t\t\ttrue,\n\t\t\t),\n\t\t\tindex: ChangesComponent::new(\n\t\t\t\tenv,\n\t\t\t\t&strings::title_index(&env.key_config),\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t),\n\t\t\tdiff: DiffComponent::new(env, false),\n\t\t\tgit_diff: AsyncDiff::new(\n\t\t\t\trepo_clone.clone(),\n\t\t\t\t&env.sender_git,\n\t\t\t),\n\t\t\tgit_status_workdir: AsyncStatus::new(\n\t\t\t\trepo_clone.clone(),\n\t\t\t\tenv.sender_git.clone(),\n\t\t\t),\n\t\t\tgit_status_stage: AsyncStatus::new(\n\t\t\t\trepo_clone,\n\t\t\t\tenv.sender_git.clone(),\n\t\t\t),\n\t\t\tgit_action_executed: false,\n\t\t\tgit_branch_state: None,\n\t\t\tgit_branch_name: cached::BranchName::new(\n\t\t\t\tenv.repo.clone(),\n\t\t\t),\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\toptions: env.options.clone(),\n\t\t\trepo: env.repo.clone(),\n\t\t}\n\t}\n\n\tfn draw_branch_state(\n\t\t&self,\n\t\tf: &mut ratatui::Frame,\n\t\tchunks: &[ratatui::layout::Rect],\n\t) {\n\t\tif let Some(branch_name) = self.git_branch_name.last() {\n\t\t\tlet ahead_behind = self\n\t\t\t\t.git_branch_state\n\t\t\t\t.as_ref()\n\t\t\t\t.map_or_else(String::new, |state| {\n\t\t\t\t\tformat!(\n\t\t\t\t\t\t\"\\u{2191}{} \\u{2193}{} \",\n\t\t\t\t\t\tstate.ahead, state.behind,\n\t\t\t\t\t)\n\t\t\t\t});\n\n\t\t\tlet w = Paragraph::new(format!(\n\t\t\t\t\"{ahead_behind}{{{branch_name}}}\"\n\t\t\t))\n\t\t\t.alignment(Alignment::Right);\n\n\t\t\tlet mut rect = if self.index_wd.focused() {\n\t\t\t\tlet mut rect = chunks[0];\n\t\t\t\trect.y += rect.height.saturating_sub(1);\n\t\t\t\trect\n\t\t\t} else {\n\t\t\t\tchunks[1]\n\t\t\t};\n\n\t\t\trect.x += 1;\n\t\t\trect.width = rect.width.saturating_sub(2);\n\t\t\trect.height = rect\n\t\t\t\t.height\n\t\t\t\t.saturating_sub(rect.height.saturating_sub(1));\n\n\t\t\tf.render_widget(w, rect);\n\t\t}\n\t}\n\n\tfn repo_state_text(repo: &RepoPath, state: &RepoState) -> String {\n\t\tmatch state {\n\t\t\tRepoState::Merge => {\n\t\t\t\tlet ids =\n\t\t\t\t\tsync::mergehead_ids(repo).unwrap_or_default();\n\n\t\t\t\tformat!(\n\t\t\t\t\t\"Commits: {}\",\n\t\t\t\t\tids.iter()\n\t\t\t\t\t\t.map(sync::CommitId::get_short_string)\n\t\t\t\t\t\t.join(\",\")\n\t\t\t\t)\n\t\t\t}\n\t\t\tRepoState::Rebase => sync::rebase_progress(repo)\n\t\t\t\t.map_or_else(\n\t\t\t\t\t|_| String::new(),\n\t\t\t\t\t|p| {\n\t\t\t\t\t\tformat!(\n\t\t\t\t\t\t\t\"Step: {}/{} Current Commit: {}\",\n\t\t\t\t\t\t\tp.current + 1,\n\t\t\t\t\t\t\tp.steps,\n\t\t\t\t\t\t\tp.current_commit\n\t\t\t\t\t\t\t\t.as_ref()\n\t\t\t\t\t\t\t\t.map(CommitId::get_short_string)\n\t\t\t\t\t\t\t\t.unwrap_or_default(),\n\t\t\t\t\t\t)\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\tRepoState::Revert => {\n\t\t\t\tformat!(\n\t\t\t\t\t\"Revert {}\",\n\t\t\t\t\tsync::revert_head(repo)\n\t\t\t\t\t\t.ok()\n\t\t\t\t\t\t.as_ref()\n\t\t\t\t\t\t.map(CommitId::get_short_string)\n\t\t\t\t\t\t.unwrap_or_default(),\n\t\t\t\t)\n\t\t\t}\n\t\t\t_ => format!(\"{state:?}\"),\n\t\t}\n\t}\n\n\tfn draw_repo_state(\n\t\t&self,\n\t\tf: &mut ratatui::Frame,\n\t\tr: ratatui::layout::Rect,\n\t) {\n\t\tif self.git_state != RepoState::Clean {\n\t\t\tlet txt = Self::repo_state_text(\n\t\t\t\t&self.repo.borrow(),\n\t\t\t\t&self.git_state,\n\t\t\t);\n\n\t\t\tlet w = Paragraph::new(txt)\n\t\t\t\t.block(\n\t\t\t\t\tBlock::default()\n\t\t\t\t\t\t.border_type(BorderType::Plain)\n\t\t\t\t\t\t.borders(Borders::all())\n\t\t\t\t\t\t.border_style(Theme::attention_block())\n\t\t\t\t\t\t.title(format!(\n\t\t\t\t\t\t\t\"Pending {:?}\",\n\t\t\t\t\t\t\tself.git_state\n\t\t\t\t\t\t)),\n\t\t\t\t)\n\t\t\t\t.style(Style::default().fg(Color::Red))\n\t\t\t\t.alignment(Alignment::Left);\n\n\t\t\tf.render_widget(w, r);\n\t\t}\n\t}\n\n\tfn repo_state_unclean(&self) -> bool {\n\t\tself.git_state != RepoState::Clean\n\t}\n\n\tfn can_focus_diff(&self) -> bool {\n\t\tmatch self.focus {\n\t\t\tFocus::WorkDir => self.index_wd.is_file_selected(),\n\t\t\tFocus::Stage => self.index.is_file_selected(),\n\t\t\tFocus::Diff => false,\n\t\t}\n\t}\n\n\tfn is_focus_on_diff(&self) -> bool {\n\t\tself.focus == Focus::Diff\n\t}\n\n\tfn switch_focus(&mut self, f: Focus) -> Result<bool> {\n\t\tif self.focus != f {\n\t\t\tself.focus = f;\n\n\t\t\tmatch self.focus {\n\t\t\t\tFocus::WorkDir => {\n\t\t\t\t\tself.set_diff_target(DiffTarget::WorkingDir);\n\t\t\t\t\tself.diff.focus(false);\n\t\t\t\t}\n\t\t\t\tFocus::Stage => {\n\t\t\t\t\tself.set_diff_target(DiffTarget::Stage);\n\t\t\t\t\tself.diff.focus(false);\n\t\t\t\t}\n\t\t\t\tFocus::Diff => {\n\t\t\t\t\tself.index.focus(false);\n\t\t\t\t\tself.index_wd.focus(false);\n\n\t\t\t\t\tself.diff.focus(true);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tself.update_diff()?;\n\n\t\t\treturn Ok(true);\n\t\t}\n\n\t\tOk(false)\n\t}\n\n\tfn set_diff_target(&mut self, target: DiffTarget) {\n\t\tself.diff_target = target;\n\t\tlet is_stage = self.diff_target == DiffTarget::Stage;\n\n\t\tself.index_wd.focus_select(!is_stage);\n\t\tself.index.focus_select(is_stage);\n\t}\n\n\tpub fn selected_path(&self) -> Option<(String, bool)> {\n\t\tlet (idx, is_stage) = match self.diff_target {\n\t\t\tDiffTarget::Stage => (&self.index, true),\n\t\t\tDiffTarget::WorkingDir => (&self.index_wd, false),\n\t\t};\n\n\t\tif let Some(item) = idx.selection() {\n\t\t\tif let FileTreeItemKind::File(i) = item.kind {\n\t\t\t\treturn Some((i.path, is_stage));\n\t\t\t}\n\t\t}\n\t\tNone\n\t}\n\n\t///\n\tpub fn update(&mut self) -> Result<()> {\n\t\tlet _ = self.git_branch_name.lookup().ok();\n\n\t\tif self.is_visible() {\n\t\t\tlet config =\n\t\t\t\tself.options.borrow().status_show_untracked();\n\n\t\t\tself.git_diff.refresh()?;\n\t\t\tself.git_status_workdir.fetch(&StatusParams::new(\n\t\t\t\tStatusType::WorkingDir,\n\t\t\t\tconfig,\n\t\t\t))?;\n\t\t\tself.git_status_stage.fetch(&StatusParams::new(\n\t\t\t\tStatusType::Stage,\n\t\t\t\tconfig,\n\t\t\t))?;\n\n\t\t\tself.git_state = sync::repo_state(&self.repo.borrow())\n\t\t\t\t.unwrap_or(RepoState::Clean);\n\n\t\t\tself.branch_compare();\n\t\t}\n\n\t\tOk(())\n\t}\n\n\t///\n\tpub fn anything_pending(&self) -> bool {\n\t\tself.git_diff.is_pending()\n\t\t\t|| self.git_status_stage.is_pending()\n\t\t\t|| self.git_status_workdir.is_pending()\n\t}\n\n\tfn check_remotes(&mut self) {\n\t\tself.remotes.has_remote_for_fetch =\n\t\t\tsync::get_default_remote_for_fetch(\n\t\t\t\t&self.repo.borrow().clone(),\n\t\t\t)\n\t\t\t.is_ok();\n\t\tself.remotes.has_remote_for_push =\n\t\t\tsync::get_default_remote_for_push(\n\t\t\t\t&self.repo.borrow().clone(),\n\t\t\t)\n\t\t\t.is_ok();\n\t}\n\n\t///\n\tpub fn update_git(\n\t\t&mut self,\n\t\tev: AsyncGitNotification,\n\t) -> Result<()> {\n\t\tif !self.is_visible() {\n\t\t\treturn Ok(());\n\t\t}\n\n\t\tmatch ev {\n\t\t\tAsyncGitNotification::Diff => self.update_diff()?,\n\t\t\tAsyncGitNotification::Status => self.update_status()?,\n\t\t\tAsyncGitNotification::Branches => self.check_remotes(),\n\t\t\tAsyncGitNotification::Push\n\t\t\t| AsyncGitNotification::Pull\n\t\t\t| AsyncGitNotification::CommitFiles => {\n\t\t\t\tself.branch_compare();\n\t\t\t}\n\t\t\t_ => (),\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tpub fn get_files_changes(&self) -> Result<Vec<StatusItem>> {\n\t\tOk(self.git_status_stage.last()?.items)\n\t}\n\n\tfn update_status(&mut self) -> Result<()> {\n\t\tlet stage_status = self.git_status_stage.last()?;\n\t\tself.index.set_items(&stage_status.items)?;\n\n\t\tlet workdir_status = self.git_status_workdir.last()?;\n\t\tself.index_wd.set_items(&workdir_status.items)?;\n\n\t\tself.update_diff()?;\n\n\t\tif self.git_action_executed {\n\t\t\tself.git_action_executed = false;\n\n\t\t\tif self.focus == Focus::WorkDir\n\t\t\t\t&& workdir_status.items.is_empty()\n\t\t\t\t&& !stage_status.items.is_empty()\n\t\t\t{\n\t\t\t\tself.switch_focus(Focus::Stage)?;\n\t\t\t} else if self.focus == Focus::Stage\n\t\t\t\t&& stage_status.items.is_empty()\n\t\t\t{\n\t\t\t\tself.switch_focus(Focus::WorkDir)?;\n\t\t\t}\n\t\t}\n\n\t\tOk(())\n\t}\n\n\t///\n\tpub fn update_diff(&mut self) -> Result<()> {\n\t\tif let Some((path, is_stage)) = self.selected_path() {\n\t\t\tlet diff_type = if is_stage {\n\t\t\t\tDiffType::Stage\n\t\t\t} else {\n\t\t\t\tDiffType::WorkDir\n\t\t\t};\n\n\t\t\tlet diff_params = DiffParams {\n\t\t\t\tpath: path.clone(),\n\t\t\t\tdiff_type,\n\t\t\t\toptions: self.options.borrow().diff_options(),\n\t\t\t};\n\n\t\t\tif self.diff.current() == (path.clone(), is_stage) {\n\t\t\t\t// we are already showing a diff of the right file\n\t\t\t\t// maybe the diff changed (outside file change)\n\t\t\t\tif let Some((params, last)) = self.git_diff.last()? {\n\t\t\t\t\tif params == diff_params {\n\t\t\t\t\t\t// all params match, so we might need to update\n\t\t\t\t\t\tself.diff.update(path, is_stage, last);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// params changed, we need to request the right diff\n\t\t\t\t\t\tself.request_diff(\n\t\t\t\t\t\t\tdiff_params,\n\t\t\t\t\t\t\tpath,\n\t\t\t\t\t\t\tis_stage,\n\t\t\t\t\t\t)?;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// we dont show the right diff right now, so we need to request\n\t\t\t\tself.request_diff(diff_params, path, is_stage)?;\n\t\t\t}\n\t\t} else {\n\t\t\tself.diff.clear(false);\n\t\t}\n\n\t\tOk(())\n\t}\n\n\tfn request_diff(\n\t\t&mut self,\n\t\tdiff_params: DiffParams,\n\t\tpath: String,\n\t\tis_stage: bool,\n\t) -> Result<(), anyhow::Error> {\n\t\tif let Some(diff) = self.git_diff.request(diff_params)? {\n\t\t\tself.diff.update(path, is_stage, diff);\n\t\t} else {\n\t\t\tself.diff.clear(true);\n\t\t}\n\n\t\tOk(())\n\t}\n\n\t/// called after confirmation\n\tpub fn reset(&self, item: &ResetItem) -> bool {\n\t\tif let Err(e) = sync::reset_workdir(\n\t\t\t&self.repo.borrow(),\n\t\t\titem.path.as_str(),\n\t\t) {\n\t\t\tself.queue.push(InternalEvent::ShowErrorMsg(format!(\n\t\t\t\t\"reset failed:\\n{e}\"\n\t\t\t)));\n\n\t\t\tfalse\n\t\t} else {\n\t\t\ttrue\n\t\t}\n\t}\n\n\tpub fn last_file_moved(&mut self) -> Result<()> {\n\t\tif !self.is_focus_on_diff() && self.is_visible() {\n\t\t\tself.switch_focus(self.focus.toggled_focus())?;\n\t\t}\n\t\tOk(())\n\t}\n\n\tfn push(&self, force: bool) {\n\t\tif self.can_push() {\n\t\t\tif let Some(branch) = self.git_branch_name.last() {\n\t\t\t\tif force {\n\t\t\t\t\tself.queue.push(InternalEvent::ConfirmAction(\n\t\t\t\t\t\tAction::ForcePush(branch, force),\n\t\t\t\t\t));\n\t\t\t\t} else {\n\t\t\t\t\tself.queue.push(InternalEvent::Push(\n\t\t\t\t\t\tbranch,\n\t\t\t\t\t\tPushType::Branch,\n\t\t\t\t\t\tforce,\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfn fetch(&self) {\n\t\tif self.can_fetch() {\n\t\t\tself.queue.push(InternalEvent::FetchRemotes);\n\t\t}\n\t}\n\n\tfn pull(&self) {\n\t\tif let Some(branch) = self.git_branch_name.last() {\n\t\t\tself.queue.push(InternalEvent::Pull(branch));\n\t\t}\n\t}\n\n\tfn undo_last_commit(&self) {\n\t\tself.queue\n\t\t\t.push(InternalEvent::ConfirmAction(Action::UndoCommit));\n\t}\n\n\tfn branch_compare(&mut self) {\n\t\tself.git_branch_state =\n\t\t\tself.git_branch_name.last().and_then(|branch| {\n\t\t\t\tsync::branch_compare_upstream(\n\t\t\t\t\t&self.repo.borrow(),\n\t\t\t\t\tbranch.as_str(),\n\t\t\t\t)\n\t\t\t\t.ok()\n\t\t\t});\n\t}\n\n\tfn can_push(&self) -> bool {\n\t\tlet is_ahead = self\n\t\t\t.git_branch_state\n\t\t\t.as_ref()\n\t\t\t.is_none_or(|state| state.ahead > 0);\n\n\t\tis_ahead && self.remotes.has_remote_for_push\n\t}\n\n\tconst fn can_fetch(&self) -> bool {\n\t\tself.remotes.has_remote_for_fetch\n\t\t\t&& self.git_branch_state.is_some()\n\t}\n\n\tfn can_abort_merge(&self) -> bool {\n\t\tself.git_state == RepoState::Merge\n\t}\n\n\tfn pending_rebase(&self) -> bool {\n\t\tself.git_state == RepoState::Rebase\n\t}\n\n\tfn pending_revert(&self) -> bool {\n\t\tself.git_state == RepoState::Revert\n\t}\n\n\tpub fn revert_pending_state(&self) {\n\t\ttry_or_popup!(\n\t\t\tself,\n\t\t\t\"revert pending state\",\n\t\t\tsync::abort_pending_state(&self.repo.borrow())\n\t\t);\n\t}\n\n\tpub fn abort_rebase(&self) {\n\t\ttry_or_popup!(\n\t\t\tself,\n\t\t\t\"abort rebase\",\n\t\t\tsync::abort_pending_rebase(&self.repo.borrow())\n\t\t);\n\t}\n\n\tfn continue_rebase(&self) {\n\t\ttry_or_popup!(\n\t\t\tself,\n\t\t\t\"continue rebase\",\n\t\t\tsync::continue_pending_rebase(&self.repo.borrow())\n\t\t);\n\t}\n\n\tfn commands_nav(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) {\n\t\tlet focus_on_diff = self.is_focus_on_diff();\n\t\tout.push(\n\t\t\tCommandInfo::new(\n\t\t\t\tstrings::commands::close_popup(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\t(self.visible && focus_on_diff) || force_all,\n\t\t\t)\n\t\t\t.order(strings::order::NAV),\n\t\t);\n\t\tout.push(\n\t\t\tCommandInfo::new(\n\t\t\t\tstrings::commands::diff_focus_right(&self.key_config),\n\t\t\t\tself.can_focus_diff(),\n\t\t\t\t(self.visible && !focus_on_diff) || force_all,\n\t\t\t)\n\t\t\t.order(strings::order::NAV),\n\t\t);\n\t\tout.push(\n\t\t\tCommandInfo::new(\n\t\t\t\tstrings::commands::select_staging(&self.key_config),\n\t\t\t\t!focus_on_diff,\n\t\t\t\t(self.visible\n\t\t\t\t\t&& !focus_on_diff\n\t\t\t\t\t&& self.focus == Focus::WorkDir)\n\t\t\t\t\t|| force_all,\n\t\t\t)\n\t\t\t.order(strings::order::NAV),\n\t\t);\n\t\tout.push(\n\t\t\tCommandInfo::new(\n\t\t\t\tstrings::commands::select_unstaged(&self.key_config),\n\t\t\t\t!focus_on_diff,\n\t\t\t\t(self.visible\n\t\t\t\t\t&& !focus_on_diff\n\t\t\t\t\t&& self.focus == Focus::Stage)\n\t\t\t\t\t|| force_all,\n\t\t\t)\n\t\t\t.order(strings::order::NAV),\n\t\t);\n\t}\n\n\tfn can_commit(&self) -> bool {\n\t\tself.index.focused()\n\t\t\t&& !self.index.is_empty()\n\t\t\t&& !self.pending_rebase()\n\t}\n}\n\nimpl Component for Status {\n\tfn commands(\n\t\t&self,\n\t\tout: &mut Vec<CommandInfo>,\n\t\tforce_all: bool,\n\t) -> CommandBlocking {\n\t\tlet focus_on_diff = self.is_focus_on_diff();\n\n\t\tif self.visible || force_all {\n\t\t\tcommand_pump(\n\t\t\t\tout,\n\t\t\t\tforce_all,\n\t\t\t\tself.components().as_slice(),\n\t\t\t);\n\n\t\t\tout.push(\n\t\t\t\tCommandInfo::new(\n\t\t\t\t\tstrings::commands::commit_open(&self.key_config),\n\t\t\t\t\ttrue,\n\t\t\t\t\tself.can_commit() || force_all,\n\t\t\t\t)\n\t\t\t\t.order(-1),\n\t\t\t);\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::open_branch_select_popup(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t),\n\t\t\t\ttrue,\n\t\t\t\t!focus_on_diff,\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::status_push(&self.key_config),\n\t\t\t\tself.can_push(),\n\t\t\t\t!focus_on_diff,\n\t\t\t));\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::status_force_push(\n\t\t\t\t\t&self.key_config,\n\t\t\t\t),\n\t\t\t\ttrue,\n\t\t\t\tself.can_push() && !focus_on_diff,\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::status_fetch(&self.key_config),\n\t\t\t\tself.can_fetch(),\n\t\t\t\t!focus_on_diff,\n\t\t\t));\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::status_pull(&self.key_config),\n\t\t\t\tself.can_fetch(),\n\t\t\t\t!focus_on_diff,\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::undo_commit(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\t(!self.pending_rebase() && !focus_on_diff)\n\t\t\t\t\t|| force_all,\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::abort_merge(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\tself.can_abort_merge() || force_all,\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::continue_rebase(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\tself.pending_rebase() || force_all,\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::abort_rebase(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\tself.pending_rebase() || force_all,\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::abort_revert(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\tself.pending_revert() || force_all,\n\t\t\t));\n\n\t\t\tout.push(CommandInfo::new(\n\t\t\t\tstrings::commands::view_submodules(&self.key_config),\n\t\t\t\ttrue,\n\t\t\t\ttrue,\n\t\t\t));\n\t\t}\n\n\t\tself.commands_nav(out, force_all);\n\n\t\tvisibility_blocking(self)\n\t}\n\n\t#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]\n\tfn event(\n\t\t&mut self,\n\t\tev: &crossterm::event::Event,\n\t) -> Result<EventState> {\n\t\tif self.visible {\n\t\t\tif event_pump(ev, self.components_mut().as_mut_slice())?\n\t\t\t\t.is_consumed()\n\t\t\t{\n\t\t\t\tself.git_action_executed = true;\n\t\t\t\treturn Ok(EventState::Consumed);\n\t\t\t}\n\n\t\t\tif let Event::Key(k) = ev {\n\t\t\t\treturn if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.open_commit,\n\t\t\t\t) && self.can_commit()\n\t\t\t\t{\n\t\t\t\t\tself.queue.push(InternalEvent::OpenCommit);\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.toggle_workarea,\n\t\t\t\t) && !self.is_focus_on_diff()\n\t\t\t\t{\n\t\t\t\t\tself.switch_focus(self.focus.toggled_focus())\n\t\t\t\t\t\t.map(Into::into)\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.move_right,\n\t\t\t\t) && self.can_focus_diff()\n\t\t\t\t{\n\t\t\t\t\tself.switch_focus(Focus::Diff).map(Into::into)\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.exit_popup,\n\t\t\t\t) {\n\t\t\t\t\tself.switch_focus(match self.diff_target {\n\t\t\t\t\t\tDiffTarget::Stage => Focus::Stage,\n\t\t\t\t\t\tDiffTarget::WorkingDir => Focus::WorkDir,\n\t\t\t\t\t})\n\t\t\t\t\t.map(Into::into)\n\t\t\t\t} else if key_match(k, self.key_config.keys.move_down)\n\t\t\t\t\t&& self.focus == Focus::WorkDir\n\t\t\t\t\t&& !self.index.is_empty()\n\t\t\t\t{\n\t\t\t\t\tself.switch_focus(Focus::Stage).map(Into::into)\n\t\t\t\t} else if key_match(k, self.key_config.keys.move_up)\n\t\t\t\t\t&& self.focus == Focus::Stage\n\t\t\t\t\t&& !self.index_wd.is_empty()\n\t\t\t\t{\n\t\t\t\t\tself.switch_focus(Focus::WorkDir).map(Into::into)\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.select_branch,\n\t\t\t\t) && !self.is_focus_on_diff()\n\t\t\t\t{\n\t\t\t\t\tself.queue.push(InternalEvent::SelectBranch);\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.force_push,\n\t\t\t\t) && !self.is_focus_on_diff()\n\t\t\t\t\t&& self.can_push()\n\t\t\t\t{\n\t\t\t\t\tself.push(true);\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(k, self.key_config.keys.push)\n\t\t\t\t\t&& !self.is_focus_on_diff()\n\t\t\t\t{\n\t\t\t\t\tself.push(false);\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(k, self.key_config.keys.fetch)\n\t\t\t\t\t&& !self.is_focus_on_diff()\n\t\t\t\t\t&& self.can_fetch()\n\t\t\t\t{\n\t\t\t\t\tself.fetch();\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(k, self.key_config.keys.pull)\n\t\t\t\t\t&& !self.is_focus_on_diff()\n\t\t\t\t\t&& self.can_fetch()\n\t\t\t\t{\n\t\t\t\t\tself.pull();\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.undo_commit,\n\t\t\t\t) && !self.is_focus_on_diff()\n\t\t\t\t{\n\t\t\t\t\tself.undo_last_commit();\n\t\t\t\t\tself.queue.push(InternalEvent::Update(\n\t\t\t\t\t\tNeedsUpdate::ALL,\n\t\t\t\t\t));\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.abort_merge,\n\t\t\t\t) {\n\t\t\t\t\tif self.can_abort_merge() {\n\t\t\t\t\t\tself.queue.push(\n\t\t\t\t\t\t\tInternalEvent::ConfirmAction(\n\t\t\t\t\t\t\t\tAction::AbortMerge,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t} else if self.pending_rebase() {\n\t\t\t\t\t\tself.queue.push(\n\t\t\t\t\t\t\tInternalEvent::ConfirmAction(\n\t\t\t\t\t\t\t\tAction::AbortRebase,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t} else if self.pending_revert() {\n\t\t\t\t\t\tself.queue.push(\n\t\t\t\t\t\t\tInternalEvent::ConfirmAction(\n\t\t\t\t\t\t\t\tAction::AbortRevert,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.rebase_branch,\n\t\t\t\t) && self.pending_rebase()\n\t\t\t\t{\n\t\t\t\t\tself.continue_rebase();\n\t\t\t\t\tself.queue.push(InternalEvent::Update(\n\t\t\t\t\t\tNeedsUpdate::ALL,\n\t\t\t\t\t));\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else if key_match(\n\t\t\t\t\tk,\n\t\t\t\t\tself.key_config.keys.view_submodules,\n\t\t\t\t) {\n\t\t\t\t\tself.queue.push(InternalEvent::ViewSubmodules);\n\t\t\t\t\tOk(EventState::Consumed)\n\t\t\t\t} else {\n\t\t\t\t\tOk(EventState::NotConsumed)\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tOk(EventState::NotConsumed)\n\t}\n\n\tfn is_visible(&self) -> bool {\n\t\tself.visible\n\t}\n\n\tfn hide(&mut self) {\n\t\tself.visible = false;\n\n\t\tself.index.hide();\n\t\tself.index_wd.hide();\n\t}\n\n\tfn show(&mut self) -> Result<()> {\n\t\tself.visible = true;\n\t\tself.index.show()?;\n\t\tself.index_wd.show()?;\n\n\t\tself.check_remotes();\n\t\tself.update()?;\n\n\t\tOk(())\n\t}\n}\n"
  },
  {
    "path": "src/ui/mod.rs",
    "content": "mod reflow;\nmod scrollbar;\nmod scrolllist;\nmod stateful_paragraph;\npub mod style;\nmod syntax_text;\n\nuse filetreelist::MoveSelection;\nuse ratatui::layout::{Constraint, Direction, Layout, Rect};\npub use scrollbar::{draw_scrollbar, Orientation};\npub use scrolllist::{draw_list, draw_list_block};\npub use stateful_paragraph::{\n\tParagraphState, ScrollPos, StatefulParagraph,\n};\npub use syntax_text::{AsyncSyntaxJob, SyntaxText};\n\nuse crate::keys::{key_match, SharedKeyConfig};\n\n/// return the scroll position (line) necessary to have the `selection` in view if it is not already\npub const fn calc_scroll_top(\n\tcurrent_top: usize,\n\theight_in_lines: usize,\n\tselection: usize,\n) -> usize {\n\tif current_top.saturating_add(height_in_lines) <= selection {\n\t\tselection.saturating_sub(height_in_lines) + 1\n\t} else if current_top > selection {\n\t\tselection\n\t} else {\n\t\tcurrent_top\n\t}\n}\n\n/// ui component size representation\n#[derive(Copy, Clone)]\npub struct Size {\n\tpub width: u16,\n\tpub height: u16,\n}\n\nimpl Size {\n\tpub const fn new(width: u16, height: u16) -> Self {\n\t\tSelf { width, height }\n\t}\n}\n\nimpl From<Rect> for Size {\n\tfn from(r: Rect) -> Self {\n\t\tSelf {\n\t\t\twidth: r.width,\n\t\t\theight: r.height,\n\t\t}\n\t}\n}\n\n/// use layouts to create a rects that\n/// centers inside `r` and sizes `percent_x`/`percent_x` of `r`\npub fn centered_rect(\n\tpercent_x: u16,\n\tpercent_y: u16,\n\tr: Rect,\n) -> Rect {\n\tlet popup_layout = Layout::default()\n\t\t.direction(Direction::Vertical)\n\t\t.constraints(\n\t\t\t[\n\t\t\t\tConstraint::Percentage((100 - percent_y) / 2),\n\t\t\t\tConstraint::Percentage(percent_y),\n\t\t\t\tConstraint::Percentage((100 - percent_y) / 2),\n\t\t\t]\n\t\t\t.as_ref(),\n\t\t)\n\t\t.split(r);\n\n\tLayout::default()\n\t\t.direction(Direction::Horizontal)\n\t\t.constraints(\n\t\t\t[\n\t\t\t\tConstraint::Percentage((100 - percent_x) / 2),\n\t\t\t\tConstraint::Percentage(percent_x),\n\t\t\t\tConstraint::Percentage((100 - percent_x) / 2),\n\t\t\t]\n\t\t\t.as_ref(),\n\t\t)\n\t\t.split(popup_layout[1])[1]\n}\n\n/// makes sure Rect `r` at least stays as big as min and not bigger than max\npub fn rect_inside(min: Size, max: Size, r: Rect) -> Rect {\n\tlet new_width = if min.width > max.width {\n\t\tmax.width\n\t} else {\n\t\tr.width.clamp(min.width, max.width)\n\t};\n\n\tlet new_height = if min.height > max.height {\n\t\tmax.height\n\t} else {\n\t\tr.height.clamp(min.height, max.height)\n\t};\n\n\tlet diff_width = new_width.saturating_sub(r.width);\n\tlet diff_height = new_height.saturating_sub(r.height);\n\n\tRect::new(\n\t\tr.x.saturating_sub(diff_width / 2),\n\t\tr.y.saturating_sub(diff_height / 2),\n\t\tnew_width,\n\t\tnew_height,\n\t)\n}\n\npub fn centered_rect_absolute(\n\twidth: u16,\n\theight: u16,\n\tr: Rect,\n) -> Rect {\n\tRect::new(\n\t\t(r.width.saturating_sub(width)) / 2,\n\t\t(r.height.saturating_sub(height)) / 2,\n\t\twidth.min(r.width),\n\t\theight.min(r.height),\n\t)\n}\n\n///\npub fn common_nav(\n\tkey: &crossterm::event::KeyEvent,\n\tkey_config: &SharedKeyConfig,\n) -> Option<MoveSelection> {\n\tif key_match(key, key_config.keys.move_down) {\n\t\tSome(MoveSelection::Down)\n\t} else if key_match(key, key_config.keys.move_up) {\n\t\tSome(MoveSelection::Up)\n\t} else if key_match(key, key_config.keys.page_up) {\n\t\tSome(MoveSelection::PageUp)\n\t} else if key_match(key, key_config.keys.page_down) {\n\t\tSome(MoveSelection::PageDown)\n\t} else if key_match(key, key_config.keys.move_right) {\n\t\tSome(MoveSelection::Right)\n\t} else if key_match(key, key_config.keys.move_left) {\n\t\tSome(MoveSelection::Left)\n\t} else if key_match(key, key_config.keys.home)\n\t\t|| key_match(key, key_config.keys.shift_up)\n\t{\n\t\tSome(MoveSelection::Top)\n\t} else if key_match(key, key_config.keys.end)\n\t\t|| key_match(key, key_config.keys.shift_down)\n\t{\n\t\tSome(MoveSelection::End)\n\t} else {\n\t\tNone\n\t}\n}\n\n#[cfg(test)]\nmod test {\n\tuse super::{rect_inside, Size};\n\tuse pretty_assertions::assert_eq;\n\tuse ratatui::layout::Rect;\n\n\t#[test]\n\tfn test_small_rect_in_rect() {\n\t\tlet rect = rect_inside(\n\t\t\tSize {\n\t\t\t\twidth: 2,\n\t\t\t\theight: 2,\n\t\t\t},\n\t\t\tSize {\n\t\t\t\twidth: 1,\n\t\t\t\theight: 1,\n\t\t\t},\n\t\t\tRect {\n\t\t\t\tx: 0,\n\t\t\t\ty: 0,\n\t\t\t\twidth: 10,\n\t\t\t\theight: 10,\n\t\t\t},\n\t\t);\n\n\t\tassert_eq!(\n\t\t\trect,\n\t\t\tRect {\n\t\t\t\tx: 0,\n\t\t\t\ty: 0,\n\t\t\t\twidth: 1,\n\t\t\t\theight: 1\n\t\t\t}\n\t\t);\n\t}\n\n\t#[test]\n\tfn test_small_rect_in_rect2() {\n\t\tlet rect = rect_inside(\n\t\t\tSize {\n\t\t\t\twidth: 1,\n\t\t\t\theight: 3,\n\t\t\t},\n\t\t\tSize {\n\t\t\t\twidth: 1,\n\t\t\t\theight: 2,\n\t\t\t},\n\t\t\tRect {\n\t\t\t\tx: 0,\n\t\t\t\ty: 0,\n\t\t\t\twidth: 10,\n\t\t\t\theight: 10,\n\t\t\t},\n\t\t);\n\n\t\tassert_eq!(\n\t\t\trect,\n\t\t\tRect {\n\t\t\t\tx: 0,\n\t\t\t\ty: 0,\n\t\t\t\twidth: 1,\n\t\t\t\theight: 2\n\t\t\t}\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "src/ui/reflow.rs",
    "content": "use crate::string_utils::trim_offset;\nuse easy_cast::Cast;\nuse ratatui::text::StyledGrapheme;\nuse unicode_width::UnicodeWidthStr;\n\nconst NBSP: &str = \"\\u{00a0}\";\n\n/// A state machine to pack styled symbols into lines.\n/// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming\n/// iterators for that).\npub trait LineComposer<'a> {\n\tfn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)>;\n}\n\n/// A state machine that wraps lines on word boundaries.\npub struct WordWrapper<'a, 'b> {\n\tsymbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,\n\tmax_line_width: u16,\n\tcurrent_line: Vec<StyledGrapheme<'a>>,\n\tnext_line: Vec<StyledGrapheme<'a>>,\n\t/// Removes the leading whitespace from lines\n\ttrim: bool,\n}\n\nimpl<'a, 'b> WordWrapper<'a, 'b> {\n\tpub fn new(\n\t\tsymbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,\n\t\tmax_line_width: u16,\n\t\ttrim: bool,\n\t) -> Self {\n\t\tSelf {\n\t\t\tsymbols,\n\t\t\tmax_line_width,\n\t\t\tcurrent_line: vec![],\n\t\t\tnext_line: vec![],\n\t\t\ttrim,\n\t\t}\n\t}\n}\n\nimpl<'a> LineComposer<'a> for WordWrapper<'a, '_> {\n\tfn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)> {\n\t\tif self.max_line_width == 0 {\n\t\t\treturn None;\n\t\t}\n\t\tstd::mem::swap(&mut self.current_line, &mut self.next_line);\n\t\tself.next_line.truncate(0);\n\n\t\tlet mut current_line_width = self\n\t\t\t.current_line\n\t\t\t.iter()\n\t\t\t.map(|StyledGrapheme { symbol, .. }| -> u16 {\n\t\t\t\tsymbol.width().cast()\n\t\t\t})\n\t\t\t.sum();\n\n\t\tlet mut symbols_to_last_word_end: usize = 0;\n\t\tlet mut width_to_last_word_end: u16 = 0;\n\t\tlet mut prev_whitespace = false;\n\t\tlet mut symbols_exhausted = true;\n\t\tfor StyledGrapheme { symbol, style } in &mut self.symbols {\n\t\t\tsymbols_exhausted = false;\n\t\t\tlet symbol_whitespace =\n\t\t\t\tsymbol.chars().all(&char::is_whitespace)\n\t\t\t\t\t&& symbol != NBSP;\n\n\t\t\t// Ignore characters wider that the total max width.\n\t\t\tif Cast::<u16>::cast(symbol.width()) > self.max_line_width\n                // Skip leading whitespace when trim is enabled.\n                || self.trim && symbol_whitespace && symbol != \"\\n\" && current_line_width == 0\n\t\t\t{\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Break on newline and discard it.\n\t\t\tif symbol == \"\\n\" {\n\t\t\t\tif prev_whitespace {\n\t\t\t\t\tcurrent_line_width = width_to_last_word_end;\n\t\t\t\t\tself.current_line\n\t\t\t\t\t\t.truncate(symbols_to_last_word_end);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Mark the previous symbol as word end.\n\t\t\tif symbol_whitespace && !prev_whitespace {\n\t\t\t\tsymbols_to_last_word_end = self.current_line.len();\n\t\t\t\twidth_to_last_word_end = current_line_width;\n\t\t\t}\n\n\t\t\tself.current_line.push(StyledGrapheme { symbol, style });\n\t\t\tcurrent_line_width += Cast::<u16>::cast(symbol.width());\n\n\t\t\tif current_line_width > self.max_line_width {\n\t\t\t\t// If there was no word break in the text, wrap at the end of the line.\n\t\t\t\tlet (truncate_at, truncated_width) =\n\t\t\t\t\tif symbols_to_last_word_end == 0 {\n\t\t\t\t\t\t(\n\t\t\t\t\t\t\tself.current_line.len() - 1,\n\t\t\t\t\t\t\tself.max_line_width,\n\t\t\t\t\t\t)\n\t\t\t\t\t} else {\n\t\t\t\t\t\t(\n\t\t\t\t\t\t\tsymbols_to_last_word_end,\n\t\t\t\t\t\t\twidth_to_last_word_end,\n\t\t\t\t\t\t)\n\t\t\t\t\t};\n\n\t\t\t\t// Push the remainder to the next line but strip leading whitespace:\n\t\t\t\t{\n\t\t\t\t\tlet remainder = &self.current_line[truncate_at..];\n\t\t\t\t\tif let Some(remainder_nonwhite) =\n\t\t\t\t\t\tremainder.iter().position(\n\t\t\t\t\t\t\t|StyledGrapheme { symbol, .. }| {\n\t\t\t\t\t\t\t\t!symbol\n\t\t\t\t\t\t\t\t\t.chars()\n\t\t\t\t\t\t\t\t\t.all(&char::is_whitespace)\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t) {\n\t\t\t\t\t\tself.next_line.extend_from_slice(\n\t\t\t\t\t\t\t&remainder[remainder_nonwhite..],\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tself.current_line.truncate(truncate_at);\n\t\t\t\tcurrent_line_width = truncated_width;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tprev_whitespace = symbol_whitespace;\n\t\t}\n\n\t\t// Even if the iterator is exhausted, pass the previous remainder.\n\t\tif symbols_exhausted && self.current_line.is_empty() {\n\t\t\tNone\n\t\t} else {\n\t\t\tSome((&self.current_line[..], current_line_width))\n\t\t}\n\t}\n}\n\n/// A state machine that truncates overhanging lines.\npub struct LineTruncator<'a, 'b> {\n\tsymbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,\n\tmax_line_width: u16,\n\tcurrent_line: Vec<StyledGrapheme<'a>>,\n\t/// Record the offset to skip render\n\thorizontal_offset: u16,\n}\n\nimpl<'a, 'b> LineTruncator<'a, 'b> {\n\tpub fn new(\n\t\tsymbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,\n\t\tmax_line_width: u16,\n\t) -> Self {\n\t\tSelf {\n\t\t\tsymbols,\n\t\t\tmax_line_width,\n\t\t\thorizontal_offset: 0,\n\t\t\tcurrent_line: vec![],\n\t\t}\n\t}\n\n\tpub const fn set_horizontal_offset(\n\t\t&mut self,\n\t\thorizontal_offset: u16,\n\t) {\n\t\tself.horizontal_offset = horizontal_offset;\n\t}\n}\n\nimpl<'a> LineComposer<'a> for LineTruncator<'a, '_> {\n\tfn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)> {\n\t\tif self.max_line_width == 0 {\n\t\t\treturn None;\n\t\t}\n\n\t\tself.current_line.truncate(0);\n\t\tlet mut current_line_width = 0;\n\n\t\tlet mut skip_rest = false;\n\t\tlet mut symbols_exhausted = true;\n\t\tlet mut horizontal_offset = self.horizontal_offset as usize;\n\t\tfor StyledGrapheme { symbol, style } in &mut self.symbols {\n\t\t\tsymbols_exhausted = false;\n\n\t\t\t// Ignore characters wider that the total max width.\n\t\t\tif Cast::<u16>::cast(symbol.width()) > self.max_line_width\n\t\t\t{\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Break on newline and discard it.\n\t\t\tif symbol == \"\\n\" {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tif current_line_width + Cast::<u16>::cast(symbol.width())\n\t\t\t\t> self.max_line_width\n\t\t\t{\n\t\t\t\t// Exhaust the remainder of the line.\n\t\t\t\tskip_rest = true;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tlet symbol = if horizontal_offset == 0 {\n\t\t\t\tsymbol\n\t\t\t} else {\n\t\t\t\tlet w = symbol.width();\n\t\t\t\tif w > horizontal_offset {\n\t\t\t\t\tlet t = trim_offset(symbol, horizontal_offset);\n\t\t\t\t\thorizontal_offset = 0;\n\t\t\t\t\tt\n\t\t\t\t} else {\n\t\t\t\t\thorizontal_offset -= w;\n\t\t\t\t\t\"\"\n\t\t\t\t}\n\t\t\t};\n\t\t\tcurrent_line_width += Cast::<u16>::cast(symbol.width());\n\t\t\tself.current_line.push(StyledGrapheme { symbol, style });\n\t\t}\n\n\t\tif skip_rest {\n\t\t\tfor StyledGrapheme { symbol, .. } in &mut self.symbols {\n\t\t\t\tif symbol == \"\\n\" {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif symbols_exhausted && self.current_line.is_empty() {\n\t\t\tNone\n\t\t} else {\n\t\t\tSome((&self.current_line[..], current_line_width))\n\t\t}\n\t}\n}\n\n#[cfg(test)]\nmod test {\n\tuse super::*;\n\tuse unicode_segmentation::UnicodeSegmentation;\n\n\tenum Composer {\n\t\tWordWrapper { trim: bool },\n\t\tLineTruncator,\n\t}\n\n\tfn run_composer(\n\t\twhich: Composer,\n\t\ttext: &str,\n\t\ttext_area_width: u16,\n\t) -> (Vec<String>, Vec<u16>) {\n\t\tlet style = Default::default();\n\t\tlet mut styled = UnicodeSegmentation::graphemes(text, true)\n\t\t\t.map(|g| StyledGrapheme { symbol: g, style });\n\t\tlet mut composer: Box<dyn LineComposer> = match which {\n\t\t\tComposer::WordWrapper { trim } => Box::new(\n\t\t\t\tWordWrapper::new(&mut styled, text_area_width, trim),\n\t\t\t),\n\t\t\tComposer::LineTruncator => Box::new(LineTruncator::new(\n\t\t\t\t&mut styled,\n\t\t\t\ttext_area_width,\n\t\t\t)),\n\t\t};\n\t\tlet mut lines = vec![];\n\t\tlet mut widths = vec![];\n\t\twhile let Some((styled, width)) = composer.next_line() {\n\t\t\tlet line = styled\n\t\t\t\t.iter()\n\t\t\t\t.map(|StyledGrapheme { symbol, .. }| *symbol)\n\t\t\t\t.collect::<String>();\n\t\t\tassert!(width <= text_area_width);\n\t\t\tlines.push(line);\n\t\t\twidths.push(width);\n\t\t}\n\t\t(lines, widths)\n\t}\n\n\t#[test]\n\tfn line_composer_one_line() {\n\t\tlet width = 40;\n\t\tfor i in 1..width {\n\t\t\tlet text = \"a\".repeat(i);\n\t\t\tlet (word_wrapper, _) = run_composer(\n\t\t\t\tComposer::WordWrapper { trim: true },\n\t\t\t\t&text,\n\t\t\t\twidth as u16,\n\t\t\t);\n\t\t\tlet (line_truncator, _) = run_composer(\n\t\t\t\tComposer::LineTruncator,\n\t\t\t\t&text,\n\t\t\t\twidth as u16,\n\t\t\t);\n\t\t\tlet expected = vec![text];\n\t\t\tassert_eq!(word_wrapper, expected);\n\t\t\tassert_eq!(line_truncator, expected);\n\t\t}\n\t}\n\n\t#[test]\n\tfn line_composer_short_lines() {\n\t\tlet width = 20;\n\t\tlet text =\n            \"abcdefg\\nhijklmno\\npabcdefg\\nhijklmn\\nopabcdefghijk\\nlmnopabcd\\n\\n\\nefghijklmno\";\n\t\tlet (word_wrapper, _) = run_composer(\n\t\t\tComposer::WordWrapper { trim: true },\n\t\t\ttext,\n\t\t\twidth,\n\t\t);\n\t\tlet (line_truncator, _) =\n\t\t\trun_composer(Composer::LineTruncator, text, width);\n\n\t\tlet wrapped: Vec<&str> = text.split('\\n').collect();\n\t\tassert_eq!(word_wrapper, wrapped);\n\t\tassert_eq!(line_truncator, wrapped);\n\t}\n\n\t#[test]\n\tfn line_composer_long_word() {\n\t\tlet width = 20;\n\t\tlet text = \"abcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmno\";\n\t\tlet (word_wrapper, _) = run_composer(\n\t\t\tComposer::WordWrapper { trim: true },\n\t\t\ttext,\n\t\t\twidth as u16,\n\t\t);\n\t\tlet (line_truncator, _) =\n\t\t\trun_composer(Composer::LineTruncator, text, width as u16);\n\n\t\tlet wrapped = vec![\n\t\t\t&text[..width],\n\t\t\t&text[width..width * 2],\n\t\t\t&text[width * 2..width * 3],\n\t\t\t&text[width * 3..],\n\t\t];\n\t\tassert_eq!(\n            word_wrapper, wrapped,\n            \"WordWrapper should detect the line cannot be broken on word boundary and \\\n             break it at line width limit.\"\n        );\n\t\tassert_eq!(line_truncator, vec![&text[..width]]);\n\t}\n\n\t#[test]\n\tfn line_composer_long_sentence() {\n\t\tlet width = 20;\n\t\tlet text =\n            \"abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l m n o\";\n\t\tlet text_multi_space =\n            \"abcd efghij    klmnopabcd efgh     ijklmnopabcdefg hijkl mnopab c d e f g h i j k l \\\n             m n o\";\n\t\tlet (word_wrapper_single_space, _) = run_composer(\n\t\t\tComposer::WordWrapper { trim: true },\n\t\t\ttext,\n\t\t\twidth as u16,\n\t\t);\n\t\tlet (word_wrapper_multi_space, _) = run_composer(\n\t\t\tComposer::WordWrapper { trim: true },\n\t\t\ttext_multi_space,\n\t\t\twidth as u16,\n\t\t);\n\t\tlet (line_truncator, _) =\n\t\t\trun_composer(Composer::LineTruncator, text, width as u16);\n\n\t\tlet word_wrapped = vec![\n\t\t\t\"abcd efghij\",\n\t\t\t\"klmnopabcd efgh\",\n\t\t\t\"ijklmnopabcdefg\",\n\t\t\t\"hijkl mnopab c d e f\",\n\t\t\t\"g h i j k l m n o\",\n\t\t];\n\t\tassert_eq!(word_wrapper_single_space, word_wrapped);\n\t\tassert_eq!(word_wrapper_multi_space, word_wrapped);\n\n\t\tassert_eq!(line_truncator, vec![&text[..width]]);\n\t}\n\n\t#[test]\n\tfn line_composer_zero_width() {\n\t\tlet width = 0;\n\t\tlet text = \"abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab \";\n\t\tlet (word_wrapper, _) = run_composer(\n\t\t\tComposer::WordWrapper { trim: true },\n\t\t\ttext,\n\t\t\twidth,\n\t\t);\n\t\tlet (line_truncator, _) =\n\t\t\trun_composer(Composer::LineTruncator, text, width);\n\n\t\tlet expected: Vec<&str> = Vec::new();\n\t\tassert_eq!(word_wrapper, expected);\n\t\tassert_eq!(line_truncator, expected);\n\t}\n\n\t#[test]\n\tfn line_composer_max_line_width_of_1() {\n\t\tlet width = 1;\n\t\tlet text = \"abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab \";\n\t\tlet (word_wrapper, _) = run_composer(\n\t\t\tComposer::WordWrapper { trim: true },\n\t\t\ttext,\n\t\t\twidth,\n\t\t);\n\t\tlet (line_truncator, _) =\n\t\t\trun_composer(Composer::LineTruncator, text, width);\n\n\t\tlet expected: Vec<&str> =\n\t\t\tUnicodeSegmentation::graphemes(text, true)\n\t\t\t\t.filter(|g| g.chars().any(|c| !c.is_whitespace()))\n\t\t\t\t.collect();\n\t\tassert_eq!(word_wrapper, expected);\n\t\tassert_eq!(line_truncator, vec![\"a\"]);\n\t}\n\n\t#[test]\n\tfn line_composer_max_line_width_of_1_double_width_characters() {\n\t\tlet width = 1;\n\t\tlet text = \"コンピュータ上で文字を扱う場合、典型的には文字\\naaaによる通信を行う場合にその\\\n                    両端点では、\";\n\t\tlet (word_wrapper, _) = run_composer(\n\t\t\tComposer::WordWrapper { trim: true },\n\t\t\ttext,\n\t\t\twidth,\n\t\t);\n\t\tlet (line_truncator, _) =\n\t\t\trun_composer(Composer::LineTruncator, text, width);\n\t\tassert_eq!(word_wrapper, vec![\"\", \"a\", \"a\", \"a\"]);\n\t\tassert_eq!(line_truncator, vec![\"\", \"a\"]);\n\t}\n\n\t/// Tests `WordWrapper` with words some of which exceed line length and some not.\n\t#[test]\n\tfn line_composer_word_wrapper_mixed_length() {\n\t\tlet width = 20;\n\t\tlet text = \"abcd efghij klmnopabcdefghijklmnopabcdefghijkl mnopab cdefghi j klmno\";\n\t\tlet (word_wrapper, _) = run_composer(\n\t\t\tComposer::WordWrapper { trim: true },\n\t\t\ttext,\n\t\t\twidth,\n\t\t);\n\t\tassert_eq!(\n\t\t\tword_wrapper,\n\t\t\tvec![\n\t\t\t\t\"abcd efghij\",\n\t\t\t\t\"klmnopabcdefghijklmn\",\n\t\t\t\t\"opabcdefghijkl\",\n\t\t\t\t\"mnopab cdefghi j\",\n\t\t\t\t\"klmno\",\n\t\t\t]\n\t\t);\n\t}\n\n\t#[test]\n\tfn line_composer_double_width_chars() {\n\t\tlet width = 20;\n\t\tlet text = \"コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点\\\n                    では、\";\n\t\tlet (word_wrapper, word_wrapper_width) = run_composer(\n\t\t\tComposer::WordWrapper { trim: true },\n\t\t\ttext,\n\t\t\twidth,\n\t\t);\n\t\tlet (line_truncator, _) =\n\t\t\trun_composer(Composer::LineTruncator, text, width);\n\t\tassert_eq!(line_truncator, vec![\"コンピュータ上で文字\"]);\n\t\tlet wrapped = vec![\n\t\t\t\"コンピュータ上で文字\",\n\t\t\t\"を扱う場合、典型的に\",\n\t\t\t\"は文字による通信を行\",\n\t\t\t\"う場合にその両端点で\",\n\t\t\t\"は、\",\n\t\t];\n\t\tassert_eq!(word_wrapper, wrapped);\n\t\tassert_eq!(\n\t\t\tword_wrapper_width,\n\t\t\tvec![width, width, width, width, 4]\n\t\t);\n\t}\n\n\t#[test]\n\tfn line_composer_leading_whitespace_removal() {\n\t\tlet width = 20;\n\t\tlet text = \"AAAAAAAAAAAAAAAAAAAA    AAA\";\n\t\tlet (word_wrapper, _) = run_composer(\n\t\t\tComposer::WordWrapper { trim: true },\n\t\t\ttext,\n\t\t\twidth,\n\t\t);\n\t\tlet (line_truncator, _) =\n\t\t\trun_composer(Composer::LineTruncator, text, width);\n\t\tassert_eq!(\n\t\t\tword_wrapper,\n\t\t\tvec![\"AAAAAAAAAAAAAAAAAAAA\", \"AAA\",]\n\t\t);\n\t\tassert_eq!(line_truncator, vec![\"AAAAAAAAAAAAAAAAAAAA\"]);\n\t}\n\n\t/// Tests truncation of leading whitespace.\n\t#[test]\n\tfn line_composer_lots_of_spaces() {\n\t\tlet width = 20;\n\t\tlet text = \"                                                                     \";\n\t\tlet (word_wrapper, _) = run_composer(\n\t\t\tComposer::WordWrapper { trim: true },\n\t\t\ttext,\n\t\t\twidth,\n\t\t);\n\t\tlet (line_truncator, _) =\n\t\t\trun_composer(Composer::LineTruncator, text, width);\n\t\tassert_eq!(word_wrapper, vec![\"\"]);\n\t\tassert_eq!(line_truncator, vec![\"                    \"]);\n\t}\n\n\t/// Tests an input starting with a letter, followed by spaces - some of the behaviour is\n\t/// incidental.\n\t#[test]\n\tfn line_composer_char_plus_lots_of_spaces() {\n\t\tlet width = 20;\n\t\tlet text = \"a                                                                     \";\n\t\tlet (word_wrapper, _) = run_composer(\n\t\t\tComposer::WordWrapper { trim: true },\n\t\t\ttext,\n\t\t\twidth,\n\t\t);\n\t\tlet (line_truncator, _) =\n\t\t\trun_composer(Composer::LineTruncator, text, width);\n\t\t// What's happening below is: the first line gets consumed, trailing spaces discarded,\n\t\t// after 20 of which a word break occurs (probably shouldn't). The second line break\n\t\t// discards all whitespace. The result should probably be vec![\"a\"] but it doesn't matter\n\t\t// that much.\n\t\tassert_eq!(word_wrapper, vec![\"a\", \"\"]);\n\t\tassert_eq!(line_truncator, vec![\"a                   \"]);\n\t}\n\n\t#[test]\n\tfn line_composer_word_wrapper_double_width_chars_mixed_with_spaces(\n\t) {\n\t\tlet width = 20;\n\t\t// Japanese seems not to use spaces but we should break on spaces anyway... We're using it\n\t\t// to test double-width chars.\n\t\t// You are more than welcome to add word boundary detection based of alterations of\n\t\t// hiragana and katakana...\n\t\t// This happens to also be a test case for mixed width because regular spaces are single width.\n\t\tlet text = \"コンピュ ータ上で文字を扱う場合、 典型的には文 字による 通信を行 う場合にその両端点では、\";\n\t\tlet (word_wrapper, word_wrapper_width) = run_composer(\n\t\t\tComposer::WordWrapper { trim: true },\n\t\t\ttext,\n\t\t\twidth,\n\t\t);\n\t\tassert_eq!(\n\t\t\tword_wrapper,\n\t\t\tvec![\n\t\t\t\t\"コンピュ\",\n\t\t\t\t\"ータ上で文字を扱う場\",\n\t\t\t\t\"合、 典型的には文\",\n\t\t\t\t\"字による 通信を行\",\n\t\t\t\t\"う場合にその両端点で\",\n\t\t\t\t\"は、\",\n\t\t\t]\n\t\t);\n\t\t// Odd-sized lines have a space in them.\n\t\tassert_eq!(word_wrapper_width, vec![8, 20, 17, 17, 20, 4]);\n\t}\n\n\t/// Ensure words separated by nbsp are wrapped as if they were a single one.\n\t#[test]\n\tfn line_composer_word_wrapper_nbsp() {\n\t\tlet width = 20;\n\t\tlet text = \"AAAAAAAAAAAAAAA AAAA\\u{00a0}AAA\";\n\t\tlet (word_wrapper, _) = run_composer(\n\t\t\tComposer::WordWrapper { trim: true },\n\t\t\ttext,\n\t\t\twidth,\n\t\t);\n\t\tassert_eq!(\n\t\t\tword_wrapper,\n\t\t\tvec![\"AAAAAAAAAAAAAAA\", \"AAAA\\u{00a0}AAA\",]\n\t\t);\n\n\t\t// Ensure that if the character was a regular space, it would be wrapped differently.\n\t\tlet text_space = text.replace('\\u{00a0}', \" \");\n\t\tlet (word_wrapper_space, _) = run_composer(\n\t\t\tComposer::WordWrapper { trim: true },\n\t\t\t&text_space,\n\t\t\twidth,\n\t\t);\n\t\tassert_eq!(\n\t\t\tword_wrapper_space,\n\t\t\tvec![\"AAAAAAAAAAAAAAA AAAA\", \"AAA\",]\n\t\t);\n\t}\n\n\t#[test]\n\tfn line_composer_word_wrapper_preserve_indentation() {\n\t\tlet width = 20;\n\t\tlet text = \"AAAAAAAAAAAAAAAAAAAA    AAA\";\n\t\tlet (word_wrapper, _) = run_composer(\n\t\t\tComposer::WordWrapper { trim: false },\n\t\t\ttext,\n\t\t\twidth,\n\t\t);\n\t\tassert_eq!(\n\t\t\tword_wrapper,\n\t\t\tvec![\"AAAAAAAAAAAAAAAAAAAA\", \"   AAA\",]\n\t\t);\n\t}\n\n\t#[test]\n\tfn line_composer_word_wrapper_preserve_indentation_with_wrap() {\n\t\tlet width = 10;\n\t\tlet text = \"AAA AAA AAAAA AA AAAAAA\\n B\\n  C\\n   D\";\n\t\tlet (word_wrapper, _) = run_composer(\n\t\t\tComposer::WordWrapper { trim: false },\n\t\t\ttext,\n\t\t\twidth,\n\t\t);\n\t\tassert_eq!(\n\t\t\tword_wrapper,\n\t\t\tvec![\n\t\t\t\t\"AAA AAA\", \"AAAAA AA\", \"AAAAAA\", \" B\", \"  C\", \"   D\"\n\t\t\t]\n\t\t);\n\t}\n\n\t#[test]\n\tfn line_composer_word_wrapper_preserve_indentation_lots_of_whitespace(\n\t) {\n\t\tlet width = 10;\n\t\tlet text =\n\t\t\t\"               4 Indent\\n                 must wrap!\";\n\t\tlet (word_wrapper, _) = run_composer(\n\t\t\tComposer::WordWrapper { trim: false },\n\t\t\ttext,\n\t\t\twidth,\n\t\t);\n\t\tassert_eq!(\n\t\t\tword_wrapper,\n\t\t\tvec![\n\t\t\t\t\"          \",\n\t\t\t\t\"    4\",\n\t\t\t\t\"Indent\",\n\t\t\t\t\"          \",\n\t\t\t\t\"      must\",\n\t\t\t\t\"wrap!\"\n\t\t\t]\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "src/ui/scrollbar.rs",
    "content": "use super::style::SharedTheme;\nuse easy_cast::CastFloat;\nuse ratatui::{\n\tbuffer::Buffer,\n\tlayout::{Margin, Rect},\n\tstyle::Style,\n\tsymbols::{\n\t\tblock::FULL,\n\t\tline::{DOUBLE_HORIZONTAL, DOUBLE_VERTICAL},\n\t},\n\twidgets::Widget,\n\tFrame,\n};\n\npub enum Orientation {\n\tVertical,\n\tHorizontal,\n}\n\n///\nstruct Scrollbar {\n\tmax: u16,\n\tpos: u16,\n\tstyle_bar: Style,\n\tstyle_pos: Style,\n\torientation: Orientation,\n}\n\nimpl Scrollbar {\n\tfn new(max: usize, pos: usize, orientation: Orientation) -> Self {\n\t\tSelf {\n\t\t\tmax: u16::try_from(max).unwrap_or_default(),\n\t\t\tpos: u16::try_from(pos).unwrap_or_default(),\n\t\t\tstyle_pos: Style::default(),\n\t\t\tstyle_bar: Style::default(),\n\t\t\torientation,\n\t\t}\n\t}\n\n\tfn render_vertical(self, area: Rect, buf: &mut Buffer) {\n\t\tif area.height <= 2 {\n\t\t\treturn;\n\t\t}\n\n\t\tif self.max == 0 {\n\t\t\treturn;\n\t\t}\n\n\t\tlet right = area.right().saturating_sub(1);\n\t\tif right <= area.left() {\n\t\t\treturn;\n\t\t}\n\n\t\tlet (bar_top, bar_height) = {\n\t\t\tlet scrollbar_area = area.inner(Margin {\n\t\t\t\thorizontal: 0,\n\t\t\t\tvertical: 1,\n\t\t\t});\n\n\t\t\t(scrollbar_area.top(), scrollbar_area.height)\n\t\t};\n\n\t\tfor y in bar_top..(bar_top + bar_height) {\n\t\t\tbuf.set_string(right, y, DOUBLE_VERTICAL, self.style_bar);\n\t\t}\n\n\t\tlet progress = f32::from(self.pos) / f32::from(self.max);\n\t\tlet progress = if progress > 1.0 { 1.0 } else { progress };\n\t\tlet pos = f32::from(bar_height) * progress;\n\n\t\tlet pos: u16 = pos.cast_nearest();\n\t\tlet pos = pos.saturating_sub(1);\n\n\t\tbuf.set_string(right, bar_top + pos, FULL, self.style_pos);\n\t}\n\n\tfn render_horizontal(self, area: Rect, buf: &mut Buffer) {\n\t\tif area.width <= 2 {\n\t\t\treturn;\n\t\t}\n\n\t\tif self.max == 0 {\n\t\t\treturn;\n\t\t}\n\n\t\tlet bottom = area.bottom().saturating_sub(1);\n\t\tif bottom <= area.top() {\n\t\t\treturn;\n\t\t}\n\n\t\tlet (bar_left, bar_width) = {\n\t\t\tlet scrollbar_area = area.inner(Margin {\n\t\t\t\thorizontal: 1,\n\t\t\t\tvertical: 0,\n\t\t\t});\n\n\t\t\t(scrollbar_area.left(), scrollbar_area.width)\n\t\t};\n\n\t\tfor x in bar_left..(bar_left + bar_width) {\n\t\t\tbuf.set_string(\n\t\t\t\tx,\n\t\t\t\tbottom,\n\t\t\t\tDOUBLE_HORIZONTAL,\n\t\t\t\tself.style_bar,\n\t\t\t);\n\t\t}\n\n\t\tlet progress = f32::from(self.pos) / f32::from(self.max);\n\t\tlet progress = if progress > 1.0 { 1.0 } else { progress };\n\t\tlet pos = f32::from(bar_width) * progress;\n\n\t\tlet pos: u16 = pos.cast_nearest();\n\t\tlet pos = pos.saturating_sub(1);\n\n\t\tbuf.set_string(bar_left + pos, bottom, FULL, self.style_pos);\n\t}\n}\n\nimpl Widget for Scrollbar {\n\tfn render(self, area: Rect, buf: &mut Buffer) {\n\t\tmatch &self.orientation {\n\t\t\tOrientation::Vertical => self.render_vertical(area, buf),\n\t\t\tOrientation::Horizontal => {\n\t\t\t\tself.render_horizontal(area, buf);\n\t\t\t}\n\t\t}\n\t}\n}\n\npub fn draw_scrollbar(\n\tf: &mut Frame,\n\tr: Rect,\n\ttheme: &SharedTheme,\n\tmax: usize,\n\tpos: usize,\n\torientation: Orientation,\n) {\n\tlet mut widget = Scrollbar::new(max, pos, orientation);\n\twidget.style_pos = theme.scroll_bar_pos();\n\tf.render_widget(widget, r);\n}\n"
  },
  {
    "path": "src/ui/scrolllist.rs",
    "content": "use super::style::SharedTheme;\nuse ratatui::{\n\tbuffer::Buffer,\n\tlayout::Rect,\n\tstyle::Style,\n\ttext::{Span, Text},\n\twidgets::{Block, Borders, List, ListItem, Widget},\n\tFrame,\n};\n\n///\nstruct ScrollableList<'b, L, S>\nwhere\n\tS: Into<Text<'b>>,\n\tL: Iterator<Item = S>,\n{\n\tblock: Option<Block<'b>>,\n\t/// Items to be displayed\n\titems: L,\n\t/// Base style of the widget\n\tstyle: Style,\n}\n\nimpl<'b, L, S> ScrollableList<'b, L, S>\nwhere\n\tS: Into<Text<'b>>,\n\tL: Iterator<Item = S>,\n{\n\tfn new(items: L) -> Self {\n\t\tSelf {\n\t\t\tblock: None,\n\t\t\titems,\n\t\t\tstyle: Style::default(),\n\t\t}\n\t}\n\n\tfn block(mut self, block: Block<'b>) -> Self {\n\t\tself.block = Some(block);\n\t\tself\n\t}\n}\n\nimpl<'b, L, S> Widget for ScrollableList<'b, L, S>\nwhere\n\tS: Into<Text<'b>>,\n\tL: Iterator<Item = S>,\n{\n\tfn render(self, area: Rect, buf: &mut Buffer) {\n\t\t// Render items\n\t\tList::new(self.items.map(ListItem::new))\n\t\t\t.block(self.block.unwrap_or_default())\n\t\t\t.style(self.style)\n\t\t\t.render(area, buf);\n\t}\n}\n\npub fn draw_list<'b, L, S>(\n\tf: &mut Frame,\n\tr: Rect,\n\ttitle: &'b str,\n\titems: L,\n\tselected: bool,\n\ttheme: &SharedTheme,\n) where\n\tS: Into<Text<'b>>,\n\tL: Iterator<Item = S>,\n{\n\tlet list = ScrollableList::new(items).block(\n\t\tBlock::default()\n\t\t\t.title(Span::styled(title, theme.title(selected)))\n\t\t\t.borders(Borders::ALL)\n\t\t\t.border_style(theme.block(selected)),\n\t);\n\tf.render_widget(list, r);\n}\n\npub fn draw_list_block<'b, L, S>(\n\tf: &mut Frame,\n\tr: Rect,\n\tblock: Block<'b>,\n\titems: L,\n) where\n\tS: Into<Text<'b>>,\n\tL: Iterator<Item = S>,\n{\n\tlet list = ScrollableList::new(items).block(block);\n\tf.render_widget(list, r);\n}\n"
  },
  {
    "path": "src/ui/stateful_paragraph.rs",
    "content": "use easy_cast::Cast;\nuse ratatui::{\n\tbuffer::Buffer,\n\tlayout::{Alignment, Position, Rect},\n\tstyle::Style,\n\ttext::{StyledGrapheme, Text},\n\twidgets::{Block, StatefulWidget, Widget, Wrap},\n};\nuse std::iter;\nuse unicode_width::UnicodeWidthStr;\n\nuse super::reflow::{LineComposer, LineTruncator, WordWrapper};\n\nconst fn get_line_offset(\n\tline_width: u16,\n\ttext_area_width: u16,\n\talignment: Alignment,\n) -> u16 {\n\tmatch alignment {\n\t\tAlignment::Center => {\n\t\t\t(text_area_width / 2).saturating_sub(line_width / 2)\n\t\t}\n\t\tAlignment::Right => {\n\t\t\ttext_area_width.saturating_sub(line_width)\n\t\t}\n\t\tAlignment::Left => 0,\n\t}\n}\n\n#[derive(Debug, Clone)]\npub struct StatefulParagraph<'a> {\n\t/// A block to wrap the widget in\n\tblock: Option<Block<'a>>,\n\t/// Widget style\n\tstyle: Style,\n\t/// How to wrap the text\n\twrap: Option<Wrap>,\n\t/// The text to display\n\ttext: Text<'a>,\n\t/// Alignment of the text\n\talignment: Alignment,\n}\n\n#[derive(Debug, Default, Clone, Copy)]\npub struct ScrollPos {\n\tpub x: u16,\n\tpub y: u16,\n}\n\n#[derive(Debug, Copy, Clone, Default)]\npub struct ParagraphState {\n\t/// Scroll\n\tscroll: ScrollPos,\n\t/// after all wrapping this is the amount of lines\n\tlines: u16,\n\t/// last visible height\n\theight: u16,\n}\n\nimpl ParagraphState {\n\tpub const fn lines(self) -> u16 {\n\t\tself.lines\n\t}\n\n\tpub const fn height(self) -> u16 {\n\t\tself.height\n\t}\n\n\tpub const fn scroll(self) -> ScrollPos {\n\t\tself.scroll\n\t}\n\n\tpub const fn set_scroll(&mut self, scroll: ScrollPos) {\n\t\tself.scroll = scroll;\n\t}\n}\n\nimpl<'a> StatefulParagraph<'a> {\n\tpub fn new<T>(text: T) -> Self\n\twhere\n\t\tT: Into<Text<'a>>,\n\t{\n\t\tSelf {\n\t\t\tblock: None,\n\t\t\tstyle: Style::default(),\n\t\t\twrap: None,\n\t\t\ttext: text.into(),\n\t\t\talignment: Alignment::Left,\n\t\t}\n\t}\n\n\tpub fn block(mut self, block: Block<'a>) -> Self {\n\t\tself.block = Some(block);\n\t\tself\n\t}\n\n\tpub const fn wrap(mut self, wrap: Wrap) -> Self {\n\t\tself.wrap = Some(wrap);\n\t\tself\n\t}\n}\n\nimpl StatefulWidget for StatefulParagraph<'_> {\n\ttype State = ParagraphState;\n\n\tfn render(\n\t\tmut self,\n\t\tarea: Rect,\n\t\tbuf: &mut Buffer,\n\t\tstate: &mut Self::State,\n\t) {\n\t\tbuf.set_style(area, self.style);\n\t\tlet text_area = self.block.take().map_or(area, |b| {\n\t\t\tlet inner_area = b.inner(area);\n\t\t\tb.render(area, buf);\n\t\t\tinner_area\n\t\t});\n\n\t\tif text_area.height < 1 {\n\t\t\treturn;\n\t\t}\n\n\t\tlet style = self.style;\n\t\tlet mut styled = self.text.lines.iter().flat_map(|line| {\n\t\t\tline.spans\n\t\t\t\t.iter()\n\t\t\t\t.flat_map(|span| span.styled_graphemes(style))\n\t\t\t\t// Required given the way composers work but might be refactored out if we change\n\t\t\t\t// composers to operate on lines instead of a stream of graphemes.\n\t\t\t\t.chain(iter::once(StyledGrapheme {\n\t\t\t\t\tsymbol: \"\\n\",\n\t\t\t\t\tstyle: self.style,\n\t\t\t\t}))\n\t\t});\n\n\t\tlet mut line_composer: Box<dyn LineComposer> =\n\t\t\tif let Some(Wrap { trim }) = self.wrap {\n\t\t\t\tBox::new(WordWrapper::new(\n\t\t\t\t\t&mut styled,\n\t\t\t\t\ttext_area.width,\n\t\t\t\t\ttrim,\n\t\t\t\t))\n\t\t\t} else {\n\t\t\t\tlet mut line_composer = Box::new(LineTruncator::new(\n\t\t\t\t\t&mut styled,\n\t\t\t\t\ttext_area.width,\n\t\t\t\t));\n\t\t\t\tif self.alignment == Alignment::Left {\n\t\t\t\t\tline_composer\n\t\t\t\t\t\t.set_horizontal_offset(state.scroll.x);\n\t\t\t\t}\n\t\t\t\tline_composer\n\t\t\t};\n\t\tlet mut y = 0;\n\t\tlet mut end_reached = false;\n\t\twhile let Some((current_line, current_line_width)) =\n\t\t\tline_composer.next_line()\n\t\t{\n\t\t\tif !end_reached && y >= state.scroll.y {\n\t\t\t\tlet mut x = get_line_offset(\n\t\t\t\t\tcurrent_line_width,\n\t\t\t\t\ttext_area.width,\n\t\t\t\t\tself.alignment,\n\t\t\t\t);\n\t\t\t\tfor StyledGrapheme { symbol, style } in current_line {\n\t\t\t\t\tbuf.cell_mut(Position::new(\n\t\t\t\t\t\ttext_area.left() + x,\n\t\t\t\t\t\ttext_area.top() + y - state.scroll.y,\n\t\t\t\t\t))\n\t\t\t\t\t.map(|cell| {\n\t\t\t\t\t\tcell.set_symbol(if symbol.is_empty() {\n\t\t\t\t\t\t\t// If the symbol is empty, the last char which rendered last time will\n\t\t\t\t\t\t\t// leave on the line. It's a quick fix.\n\t\t\t\t\t\t\t\" \"\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tsymbol\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.set_style(*style)\n\t\t\t\t\t});\n\t\t\t\t\tx += Cast::<u16>::cast(symbol.width());\n\t\t\t\t}\n\t\t\t}\n\t\t\ty += 1;\n\t\t\tif y >= text_area.height + state.scroll.y {\n\t\t\t\tend_reached = true;\n\t\t\t}\n\t\t}\n\n\t\tstate.lines = y;\n\t\tstate.height = area.height;\n\t}\n}\n"
  },
  {
    "path": "src/ui/style.rs",
    "content": "use crate::ui::syntax_text::DEFAULT_SYNTAX_THEME;\nuse anyhow::Result;\nuse asyncgit::{DiffLineType, StatusItemType};\nuse ratatui::style::{Color, Modifier, Style};\nuse ron::ser::{to_string_pretty, PrettyConfig};\nuse serde::{Deserialize, Serialize};\nuse std::{fs::File, io::Write, path::PathBuf, rc::Rc};\nuse struct_patch::Patch;\n\npub type SharedTheme = Rc<Theme>;\n\n#[derive(Serialize, Deserialize, Debug, Clone, Patch)]\n#[patch(attribute(derive(Serialize, Deserialize)))]\npub struct Theme {\n\tselected_tab: Color,\n\tcommand_fg: Color,\n\tselection_bg: Color,\n\tselection_fg: Color,\n\tuse_selection_fg: bool,\n\tcmdbar_bg: Color,\n\tdisabled_fg: Color,\n\tdiff_line_add: Color,\n\tdiff_line_delete: Color,\n\tdiff_file_added: Color,\n\tdiff_file_removed: Color,\n\tdiff_file_moved: Color,\n\tdiff_file_modified: Color,\n\tcommit_hash: Color,\n\tcommit_time: Color,\n\tcommit_author: Color,\n\tdanger_fg: Color,\n\tpush_gauge_bg: Color,\n\tpush_gauge_fg: Color,\n\ttag_fg: Color,\n\tbranch_fg: Color,\n\tline_break: String,\n\tblock_title_focused: Color,\n\tsyntax: String,\n}\n\nimpl Theme {\n\tpub fn scroll_bar_pos(&self) -> Style {\n\t\tStyle::default().fg(self.selection_bg)\n\t}\n\n\tpub fn block(&self, focus: bool) -> Style {\n\t\tif focus {\n\t\t\tStyle::default()\n\t\t} else {\n\t\t\tStyle::default().fg(self.disabled_fg)\n\t\t}\n\t}\n\n\tpub fn title(&self, focused: bool) -> Style {\n\t\tif focused {\n\t\t\tStyle::default()\n\t\t\t\t.fg(self.block_title_focused)\n\t\t\t\t.add_modifier(Modifier::BOLD)\n\t\t} else {\n\t\t\tStyle::default().fg(self.disabled_fg)\n\t\t}\n\t}\n\n\tpub fn branch(&self, selected: bool, head: bool) -> Style {\n\t\tlet branch = if head {\n\t\t\tStyle::default().add_modifier(Modifier::BOLD)\n\t\t} else {\n\t\t\tStyle::default()\n\t\t}\n\t\t.fg(self.branch_fg);\n\n\t\tif selected {\n\t\t\tbranch.patch(Style::default().bg(self.selection_bg))\n\t\t} else {\n\t\t\tbranch\n\t\t}\n\t}\n\n\tpub fn tab(&self, selected: bool) -> Style {\n\t\tif selected {\n\t\t\tself.text(true, false)\n\t\t\t\t.fg(self.selected_tab)\n\t\t\t\t.add_modifier(Modifier::UNDERLINED)\n\t\t} else {\n\t\t\tself.text(false, false)\n\t\t}\n\t}\n\n\tpub fn tags(&self, selected: bool) -> Style {\n\t\tStyle::default()\n\t\t\t.fg(self.tag_fg)\n\t\t\t.add_modifier(Modifier::BOLD)\n\t\t\t.bg(if selected {\n\t\t\t\tself.selection_bg\n\t\t\t} else {\n\t\t\t\tColor::Reset\n\t\t\t})\n\t}\n\n\tpub fn text(&self, enabled: bool, selected: bool) -> Style {\n\t\tmatch (enabled, selected) {\n\t\t\t(false, false) => Style::default().fg(self.disabled_fg),\n\t\t\t(false, true) => Style::default().bg(self.selection_bg),\n\t\t\t(true, false) => Style::default(),\n\t\t\t(true, true) => Style::default()\n\t\t\t\t.fg(self.command_fg)\n\t\t\t\t.bg(self.selection_bg),\n\t\t}\n\t}\n\n\tpub fn item(&self, typ: StatusItemType, selected: bool) -> Style {\n\t\tlet style = match typ {\n\t\t\tStatusItemType::New => {\n\t\t\t\tStyle::default().fg(self.diff_file_added)\n\t\t\t}\n\t\t\tStatusItemType::Modified => {\n\t\t\t\tStyle::default().fg(self.diff_file_modified)\n\t\t\t}\n\t\t\tStatusItemType::Deleted => {\n\t\t\t\tStyle::default().fg(self.diff_file_removed)\n\t\t\t}\n\t\t\tStatusItemType::Renamed => {\n\t\t\t\tStyle::default().fg(self.diff_file_moved)\n\t\t\t}\n\t\t\tStatusItemType::Conflicted => Style::default()\n\t\t\t\t.fg(self.diff_file_modified)\n\t\t\t\t.add_modifier(Modifier::BOLD),\n\t\t\tStatusItemType::Typechange => Style::default(),\n\t\t};\n\n\t\tself.apply_select(style, selected)\n\t}\n\n\tpub fn file_tree_item(\n\t\t&self,\n\t\tis_folder: bool,\n\t\tselected: bool,\n\t) -> Style {\n\t\tlet style = if is_folder {\n\t\t\tStyle::default()\n\t\t} else {\n\t\t\tStyle::default().fg(self.diff_file_modified)\n\t\t};\n\n\t\tself.apply_select(style, selected)\n\t}\n\n\tconst fn apply_select(\n\t\t&self,\n\t\tstyle: Style,\n\t\tselected: bool,\n\t) -> Style {\n\t\tif selected {\n\t\t\tif self.use_selection_fg {\n\t\t\t\tstyle.bg(self.selection_bg).fg(self.selection_fg)\n\t\t\t} else {\n\t\t\t\tstyle.bg(self.selection_bg)\n\t\t\t}\n\t\t} else {\n\t\t\tstyle\n\t\t}\n\t}\n\n\tpub fn option(&self, on: bool) -> Style {\n\t\tif on {\n\t\t\tStyle::default().fg(self.diff_line_add)\n\t\t} else {\n\t\t\tStyle::default().fg(self.diff_line_delete)\n\t\t}\n\t}\n\n\tpub fn diff_hunk_marker(&self, selected: bool) -> Style {\n\t\tif selected {\n\t\t\tStyle::default().bg(self.selection_bg)\n\t\t} else {\n\t\t\tStyle::default().fg(self.disabled_fg)\n\t\t}\n\t}\n\n\tpub fn diff_line(\n\t\t&self,\n\t\ttyp: DiffLineType,\n\t\tselected: bool,\n\t) -> Style {\n\t\tlet style = match typ {\n\t\t\tDiffLineType::Add => {\n\t\t\t\tStyle::default().fg(self.diff_line_add)\n\t\t\t}\n\t\t\tDiffLineType::Delete => {\n\t\t\t\tStyle::default().fg(self.diff_line_delete)\n\t\t\t}\n\t\t\tDiffLineType::Header => Style::default()\n\t\t\t\t.fg(self.disabled_fg)\n\t\t\t\t.add_modifier(Modifier::BOLD),\n\t\t\tDiffLineType::None => Style::default().fg(if selected {\n\t\t\t\tself.command_fg\n\t\t\t} else {\n\t\t\t\tColor::Reset\n\t\t\t}),\n\t\t};\n\n\t\tself.apply_select(style, selected)\n\t}\n\n\tpub fn text_danger(&self) -> Style {\n\t\tStyle::default().fg(self.danger_fg)\n\t}\n\n\tpub fn line_break(&self) -> String {\n\t\tself.line_break.clone()\n\t}\n\n\tpub fn commandbar(&self, enabled: bool) -> Style {\n\t\tif enabled {\n\t\t\tStyle::default().fg(self.command_fg)\n\t\t} else {\n\t\t\tStyle::default().fg(self.disabled_fg)\n\t\t}\n\t\t.bg(self.cmdbar_bg)\n\t}\n\n\tpub fn commit_hash(&self, selected: bool) -> Style {\n\t\tself.apply_select(\n\t\t\tStyle::default().fg(self.commit_hash),\n\t\t\tselected,\n\t\t)\n\t}\n\n\tpub fn commit_unhighlighted(&self) -> Style {\n\t\tStyle::default().fg(self.disabled_fg)\n\t}\n\n\tpub fn log_marker(&self, selected: bool) -> Style {\n\t\tlet mut style = Style::default()\n\t\t\t.fg(self.commit_author)\n\t\t\t.add_modifier(Modifier::BOLD);\n\n\t\tstyle = self.apply_select(style, selected);\n\n\t\tstyle\n\t}\n\n\tpub fn commit_time(&self, selected: bool) -> Style {\n\t\tself.apply_select(\n\t\t\tStyle::default().fg(self.commit_time),\n\t\t\tselected,\n\t\t)\n\t}\n\n\tpub fn commit_author(&self, selected: bool) -> Style {\n\t\tself.apply_select(\n\t\t\tStyle::default().fg(self.commit_author),\n\t\t\tselected,\n\t\t)\n\t}\n\n\tpub fn commit_hash_in_blame(\n\t\t&self,\n\t\tis_blamed_commit: bool,\n\t) -> Style {\n\t\tif is_blamed_commit {\n\t\t\tStyle::default()\n\t\t\t\t.fg(self.commit_hash)\n\t\t\t\t.add_modifier(Modifier::BOLD)\n\t\t} else {\n\t\t\tStyle::default().fg(self.commit_hash)\n\t\t}\n\t}\n\n\tpub fn push_gauge(&self) -> Style {\n\t\tStyle::default()\n\t\t\t.fg(self.push_gauge_fg)\n\t\t\t.bg(self.push_gauge_bg)\n\t}\n\n\tpub fn attention_block() -> Style {\n\t\tStyle::default().fg(Color::Yellow)\n\t}\n\n\tfn load_patch(theme_path: &PathBuf) -> Result<ThemePatch> {\n\t\tlet file = File::open(theme_path)?;\n\n\t\tOk(ron::de::from_reader(file)?)\n\t}\n\n\tfn load_old_theme(theme_path: &PathBuf) -> Result<Self> {\n\t\tlet old_file = File::open(theme_path)?;\n\n\t\tOk(ron::de::from_reader::<File, Self>(old_file)?)\n\t}\n\n\t// This is supposed to be called when theme.ron doesn't already exists.\n\tfn save_patch(&self, theme_path: &PathBuf) -> Result<()> {\n\t\tlet mut file = File::create(theme_path)?;\n\t\tlet patch = self.clone().into_patch_by_diff(Self::default());\n\t\tlet data = to_string_pretty(&patch, PrettyConfig::default())?;\n\n\t\tfile.write_all(data.as_bytes())?;\n\n\t\tOk(())\n\t}\n\n\tpub fn get_syntax(&self) -> String {\n\t\tself.syntax.clone()\n\t}\n\n\tpub fn init(theme_path: &PathBuf) -> Self {\n\t\tlet mut theme = Self::default();\n\n\t\tif let Ok(patch) = Self::load_patch(theme_path).map_err(|e| {\n\t\t\tlog::error!(\"theme error [{theme_path:?}]: {e}\");\n\t\t\te\n\t\t}) {\n\t\t\ttheme.apply(patch);\n\t\t} else if let Ok(old_theme) = Self::load_old_theme(theme_path)\n\t\t{\n\t\t\ttheme = old_theme;\n\n\t\t\tif theme.save_patch(theme_path).is_ok() {\n\t\t\t\tlog::info!(\"Converted old theme to new format. ({theme_path:?})\");\n\t\t\t} else {\n\t\t\t\tlog::warn!(\"Failed to save theme in new format. ({theme_path:?})\");\n\t\t\t}\n\t\t}\n\n\t\ttheme\n\t}\n}\n\nimpl Default for Theme {\n\tfn default() -> Self {\n\t\tSelf {\n\t\t\tselected_tab: Color::Reset,\n\t\t\tcommand_fg: Color::White,\n\t\t\tselection_bg: Color::Blue,\n\t\t\tselection_fg: Color::White,\n\t\t\tuse_selection_fg: true,\n\t\t\tcmdbar_bg: Color::Blue,\n\t\t\tdisabled_fg: Color::DarkGray,\n\t\t\tdiff_line_add: Color::Green,\n\t\t\tdiff_line_delete: Color::Red,\n\t\t\tdiff_file_added: Color::LightGreen,\n\t\t\tdiff_file_removed: Color::LightRed,\n\t\t\tdiff_file_moved: Color::LightMagenta,\n\t\t\tdiff_file_modified: Color::Yellow,\n\t\t\tcommit_hash: Color::Magenta,\n\t\t\tcommit_time: Color::LightCyan,\n\t\t\tcommit_author: Color::Green,\n\t\t\tdanger_fg: Color::Red,\n\t\t\tpush_gauge_bg: Color::Blue,\n\t\t\tpush_gauge_fg: Color::Reset,\n\t\t\ttag_fg: Color::LightMagenta,\n\t\t\tbranch_fg: Color::LightYellow,\n\t\t\tline_break: \"¶\".to_string(),\n\t\t\tblock_title_focused: Color::Reset,\n\t\t\t// Available themes can be found in:\n\t\t\t// [ThemeSet::load_defaults function](https://github.com/trishume/syntect/blob/7fe13c0fd53cdfa0f9fea1aa14c5ba37f81d8b71/src/dumps.rs#L215).\n\t\t\tsyntax: DEFAULT_SYNTAX_THEME.to_string(),\n\t\t}\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse pretty_assertions::assert_eq;\n\tuse tempfile::NamedTempFile;\n\n\t#[test]\n\tfn test_smoke() {\n\t\tlet _ = env_logger::builder()\n\t\t\t.is_test(true)\n\t\t\t.filter_level(log::LevelFilter::Trace)\n\t\t\t.try_init();\n\n\t\tlet mut file = NamedTempFile::new().unwrap();\n\n\t\twriteln!(\n\t\t\tfile,\n\t\t\tr##\"\n(\n\tselection_bg: Some(\"Black\"),\n\tselection_fg: Some(\"#ffffff\"),\n\tuse_selection_fg: Some(false),\n\tsyntax: Some(\"InspiredGitHub\")\n)\n\"##\n\t\t)\n\t\t.unwrap();\n\n\t\tlet theme = Theme::init(&file.path().to_path_buf());\n\n\t\tassert_eq!(theme.selected_tab, Theme::default().selected_tab);\n\n\t\tassert_ne!(theme.selection_bg, Theme::default().selection_bg);\n\t\tassert_ne!(theme.syntax, Theme::default().syntax);\n\t\tassert_eq!(theme.selection_bg, Color::Black);\n\t\tassert_eq!(theme.selection_fg, Color::Rgb(255, 255, 255));\n\t\tassert_eq!(theme.syntax, \"InspiredGitHub\");\n\t}\n}\n"
  },
  {
    "path": "src/ui/syntax_text.rs",
    "content": "use asyncgit::{\n\tasyncjob::{AsyncJob, RunParams},\n\tProgressPercent,\n};\nuse once_cell::sync::{Lazy, OnceCell};\nuse ratatui::text::{Line, Span};\nuse scopetime::scope_time;\nuse std::{\n\tops::Range,\n\tpath::{Path, PathBuf},\n\tsync::{Arc, Mutex},\n\ttime::{Duration, Instant},\n};\nuse syntect::{\n\thighlighting::{\n\t\tFontStyle, HighlightState, Highlighter,\n\t\tRangedHighlightIterator, Style, Theme, ThemeSet,\n\t},\n\tparsing::{ParseState, ScopeStack, SyntaxSet},\n};\n\nuse crate::{AsyncAppNotification, SyntaxHighlightProgress};\n\npub const DEFAULT_SYNTAX_THEME: &str = \"base16-eighties.dark\";\n\nstruct SyntaxLine {\n\titems: Vec<(Style, usize, Range<usize>)>,\n}\n\npub struct SyntaxText {\n\ttext: String,\n\tlines: Vec<SyntaxLine>,\n\tpath: PathBuf,\n}\n\nstatic SYNTAX_SET: Lazy<SyntaxSet> =\n\tLazy::new(two_face::syntax::extra_no_newlines);\nstatic THEME: OnceCell<Theme> = OnceCell::new();\n\npub struct AsyncProgressBuffer {\n\tcurrent: usize,\n\ttotal: usize,\n\tlast_send: Option<Instant>,\n\tmin_interval: Duration,\n}\n\nimpl AsyncProgressBuffer {\n\tpub const fn new(total: usize, min_interval: Duration) -> Self {\n\t\tSelf {\n\t\t\tcurrent: 0,\n\t\t\ttotal,\n\t\t\tlast_send: None,\n\t\t\tmin_interval,\n\t\t}\n\t}\n\n\tpub fn send_progress(&mut self) -> ProgressPercent {\n\t\tself.last_send = Some(Instant::now());\n\t\tProgressPercent::new(self.current, self.total)\n\t}\n\n\tpub fn update(&mut self, current: usize) -> bool {\n\t\tself.current = current;\n\t\tself.last_send.is_none_or(|last_send| {\n\t\t\tlast_send.elapsed() > self.min_interval\n\t\t})\n\t}\n}\n\nimpl SyntaxText {\n\tpub fn new(\n\t\ttext: String,\n\t\tfile_path: &Path,\n\t\tparams: &RunParams<AsyncAppNotification, ProgressPercent>,\n\t\tsyntax: &str,\n\t) -> asyncgit::Result<Self> {\n\t\tscope_time!(\"syntax_highlighting\");\n\t\tlet mut state = {\n\t\t\tscope_time!(\"syntax_highlighting.0\");\n\t\t\tlet plain_text = || SYNTAX_SET.find_syntax_plain_text();\n\t\t\tlet syntax = SYNTAX_SET\n\t\t\t\t.find_syntax_for_file(file_path)\n\t\t\t\t.unwrap_or_else(|e| {\n\t\t\t\t\tlog::error!(\"Could not read the file to detect its syntax: {e}\");\n\t\t\t\t\tSome(plain_text())\n\t\t\t\t})\n\t\t\t\t.unwrap_or_else(plain_text);\n\n\t\t\tParseState::new(syntax)\n\t\t};\n\n\t\tlet theme = THEME.get_or_try_init(|| -> Result<Theme, asyncgit::Error> {\n\t\t\tlet theme_path = crate::args::get_app_config_path()\n\t\t\t\t.map_err(|e| asyncgit::Error::Generic(e.to_string()))?.join(format!(\"{syntax}.tmTheme\"));\n\n\t\t\tmatch ThemeSet::get_theme(&theme_path) {\n\t\t\t\tOk(t) => return Ok(t),\n\t\t\t    Err(e) => log::info!(\"could not load '{}': {e}, trying from the set of default themes\", theme_path.display()),\n\t\t\t}\n\n\t\t\tlet mut theme_set = ThemeSet::load_defaults();\n\t\t\tif let Some(t) = theme_set.themes.remove(syntax) {\n\t\t\t    return Ok(t);\n\t\t\t}\n\n\t\t\tlog::error!(\"the syntax theme '{syntax}' cannot be found. Using default theme ('{DEFAULT_SYNTAX_THEME}') instead\");\n\t\t\tOk(theme_set.themes.remove(DEFAULT_SYNTAX_THEME).expect(\"the default theme should be there\"))\n\t\t})?;\n\n\t\tlet highlighter = Highlighter::new(theme);\n\t\tlet mut syntax_lines: Vec<SyntaxLine> = Vec::new();\n\n\t\tlet mut highlight_state =\n\t\t\tHighlightState::new(&highlighter, ScopeStack::new());\n\n\t\t{\n\t\t\tlet total_count = text.lines().count();\n\n\t\t\tlet mut buffer = AsyncProgressBuffer::new(\n\t\t\t\ttotal_count,\n\t\t\t\tDuration::from_millis(200),\n\t\t\t);\n\t\t\tparams.set_progress(buffer.send_progress())?;\n\t\t\tparams.send(AsyncAppNotification::SyntaxHighlighting(\n\t\t\t\tSyntaxHighlightProgress::Progress,\n\t\t\t))?;\n\n\t\t\tfor (number, line) in text.lines().enumerate() {\n\t\t\t\tlet ops = state\n\t\t\t\t\t.parse_line(line, &SYNTAX_SET)\n\t\t\t\t\t.map_err(|e| {\n\t\t\t\t\t\tlog::error!(\"syntax error: {e:?}\");\n\t\t\t\t\t\tasyncgit::Error::Generic(\n\t\t\t\t\t\t\t\"syntax error\".to_string(),\n\t\t\t\t\t\t)\n\t\t\t\t\t})?;\n\t\t\t\tlet iter = RangedHighlightIterator::new(\n\t\t\t\t\t&mut highlight_state,\n\t\t\t\t\t&ops[..],\n\t\t\t\t\tline,\n\t\t\t\t\t&highlighter,\n\t\t\t\t);\n\n\t\t\t\tsyntax_lines.push(SyntaxLine {\n\t\t\t\t\titems: iter\n\t\t\t\t\t\t.map(|(style, _, range)| {\n\t\t\t\t\t\t\t(style, number, range)\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.collect(),\n\t\t\t\t});\n\n\t\t\t\tif buffer.update(number) {\n\t\t\t\t\tparams.set_progress(buffer.send_progress())?;\n\t\t\t\t\tparams.send(\n\t\t\t\t\t\tAsyncAppNotification::SyntaxHighlighting(\n\t\t\t\t\t\t\tSyntaxHighlightProgress::Progress,\n\t\t\t\t\t\t),\n\t\t\t\t\t)?;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tOk(Self {\n\t\t\ttext,\n\t\t\tlines: syntax_lines,\n\t\t\tpath: file_path.into(),\n\t\t})\n\t}\n\n\t///\n\tpub fn path(&self) -> &Path {\n\t\t&self.path\n\t}\n}\n\nimpl<'a> From<&'a SyntaxText> for ratatui::text::Text<'a> {\n\tfn from(v: &'a SyntaxText) -> Self {\n\t\tlet mut result_lines: Vec<Line> =\n\t\t\tVec::with_capacity(v.lines.len());\n\n\t\tfor (syntax_line, line_content) in\n\t\t\tv.lines.iter().zip(v.text.lines())\n\t\t{\n\t\t\tlet mut line_span: Line =\n\t\t\t\tVec::with_capacity(syntax_line.items.len()).into();\n\n\t\t\tfor (style, _, range) in &syntax_line.items {\n\t\t\t\tlet item_content = &line_content[range.clone()];\n\t\t\t\tlet item_style = syntact_style_to_tui(style);\n\n\t\t\t\tline_span\n\t\t\t\t\t.spans\n\t\t\t\t\t.push(Span::styled(item_content, item_style));\n\t\t\t}\n\n\t\t\tresult_lines.push(line_span);\n\t\t}\n\n\t\tresult_lines.into()\n\t}\n}\n\nfn syntact_style_to_tui(style: &Style) -> ratatui::style::Style {\n\tlet mut res = ratatui::style::Style::default().fg(\n\t\tratatui::style::Color::Rgb(\n\t\t\tstyle.foreground.r,\n\t\t\tstyle.foreground.g,\n\t\t\tstyle.foreground.b,\n\t\t),\n\t);\n\n\tif style.font_style.contains(FontStyle::BOLD) {\n\t\tres = res.add_modifier(ratatui::style::Modifier::BOLD);\n\t}\n\tif style.font_style.contains(FontStyle::ITALIC) {\n\t\tres = res.add_modifier(ratatui::style::Modifier::ITALIC);\n\t}\n\tif style.font_style.contains(FontStyle::UNDERLINE) {\n\t\tres = res.add_modifier(ratatui::style::Modifier::UNDERLINED);\n\t}\n\n\tres\n}\n\nenum JobState {\n\tRequest((String, String)),\n\tResponse(SyntaxText),\n}\n\n#[derive(Clone, Default)]\npub struct AsyncSyntaxJob {\n\tstate: Arc<Mutex<Option<JobState>>>,\n\tsyntax: String,\n}\n\nimpl AsyncSyntaxJob {\n\tpub fn new(\n\t\tcontent: String,\n\t\tpath: String,\n\t\tsyntax: String,\n\t) -> Self {\n\t\tSelf {\n\t\t\tstate: Arc::new(Mutex::new(Some(JobState::Request((\n\t\t\t\tcontent, path,\n\t\t\t))))),\n\t\t\tsyntax,\n\t\t}\n\t}\n\n\t///\n\tpub fn result(&self) -> Option<SyntaxText> {\n\t\tif let Ok(mut state) = self.state.lock() {\n\t\t\tif let Some(state) = state.take() {\n\t\t\t\treturn match state {\n\t\t\t\t\tJobState::Request(_) => None,\n\t\t\t\t\tJobState::Response(text) => Some(text),\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tNone\n\t}\n}\n\nimpl AsyncJob for AsyncSyntaxJob {\n\ttype Notification = AsyncAppNotification;\n\ttype Progress = ProgressPercent;\n\n\tfn run(\n\t\t&mut self,\n\t\tparams: RunParams<Self::Notification, Self::Progress>,\n\t) -> asyncgit::Result<Self::Notification> {\n\t\tlet mut state_mutex = self.state.lock()?;\n\n\t\tif let Some(state) = state_mutex.take() {\n\t\t\t*state_mutex = Some(match state {\n\t\t\t\tJobState::Request((content, path)) => {\n\t\t\t\t\tlet syntax = SyntaxText::new(\n\t\t\t\t\t\tcontent,\n\t\t\t\t\t\tPath::new(&path),\n\t\t\t\t\t\t&params,\n\t\t\t\t\t\t&self.syntax,\n\t\t\t\t\t)?;\n\t\t\t\t\tJobState::Response(syntax)\n\t\t\t\t}\n\t\t\t\tJobState::Response(res) => JobState::Response(res),\n\t\t\t});\n\t\t}\n\n\t\tOk(AsyncAppNotification::SyntaxHighlighting(\n\t\t\tSyntaxHighlightProgress::Done,\n\t\t))\n\t}\n}\n"
  },
  {
    "path": "src/watcher.rs",
    "content": "use anyhow::Result;\nuse crossbeam_channel::{unbounded, Sender};\nuse notify::{RecommendedWatcher, RecursiveMode, Watcher};\nuse notify_debouncer_mini::{new_debouncer, DebounceEventResult};\nuse scopetime::scope_time;\nuse std::{path::Path, thread, time::Duration};\n\npub struct RepoWatcher {\n\treceiver: crossbeam_channel::Receiver<()>,\n}\n\nimpl RepoWatcher {\n\tpub fn new(workdir: &str) -> Self {\n\t\tlog::trace!(\n\t\t\t\"recommended watcher: {:?}\",\n\t\t\tRecommendedWatcher::kind()\n\t\t);\n\n\t\tlet (tx, rx) = std::sync::mpsc::channel();\n\n\t\tlet workdir = workdir.to_string();\n\n\t\tthread::spawn(move || {\n\t\t\tlet timeout = Duration::from_secs(2);\n\t\t\tcreate_watcher(timeout, tx, &workdir);\n\t\t});\n\n\t\tlet (out_tx, out_rx) = unbounded();\n\n\t\tthread::spawn(move || {\n\t\t\tif let Err(e) = Self::forwarder(&rx, &out_tx) {\n\t\t\t\t//maybe we need to restart the forwarder now?\n\t\t\t\tlog::error!(\"notify receive error: {e}\");\n\t\t\t}\n\t\t});\n\n\t\tSelf { receiver: out_rx }\n\t}\n\n\t///\n\tpub fn receiver(&self) -> crossbeam_channel::Receiver<()> {\n\t\tself.receiver.clone()\n\t}\n\n\tfn forwarder(\n\t\treceiver: &std::sync::mpsc::Receiver<DebounceEventResult>,\n\t\tsender: &Sender<()>,\n\t) -> Result<()> {\n\t\tloop {\n\t\t\tlet ev = receiver.recv()?;\n\n\t\t\tif let Ok(ev) = ev {\n\t\t\t\tlog::debug!(\"notify events: {}\", ev.len());\n\n\t\t\t\tfor (idx, ev) in ev.iter().enumerate() {\n\t\t\t\t\tlog::debug!(\"notify [{idx}]: {ev:?}\");\n\t\t\t\t}\n\n\t\t\t\tif !ev.is_empty() {\n\t\t\t\t\tsender.send(())?;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfn create_watcher(\n\ttimeout: Duration,\n\ttx: std::sync::mpsc::Sender<DebounceEventResult>,\n\tworkdir: &str,\n) {\n\tscope_time!(\"create_watcher\");\n\n\tlet mut bouncer =\n\t\tnew_debouncer(timeout, tx).expect(\"Watch create error\");\n\tbouncer\n\t\t.watcher()\n\t\t.watch(Path::new(&workdir), RecursiveMode::Recursive)\n\t\t.expect(\"Watch error\");\n\n\tstd::mem::forget(bouncer);\n}\n"
  },
  {
    "path": "typos.toml",
    "content": "# configuration for https://github.com/crate-ci/typos\n\n[default.extend-words]\nratatui = \"ratatui\"\nsyntact = \"syntact\"\n\n[files]\nextend-exclude = [\"CHANGELOG.md\"]\n"
  },
  {
    "path": "vim_style_key_config.ron",
    "content": "// Note:\n// If the default key layout is lower case,\n// and you want to use `Shift + q` to trigger the exit event,\n// the setting should like this `exit: Some(( code: Char('Q'), modifiers: \"SHIFT\")),`\n// The Char should be upper case, and the modifier should be set to \"SHIFT\".\n//\n// Note:\n// find `KeysList` type in src/keys/key_list.rs for all possible keys.\n// every key not overwritten via the config file will use the default specified there\n(\n    open_help: Some(( code: F(1), modifiers: \"\")),\n\n    move_left: Some(( code: Char('h'), modifiers: \"\")),\n    move_right: Some(( code: Char('l'), modifiers: \"\")),\n    move_up: Some(( code: Char('k'), modifiers: \"\")),\n    move_down: Some(( code: Char('j'), modifiers: \"\")),\n    \n    popup_up: Some(( code: Char('p'), modifiers: \"CONTROL\")),\n    popup_down: Some(( code: Char('n'), modifiers: \"CONTROL\")),\n    page_up: Some(( code: Char('b'), modifiers: \"CONTROL\")),\n    page_down: Some(( code: Char('f'), modifiers: \"CONTROL\")),\n    home: Some(( code: Char('g'), modifiers: \"\")),\n    end: Some(( code: Char('G'), modifiers: \"SHIFT\")),\n    shift_up: Some(( code: Char('K'), modifiers: \"SHIFT\")),\n    shift_down: Some(( code: Char('J'), modifiers: \"SHIFT\")),\n\n    edit_file: Some(( code: Char('I'), modifiers: \"SHIFT\")),\n\n    status_reset_item: Some(( code: Char('U'), modifiers: \"SHIFT\")),\n\n    diff_reset_lines: Some(( code: Char('u'), modifiers: \"\")),\n    diff_stage_lines: Some(( code: Char('s'), modifiers: \"\")),\n\n    stashing_save: Some(( code: Char('w'), modifiers: \"\")),\n    stashing_toggle_index: Some(( code: Char('m'), modifiers: \"\")),\n\n    stash_open: Some(( code: Char('l'), modifiers: \"\")),\n\n    abort_merge: Some(( code: Char('M'), modifiers: \"SHIFT\")),\n)\n"
  },
  {
    "path": "wix/main.wxs",
    "content": "<?xml version='1.0' encoding='windows-1252'?>\n<!--\n  Copyright (C) 2017 Christopher R. Field.\n\n  Licensed under the Apache License, Version 2.0 (the \"License\");\n  you may not use this file except in compliance with the License.\n  You may obtain a copy of the License at\n\n  http://www.apache.org/licenses/LICENSE-2.0\n\n  Unless required by applicable law or agreed to in writing, software\n  distributed under the License is distributed on an \"AS IS\" BASIS,\n  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  See the License for the specific language governing permissions and\n  limitations under the License.\n-->\n\n<!--\n  Please do not remove these pre-processor If-Else blocks. These are used with\n  the `cargo wix` subcommand to automatically determine the installation\n  destination for 32-bit versus 64-bit installers. Removal of these lines will\n  cause installation errors.\n-->\n<?if $(var.Platform) = x64 ?>\n    <?define Win64 = \"yes\" ?>\n    <?define PlatformProgramFilesFolder = \"ProgramFiles64Folder\" ?>\n<?else ?>\n  <?define Win64 = \"no\" ?>\n  <?define PlatformProgramFilesFolder = \"ProgramFilesFolder\" ?>\n<?endif ?>\n\n<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>\n\n    <Product\n        Id='*'\n        Name='gitui'\n        UpgradeCode='C1CADE63-A601-4E02-96CC-FB921D5B174E'\n        Manufacturer='gitui-org'\n        Language='1033'\n        Codepage='1252'\n        Version='$(var.Version)'>\n\n        <Package Id='*'\n            Keywords='Installer'\n            Description='blazing fast terminal-ui for git'\n            Manufacturer='gitui-org'\n            InstallerVersion='450'\n            Languages='1033'\n            Compressed='yes'\n            InstallScope='perMachine'\n            SummaryCodepage='1252'\n            Platform='$(var.Platform)'/>\n\n        <MajorUpgrade\n            Schedule='afterInstallInitialize'\n            DowngradeErrorMessage='A newer version of [ProductName] is already installed. Setup will now exit.'/>\n\n        <Media Id='1' Cabinet='media1.cab' EmbedCab='yes' DiskPrompt='CD-ROM #1'/>\n        <Property Id='DiskPrompt' Value='gitui Installation'/>\n\n        <Directory Id='TARGETDIR' Name='SourceDir'>\n            <Directory Id='$(var.PlatformProgramFilesFolder)' Name='PFiles'>\n                <Directory Id='APPLICATIONFOLDER' Name='gitui'>\n                    <!--\n                      Disabling the license sidecar file in the installer is a two step process:\n\n                      1. Comment out or remove the `Component` tag along with its contents.\n                      2. Comment out or remove the `ComponentRef` tag with the \"License\" Id\n                         attribute value further down in this file.\n                    -->\n                    <Component Id='License' Guid='*' Win64='$(var.Win64)'>\n                        <File Id='LicenseFile'\n                            Name='License.rtf'\n                            DiskId='1'\n                            Source='wix\\License.rtf'\n                            KeyPath='yes'/>\n                    </Component>\n                    \n                    <Directory Id='Bin' Name='bin'>\n                        <Component Id='Path' Guid='6FDC3234-CA26-4127-8EB2-6B88F4C5507A' Win64='$(var.Win64)' KeyPath='yes'>\n                            <Environment\n                                Id='PATH'\n                                Name='PATH'\n                                Value='[Bin]'\n                                Permanent='no'\n                                Part='last'\n                                Action='set'\n                                System='yes'/>\n                        </Component>\n                        <Component Id='binary0' Guid='*' Win64='$(var.Win64)'>\n                            <File\n                                Id='exe0'\n                                Name='gitui.exe'\n                                DiskId='1'\n                                Source='target\\$(var.Profile)\\gitui.exe'\n                                KeyPath='yes'/>\n                        </Component>\n                    </Directory>\n                </Directory>\n            </Directory>\n            \n        </Directory>\n        <!--\n          added by hand to force the installation of VC runtime\n        -->\n        <DirectoryRef Id=\"TARGETDIR\"> \n            <Merge Id=\"VCRedist\" SourceFile=\"wix\\Microsoft_VC142_CRT_x64.msm\" DiskId=\"1\" Language=\"0\"/>\n        </DirectoryRef>\n        <Feature\n            Id='Binaries'\n            Title='Application'\n            Description='Installs all binaries and the license.'\n            Level='1'\n            ConfigurableDirectory='APPLICATIONFOLDER'\n            AllowAdvertise='no'\n            Display='expand'\n            Absent='disallow'>\n            <!--\n              Comment out or remove the following `ComponentRef` tag to remove\n              the license sidecar file from the installer.\n            -->\n            <ComponentRef Id='License'/>\n            \n            <ComponentRef Id='binary0'/>\n\n            <Feature\n                Id='Environment'\n                Title='PATH Environment Variable'\n                Description='Add the install location of the [ProductName] executable to the PATH system environment variable. This allows the [ProductName] executable to be called from any location.'\n                Level='1'\n                Absent='allow'>\n                <ComponentRef Id='Path'/>\n            </Feature>\n            <!--\n                added by hand to force the installation of VC runtime\n            -->\n           <Feature Id=\"VCRedist\" Title=\"Visual C++ Runtime\" AllowAdvertise=\"no\" Display=\"hidden\" Level=\"1\">\n            <MergeRef Id=\"VCRedist\"/>\n          </Feature>\n        </Feature>\n\n        <SetProperty Id='ARPINSTALLLOCATION' Value='[APPLICATIONFOLDER]' After='CostFinalize'/>\n\n        \n        <!--\n          Uncomment the following `Icon` and `Property` tags to change the product icon.\n\n          The product icon is the graphic that appears in the Add/Remove\n          Programs control panel for the application.\n        -->\n        <!--<Icon Id='ProductICO' SourceFile='wix\\Product.ico'/>-->\n        <!--<Property Id='ARPPRODUCTICON' Value='ProductICO' />-->\n\n        <Property Id='ARPHELPLINK' Value='https://github.com/gitui-org/gitui'/>\n        \n        <UI>\n            <UIRef Id='WixUI_FeatureTree'/>\n            <!--\n              Disabling the EULA dialog in the installer is a two step process:\n\n                 1. Uncomment the following two `Publish` tags\n                 2. Comment out or remove the `<WiXVariable Id='WixUILicenseRtf'...` tag further down\n\n            -->\n            <!--<Publish Dialog='WelcomeDlg' Control='Next' Event='NewDialog' Value='CustomizeDlg' Order='99'>1</Publish>-->\n            <!--<Publish Dialog='CustomizeDlg' Control='Back' Event='NewDialog' Value='WelcomeDlg' Order='99'>1</Publish>-->\n            \n        </UI>\n\n        <!--\n          Disabling the EULA dialog in the installer requires commenting out\n          or removing the following `WixVariable` tag\n        -->\n        <WixVariable Id='WixUILicenseRtf' Value='wix\\License.rtf'/>\n        \n        \n        <!--\n          Uncomment the next `WixVariable` tag to customize the installer's\n          Graphical User Interface (GUI) and add a custom banner image across\n          the top of each screen. See the WiX Toolset documentation for details\n          about customization.\n\n          The banner BMP dimensions are 493 x 58 pixels.\n        -->\n        <!--<WixVariable Id='WixUIBannerBmp' Value='wix\\Banner.bmp'/>-->\n\n        \n        <!--\n          Uncomment the next `WixVariable` tag to customize the installer's\n          Graphical User Interface (GUI) and add a custom image to the first\n          dialog, or screen. See the WiX Toolset documentation for details about\n          customization.\n\n          The dialog BMP dimensions are 493 x 312 pixels.\n        -->\n        <!--<WixVariable Id='WixUIDialogBmp' Value='wix\\Dialog.bmp'/>-->\n\n    </Product>\n\n</Wix>\n"
  }
]