diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..44dd1d1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,52 @@ +# EditorConfig - https://editorconfig.org +# Helps maintain consistent coding styles across different editors + +root = true + +# Default settings for all files +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +# Rust files +[*.rs] +indent_size = 4 +max_line_length = 100 + +# TOML files (Cargo.toml, etc.) +[*.toml] +indent_size = 4 + +# YAML files +[*.{yml,yaml}] +indent_size = 2 + +# JSON files +[*.json] +indent_size = 2 + +# Markdown files +[*.md] +trim_trailing_whitespace = false +max_line_length = off + +# Python files +[*.py] +indent_size = 4 +max_line_length = 100 + +# Shell scripts +[*.sh] +indent_size = 2 + +# Makefiles require tabs +[Makefile] +indent_style = tab + +# Git config files +[.git*] +indent_size = 4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4cb4704 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,247 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + # Minimum supported Rust version + MSRV: "1.75" + +jobs: + # ========================================================================== + # Format check + # ========================================================================== + fmt: + name: Format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + + - name: Check formatting + run: cargo fmt --all -- --check + + # ========================================================================== + # Clippy lints + # ========================================================================== + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: clippy + + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + + - name: Run Clippy + run: cargo clippy --all-targets --all-features -- -D warnings + + # ========================================================================== + # Tests + # ========================================================================== + test: + name: Test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + + - name: Run tests + run: cargo test --all-features + + - name: Run doc tests + run: cargo test --doc + + # ========================================================================== + # MSRV check + # ========================================================================== + msrv: + name: MSRV (${{ env.MSRV }}) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust ${{ env.MSRV }} + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.MSRV }} + + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + + - name: Check MSRV + run: cargo check --all-features + + # ========================================================================== + # Documentation + # ========================================================================== + docs: + name: Documentation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + + - name: Build docs + run: cargo doc --no-deps --all-features + env: + RUSTDOCFLAGS: -D warnings + + # ========================================================================== + # Build + # ========================================================================== + build: + name: Build (${{ matrix.target }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-unknown-linux-gnu + os: ubuntu-latest + - target: x86_64-unknown-linux-musl + os: ubuntu-latest + - target: aarch64-unknown-linux-gnu + os: ubuntu-latest + - target: x86_64-apple-darwin + os: macos-latest + - target: aarch64-apple-darwin + os: macos-latest + - target: x86_64-pc-windows-msvc + os: windows-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Install cross-compilation tools + if: matrix.target == 'aarch64-unknown-linux-gnu' + run: | + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu + + - name: Install musl tools + if: matrix.target == 'x86_64-unknown-linux-musl' + run: | + sudo apt-get update + sudo apt-get install -y musl-tools + + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + with: + key: ${{ matrix.target }} + + - name: Build + run: cargo build --release --target ${{ matrix.target }} + env: + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: apc-${{ matrix.target }} + path: | + target/${{ matrix.target }}/release/apc + target/${{ matrix.target }}/release/apc.exe + if-no-files-found: ignore + + # ========================================================================== + # Security audit + # ========================================================================== + security: + name: Security Audit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install cargo-audit + run: cargo install cargo-audit + + - name: Run audit + run: cargo audit + + # ========================================================================== + # Coverage + # ========================================================================== + coverage: + name: Coverage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: llvm-tools-preview + + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov + + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + + - name: Generate coverage + run: cargo llvm-cov --all-features --lcov --output-path lcov.info + + - name: Upload to Codecov + uses: codecov/codecov-action@v4 + with: + files: lcov.info + fail_ci_if_error: false + + # ========================================================================== + # All checks passed + # ========================================================================== + ci-success: + name: CI Success + needs: [fmt, clippy, test, msrv, docs, build, security] + runs-on: ubuntu-latest + if: always() + steps: + - name: Check all jobs passed + run: | + if [[ "${{ needs.fmt.result }}" != "success" ]] || \ + [[ "${{ needs.clippy.result }}" != "success" ]] || \ + [[ "${{ needs.test.result }}" != "success" ]] || \ + [[ "${{ needs.msrv.result }}" != "success" ]] || \ + [[ "${{ needs.docs.result }}" != "success" ]] || \ + [[ "${{ needs.build.result }}" != "success" ]] || \ + [[ "${{ needs.security.result }}" != "success" ]]; then + echo "One or more jobs failed" + exit 1 + fi + echo "All CI checks passed!" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8851d48 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,368 @@ +name: Release + +on: + push: + tags: + - "v*" + workflow_dispatch: + inputs: + tag: + description: "Release tag (e.g., v0.1.0)" + required: true + +env: + CARGO_TERM_COLOR: always + +jobs: + # ========================================================================== + # Create release + # ========================================================================== + create-release: + name: Create Release + runs-on: ubuntu-latest + outputs: + upload_url: ${{ steps.create_release.outputs.upload_url }} + version: ${{ steps.get_version.outputs.version }} + steps: + - uses: actions/checkout@v4 + + - name: Get version + id: get_version + run: | + if [ -n "${{ github.event.inputs.tag }}" ]; then + VERSION="${{ github.event.inputs.tag }}" + else + VERSION="${GITHUB_REF#refs/tags/}" + fi + echo "version=${VERSION}" >> $GITHUB_OUTPUT + echo "Version: ${VERSION}" + + - name: Create Release + id: create_release + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ steps.get_version.outputs.version }} + name: Release ${{ steps.get_version.outputs.version }} + draft: true + prerelease: ${{ contains(steps.get_version.outputs.version, '-') }} + generate_release_notes: true + + # ========================================================================== + # Build binaries + # ========================================================================== + build-binaries: + name: Build (${{ matrix.target }}) + needs: create-release + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + # Linux x86_64 (glibc) + - target: x86_64-unknown-linux-gnu + os: ubuntu-latest + archive: tar.gz + # Linux x86_64 (musl - static) + - target: x86_64-unknown-linux-musl + os: ubuntu-latest + archive: tar.gz + # Linux ARM64 + - target: aarch64-unknown-linux-gnu + os: ubuntu-latest + archive: tar.gz + # macOS x86_64 + - target: x86_64-apple-darwin + os: macos-latest + archive: tar.gz + # macOS ARM64 (Apple Silicon) + - target: aarch64-apple-darwin + os: macos-latest + archive: tar.gz + # Windows x86_64 + - target: x86_64-pc-windows-msvc + os: windows-latest + archive: zip + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Install cross-compilation tools (Linux ARM64) + if: matrix.target == 'aarch64-unknown-linux-gnu' + run: | + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu + + - name: Install musl tools + if: matrix.target == 'x86_64-unknown-linux-musl' + run: | + sudo apt-get update + sudo apt-get install -y musl-tools + + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + with: + key: release-${{ matrix.target }} + + - name: Build release binary + run: cargo build --release --target ${{ matrix.target }} + env: + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc + + - name: Prepare archive (Unix) + if: matrix.os != 'windows-latest' + run: | + ARCHIVE_NAME="apc-${{ needs.create-release.outputs.version }}-${{ matrix.target }}" + mkdir -p "${ARCHIVE_NAME}" + cp "target/${{ matrix.target }}/release/apc" "${ARCHIVE_NAME}/" + cp Readme.md LICENSE* "${ARCHIVE_NAME}/" 2>/dev/null || true + tar -czvf "${ARCHIVE_NAME}.tar.gz" "${ARCHIVE_NAME}" + echo "ARCHIVE_NAME=${ARCHIVE_NAME}.tar.gz" >> $GITHUB_ENV + + - name: Prepare archive (Windows) + if: matrix.os == 'windows-latest' + shell: pwsh + run: | + $ARCHIVE_NAME = "apc-${{ needs.create-release.outputs.version }}-${{ matrix.target }}" + New-Item -ItemType Directory -Force -Path $ARCHIVE_NAME + Copy-Item "target/${{ matrix.target }}/release/apc.exe" -Destination "$ARCHIVE_NAME/" + Copy-Item "Readme.md" -Destination "$ARCHIVE_NAME/" -ErrorAction SilentlyContinue + Compress-Archive -Path $ARCHIVE_NAME -DestinationPath "$ARCHIVE_NAME.zip" + echo "ARCHIVE_NAME=$ARCHIVE_NAME.zip" >> $env:GITHUB_ENV + + - name: Upload release asset + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ needs.create-release.outputs.version }} + files: ${{ env.ARCHIVE_NAME }} + + # ========================================================================== + # Build Python wheels + # ========================================================================== + build-wheels: + name: Build Wheels (${{ matrix.os }}) + needs: create-release + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Install maturin + run: pip install maturin + + - name: Build wheels + run: | + maturin build --release --strip + + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-${{ matrix.os }} + path: target/wheels/*.whl + + # ========================================================================== + # Publish to PyPI + # ========================================================================== + publish-pypi: + name: Publish to PyPI + needs: [create-release, build-wheels] + runs-on: ubuntu-latest + environment: pypi + steps: + - name: Download wheels + uses: actions/download-artifact@v4 + with: + pattern: wheels-* + merge-multiple: true + path: dist + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} + skip-existing: true + + # ========================================================================== + # Publish to crates.io + # ========================================================================== + publish-crates: + name: Publish to crates.io + needs: create-release + runs-on: ubuntu-latest + environment: crates-io + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Publish to crates.io + run: cargo publish + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + + # ========================================================================== + # Update Homebrew + # ========================================================================== + update-homebrew: + name: Update Homebrew Formula + needs: [create-release, build-binaries] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Calculate checksums + run: | + VERSION="${{ needs.create-release.outputs.version }}" + BASE_URL="https://github.com/${{ github.repository }}/releases/download/${VERSION}" + + # Download release assets and calculate checksums + for TARGET in x86_64-apple-darwin aarch64-apple-darwin x86_64-unknown-linux-gnu; do + ARCHIVE="apc-${VERSION}-${TARGET}.tar.gz" + curl -sL "${BASE_URL}/${ARCHIVE}" -o "${ARCHIVE}" + SHA=$(shasum -a 256 "${ARCHIVE}" | cut -d' ' -f1) + echo "${TARGET}_SHA256=${SHA}" >> $GITHUB_ENV + done + + - name: Generate Homebrew formula + run: | + VERSION="${{ needs.create-release.outputs.version }}" + VERSION_NUM="${VERSION#v}" + + cat > agent-precommit.rb << EOF + class AgentPrecommit < Formula + desc "Smart pre-commit hooks for humans and AI coding agents" + homepage "https://github.com/${{ github.repository }}" + version "${VERSION_NUM}" + license "MIT" + + on_macos do + if Hardware::CPU.arm? + url "https://github.com/${{ github.repository }}/releases/download/${VERSION}/apc-${VERSION}-aarch64-apple-darwin.tar.gz" + sha256 "${{ env.aarch64-apple-darwin_SHA256 }}" + else + url "https://github.com/${{ github.repository }}/releases/download/${VERSION}/apc-${VERSION}-x86_64-apple-darwin.tar.gz" + sha256 "${{ env.x86_64-apple-darwin_SHA256 }}" + end + end + + on_linux do + url "https://github.com/${{ github.repository }}/releases/download/${VERSION}/apc-${VERSION}-x86_64-unknown-linux-gnu.tar.gz" + sha256 "${{ env.x86_64-unknown-linux-gnu_SHA256 }}" + end + + def install + bin.install "apc" + end + + test do + system "#{bin}/apc", "--version" + end + end + EOF + + cat agent-precommit.rb + + - name: Upload formula + uses: actions/upload-artifact@v4 + with: + name: homebrew-formula + path: agent-precommit.rb + + # ========================================================================== + # Generate install script + # ========================================================================== + generate-install-script: + name: Generate Install Script + needs: create-release + runs-on: ubuntu-latest + steps: + - name: Generate install.sh + run: | + VERSION="${{ needs.create-release.outputs.version }}" + cat > install.sh << 'SCRIPT' + #!/bin/sh + set -e + + # agent-precommit installer + # Usage: curl -fsSL https://agent-precommit.dev/install.sh | sh + + VERSION="${1:-latest}" + REPO="agent-precommit/agent-precommit" + INSTALL_DIR="${HOME}/.local/bin" + + # Detect OS and architecture + OS=$(uname -s | tr '[:upper:]' '[:lower:]') + ARCH=$(uname -m) + + case "${OS}" in + linux) + case "${ARCH}" in + x86_64) TARGET="x86_64-unknown-linux-gnu" ;; + aarch64) TARGET="aarch64-unknown-linux-gnu" ;; + *) echo "Unsupported architecture: ${ARCH}"; exit 1 ;; + esac + ;; + darwin) + case "${ARCH}" in + x86_64) TARGET="x86_64-apple-darwin" ;; + arm64) TARGET="aarch64-apple-darwin" ;; + *) echo "Unsupported architecture: ${ARCH}"; exit 1 ;; + esac + ;; + *) + echo "Unsupported OS: ${OS}" + exit 1 + ;; + esac + + # Get latest version if not specified + if [ "${VERSION}" = "latest" ]; then + VERSION=$(curl -sL "https://api.github.com/repos/${REPO}/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') + fi + + echo "Installing agent-precommit ${VERSION} for ${TARGET}..." + + # Download and extract + ARCHIVE="apc-${VERSION}-${TARGET}.tar.gz" + URL="https://github.com/${REPO}/releases/download/${VERSION}/${ARCHIVE}" + + mkdir -p "${INSTALL_DIR}" + curl -fsSL "${URL}" | tar -xz -C "${INSTALL_DIR}" --strip-components=1 apc-${VERSION}-${TARGET}/apc + + chmod +x "${INSTALL_DIR}/apc" + + echo "" + echo "✓ Installed apc to ${INSTALL_DIR}/apc" + echo "" + echo "Make sure ${INSTALL_DIR} is in your PATH:" + echo ' export PATH="${HOME}/.local/bin:${PATH}"' + echo "" + echo "Get started:" + echo " apc init" + echo " apc install" + SCRIPT + + chmod +x install.sh + + - name: Upload install script + uses: actions/upload-artifact@v4 + with: + name: install-script + path: install.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ae9a5ef --- /dev/null +++ b/.gitignore @@ -0,0 +1,187 @@ +# ============================================================================= +# Rust +# ============================================================================= + +# Compiled files +/target/ +**/*.rs.bk +*.pdb + +# Cargo.lock for binaries (we track it for reproducible builds) +# Uncomment if you want to ignore it: +# Cargo.lock + +# ============================================================================= +# Python +# ============================================================================= + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Virtual environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Maturin +target/wheels/ + +# ============================================================================= +# IDE / Editors +# ============================================================================= + +# VS Code +.vscode/ +*.code-workspace + +# IntelliJ IDEA +.idea/ +*.iml +*.ipr +*.iws + +# Vim +*.swp +*.swo +*~ + +# Emacs +*~ +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +*.elc +auto-save-list +tramp +.\#* + +# Sublime Text +*.sublime-workspace +*.sublime-project + +# ============================================================================= +# OS-specific +# ============================================================================= + +# macOS +.DS_Store +.AppleDouble +.LSOverride +._* +.Spotlight-V100 +.Trashes + +# Windows +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db +*.stackdump +[Dd]esktop.ini +$RECYCLE.BIN/ + +# Linux +*~ +.fuse_hidden* +.directory +.Trash-* +.nfs* + +# ============================================================================= +# Testing and benchmarking +# ============================================================================= + +# Test artifacts +/tests/output/ +*.profraw +*.profdata + +# Benchmarking +/criterion/ +/flamegraph.svg +/perf.data* + +# ============================================================================= +# Documentation +# ============================================================================= + +# Generated documentation +/docs/_build/ +/site/ + +# ============================================================================= +# Misc +# ============================================================================= + +# Secrets and credentials (NEVER commit these) +.env.local +.env.*.local +*.pem +*.key +secrets.json +credentials.json + +# Logs +*.log +logs/ + +# Temporary files +*.tmp +*.temp +.tmp/ +.temp/ + +# Local configuration +.local/ +*.local.toml +*.local.yaml +*.local.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..d05cf02 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,115 @@ +# Pre-commit configuration for agent-precommit +# https://pre-commit.com + +repos: + # ========================================================================== + # General hooks + # ========================================================================== + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-toml + - id: check-json + - id: check-added-large-files + args: ["--maxkb=1000"] + - id: check-merge-conflict + - id: check-case-conflict + - id: detect-private-key + - id: mixed-line-ending + args: ["--fix=lf"] + + # ========================================================================== + # Rust hooks + # ========================================================================== + - repo: local + hooks: + # Format check + - id: cargo-fmt + name: cargo fmt + entry: cargo fmt --all -- --check + language: system + types: [rust] + pass_filenames: false + + # Clippy lints + - id: cargo-clippy + name: cargo clippy + entry: cargo clippy --all-targets --all-features -- -D warnings + language: system + types: [rust] + pass_filenames: false + + # Run tests + - id: cargo-test + name: cargo test + entry: cargo test --all-features + language: system + types: [rust] + pass_filenames: false + stages: [pre-push] + + # Check for build errors + - id: cargo-check + name: cargo check + entry: cargo check --all-features + language: system + types: [rust] + pass_filenames: false + + # ========================================================================== + # Python hooks (for Python wrapper) + # ========================================================================== + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.9 + hooks: + - id: ruff + args: ["--fix"] + - id: ruff-format + + # ========================================================================== + # Markdown and documentation + # ========================================================================== + - repo: https://github.com/igorshubovych/markdownlint-cli + rev: v0.38.0 + hooks: + - id: markdownlint + args: ["--fix", "--disable", "MD013", "MD033", "MD041", "--"] + + # ========================================================================== + # YAML validation + # ========================================================================== + - repo: https://github.com/adrienverge/yamllint + rev: v1.33.0 + hooks: + - id: yamllint + args: ["-d", "{extends: relaxed, rules: {line-length: disable}}"] + + # ========================================================================== + # Security scanning + # ========================================================================== + - repo: https://github.com/gitleaks/gitleaks + rev: v8.18.1 + hooks: + - id: gitleaks + + # ========================================================================== + # Commit message format + # ========================================================================== + - repo: https://github.com/commitizen-tools/commitizen + rev: v3.13.0 + hooks: + - id: commitizen + stages: [commit-msg] + +# Default stages +default_stages: [pre-commit] + +# CI configuration +ci: + autofix_commit_msg: "style: auto-fix pre-commit hooks" + autoupdate_commit_msg: "chore: update pre-commit hooks" + autoupdate_schedule: monthly + skip: [cargo-test] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d51b9be --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,177 @@ +# Contributing to agent-precommit + +Thank you for your interest in contributing to agent-precommit! This document provides guidelines for contributing. + +## Development Setup + +### Prerequisites + +- Rust 1.75 or later +- Python 3.8+ (for Python wrapper development) +- Git + +### Getting Started + +1. Clone the repository: + + ```bash + git clone https://github.com/agent-precommit/agent-precommit.git + cd agent-precommit + ``` + +2. Install pre-commit hooks: + + ```bash + pip install pre-commit + pre-commit install + ``` + +3. Build the project: + + ```bash + cargo build + ``` + +4. Run tests: + + ```bash + cargo test + ``` + +## Code Quality Standards + +This project maintains extremely high code quality standards: + +### Rust + +- **Formatting**: All code must pass `cargo fmt --check` +- **Linting**: All code must pass `cargo clippy` with no warnings +- **Tests**: All tests must pass +- **Documentation**: Public APIs must be documented + +### Linting Configuration + +The project uses strict Clippy linting. See `Cargo.toml` for the full configuration: + +- `unsafe_code = "deny"` - No unsafe code allowed +- `unwrap_used = "deny"` - No `.unwrap()` calls +- `expect_used = "deny"` - No `.expect()` calls +- `panic = "deny"` - No explicit panics +- `todo = "deny"` - No TODO macros in production code + +### Pre-commit Hooks + +The following checks run automatically on each commit: + +- `cargo fmt` - Code formatting +- `cargo clippy` - Lints +- `cargo check` - Compilation check +- Various file quality checks + +## Making Changes + +### Branching + +- Create feature branches from `main` +- Use descriptive branch names: `feature/add-go-preset`, `fix/timeout-handling` + +### Commits + +- Write clear, concise commit messages +- Use conventional commit format: + - `feat:` for new features + - `fix:` for bug fixes + - `docs:` for documentation + - `refactor:` for code refactoring + - `test:` for tests + - `chore:` for maintenance + +### Pull Requests + +1. Ensure all CI checks pass +2. Update documentation if needed +3. Add tests for new functionality +4. Request review from maintainers + +## Testing + +### Running Tests + +```bash +# Run all tests +cargo test + +# Run tests with output +cargo test -- --nocapture + +# Run specific test +cargo test test_name + +# Run integration tests +cargo test --test '*' +``` + +### Writing Tests + +- Place unit tests in the same file as the code +- Use the `#[cfg(test)]` attribute +- Place integration tests in `tests/` +- Aim for high coverage on critical paths + +## Documentation + +### Code Documentation + +- Document all public functions, structs, and modules +- Use `///` for doc comments +- Include examples where helpful + +### Building Docs + +```bash +cargo doc --open +``` + +## Architecture Overview + +```text +src/ +├── lib.rs # Library root +├── main.rs # CLI entry point +├── cli/ # CLI commands +│ ├── mod.rs # CLI structure +│ └── commands.rs # Command implementations +├── config/ # Configuration handling +│ └── mod.rs # Config structs and loading +├── core/ # Core functionality +│ ├── mod.rs # Module root +│ ├── detector.rs # Mode detection +│ ├── error.rs # Error types +│ ├── executor.rs # Command execution +│ ├── git.rs # Git operations +│ └── runner.rs # Check orchestration +├── checks/ # Check implementations +│ ├── mod.rs # Module root +│ ├── builtin.rs # Built-in checks +│ └── precommit.rs # Pre-commit integration +└── presets/ # Configuration presets + └── mod.rs # Preset definitions +``` + +## Release Process + +1. Update version in `Cargo.toml` and `pyproject.toml` +2. Update CHANGELOG.md +3. Create a git tag: `git tag v0.x.0` +4. Push the tag: `git push origin v0.x.0` +5. GitHub Actions handles the rest + +## Getting Help + +- Open an issue for bugs or feature requests +- Check existing issues before creating new ones +- Join discussions in pull requests + +## Code of Conduct + +Be respectful and inclusive. We welcome contributions from everyone. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..4025613 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3989 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "agent-precommit" +version = "0.1.0" +dependencies = [ + "anyhow", + "arbitrary", + "assert_cmd", + "chrono", + "clap", + "clap_complete", + "console", + "criterion", + "dialoguer", + "dirs", + "gix", + "glob", + "humantime", + "indicatif", + "insta", + "mockall", + "predicates", + "pretty_assertions", + "regex", + "rstest", + "serde", + "serde_json", + "tempfile", + "thiserror 2.0.17", + "tokio", + "toml", + "tracing", + "tracing-subscriber", + "walkdir", + "which", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "arc-swap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e" +dependencies = [ + "rustversion", +] + +[[package]] +name = "assert_cmd" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcbb6924530aa9e0432442af08bbcafdad182db80d2e560da42a6d442535bf85" +dependencies = [ + "anstyle", + "bstr", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", + "terminal_size", +] + +[[package]] +name = "clap_complete" +version = "4.5.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c0da80818b2d95eca9aa614a30783e42f62bf5fdfee24e68cfb960b071ba8d1" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "clru" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd0f76e066e64fdc5631e3bb46381254deab9ef1158292f27c8c57e3bf3fe59" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.59.0", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "futures", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "tokio", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dialoguer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +dependencies = [ + "console", + "shell-words", + "tempfile", + "thiserror 1.0.69", + "zeroize", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_home" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "faster-hex" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183" +dependencies = [ + "serde", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fragile" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "gix" +version = "0.68.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b04c66359b5e17f92395abc433861df0edf48f39f3f590818d1d7217327dd6a1" +dependencies = [ + "gix-actor", + "gix-attributes", + "gix-command", + "gix-commitgraph", + "gix-config", + "gix-credentials", + "gix-date", + "gix-diff", + "gix-discover", + "gix-features", + "gix-filter", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-hashtable", + "gix-ignore", + "gix-index", + "gix-lock", + "gix-negotiate", + "gix-object", + "gix-odb", + "gix-pack", + "gix-path", + "gix-pathspec", + "gix-prompt", + "gix-protocol", + "gix-ref", + "gix-refspec", + "gix-revision", + "gix-revwalk", + "gix-sec", + "gix-submodule", + "gix-tempfile", + "gix-trace", + "gix-transport", + "gix-traverse", + "gix-url", + "gix-utils", + "gix-validate 0.9.4", + "gix-worktree", + "once_cell", + "smallvec", + "thiserror 2.0.17", +] + +[[package]] +name = "gix-actor" +version = "0.33.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20018a1a6332e065f1fcc8305c1c932c6b8c9985edea2284b3c79dc6fa3ee4b2" +dependencies = [ + "bstr", + "gix-date", + "gix-utils", + "itoa", + "thiserror 2.0.17", + "winnow 0.6.26", +] + +[[package]] +name = "gix-attributes" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf9bf852194c0edfe699a2d36422d2c1f28f73b7c6d446c3f0ccd3ba232cadc" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-quote", + "gix-trace", + "kstring", + "smallvec", + "thiserror 2.0.17", + "unicode-bom", +] + +[[package]] +name = "gix-bitmap" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e150161b8a75b5860521cb876b506879a3376d3adc857ec7a9d35e7c6a5e531" +dependencies = [ + "thiserror 2.0.17", +] + +[[package]] +name = "gix-chunk" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c356b3825677cb6ff579551bb8311a81821e184453cbd105e2fc5311b288eeb" +dependencies = [ + "thiserror 2.0.17", +] + +[[package]] +name = "gix-command" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7d6b8f3a64453fd7e8191eb80b351eb7ac0839b40a1237cd2c137d5079fe53" +dependencies = [ + "bstr", + "gix-path", + "gix-trace", + "shell-words", +] + +[[package]] +name = "gix-commitgraph" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8da6591a7868fb2b6dabddea6b09988b0b05e0213f938dbaa11a03dd7a48d85" +dependencies = [ + "bstr", + "gix-chunk", + "gix-features", + "gix-hash", + "memmap2", + "thiserror 2.0.17", +] + +[[package]] +name = "gix-config" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6649b406ca1f99cb148959cf00468b231f07950f8ec438cc0903cda563606f19" +dependencies = [ + "bstr", + "gix-config-value", + "gix-features", + "gix-glob", + "gix-path", + "gix-ref", + "gix-sec", + "memchr", + "once_cell", + "smallvec", + "thiserror 2.0.17", + "unicode-bom", + "winnow 0.6.26", +] + +[[package]] +name = "gix-config-value" +version = "0.14.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc2c844c4cf141884678cabef736fd91dd73068b9146e6f004ba1a0457944b6" +dependencies = [ + "bitflags", + "bstr", + "gix-path", + "libc", + "thiserror 2.0.17", +] + +[[package]] +name = "gix-credentials" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2be87bb8685fc7e6e7032ef71c45068ffff609724a0c897b8047fde10db6ae71" +dependencies = [ + "bstr", + "gix-command", + "gix-config-value", + "gix-path", + "gix-prompt", + "gix-sec", + "gix-trace", + "gix-url", + "thiserror 2.0.17", +] + +[[package]] +name = "gix-date" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daa30058ec7d3511fbc229e4f9e696a35abd07ec5b82e635eff864a2726217e4" +dependencies = [ + "bstr", + "itoa", + "jiff", + "thiserror 2.0.17", +] + +[[package]] +name = "gix-diff" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a327be31a392144b60ab0b1c863362c32a1c8f7effdfa2141d5d5b6b916ef3bf" +dependencies = [ + "bstr", + "gix-hash", + "gix-object", + "thiserror 2.0.17", +] + +[[package]] +name = "gix-discover" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83bf6dfa4e266a4a9becb4d18fc801f92c3f7cc6c433dd86fdadbcf315ffb6ef" +dependencies = [ + "bstr", + "dunce", + "gix-fs", + "gix-hash", + "gix-path", + "gix-ref", + "gix-sec", + "thiserror 2.0.17", +] + +[[package]] +name = "gix-features" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d85d673f2e022a340dba4713bed77ef2cf4cd737d2f3e0f159d45e0935fd81f" +dependencies = [ + "bytes", + "crc32fast", + "flate2", + "gix-hash", + "gix-trace", + "gix-utils", + "libc", + "once_cell", + "prodash", + "sha1_smol", + "thiserror 2.0.17", + "walkdir", +] + +[[package]] +name = "gix-filter" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5108cc58d58b27df10ac4de7f31b2eb96d588a33e5eba23739b865f5d8db7995" +dependencies = [ + "bstr", + "encoding_rs", + "gix-attributes", + "gix-command", + "gix-hash", + "gix-object", + "gix-packetline-blocking", + "gix-path", + "gix-quote", + "gix-trace", + "gix-utils", + "smallvec", + "thiserror 2.0.17", +] + +[[package]] +name = "gix-fs" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3d4fac505a621f97e5ce2c69fdc425742af00c0920363ca4074f0eb48b1db9" +dependencies = [ + "fastrand", + "gix-features", + "gix-utils", +] + +[[package]] +name = "gix-glob" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf69a6bec0a3581567484bf99a4003afcaf6c469fd4214352517ea355cf3435" +dependencies = [ + "bitflags", + "bstr", + "gix-features", + "gix-path", +] + +[[package]] +name = "gix-hash" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5eccc17194ed0e67d49285e4853307e4147e95407f91c1c3e4a13ba9f4e4ce" +dependencies = [ + "faster-hex", + "thiserror 2.0.17", +] + +[[package]] +name = "gix-hashtable" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef65b256631078ef733bc5530c4e6b1c2e7d5c2830b75d4e9034ab3997d18fe" +dependencies = [ + "gix-hash", + "hashbrown 0.14.5", + "parking_lot", +] + +[[package]] +name = "gix-ignore" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b1fb24d2a4af0aa7438e2771d60c14a80cf2c9bd55c29cf1712b841f05bb8a" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-trace", + "unicode-bom", +] + +[[package]] +name = "gix-index" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "270645fd20556b64c8ffa1540d921b281e6994413a0ca068596f97e9367a257a" +dependencies = [ + "bitflags", + "bstr", + "filetime", + "fnv", + "gix-bitmap", + "gix-features", + "gix-fs", + "gix-hash", + "gix-lock", + "gix-object", + "gix-traverse", + "gix-utils", + "gix-validate 0.9.4", + "hashbrown 0.14.5", + "itoa", + "libc", + "memmap2", + "rustix 0.38.44", + "smallvec", + "thiserror 2.0.17", +] + +[[package]] +name = "gix-lock" +version = "15.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd3ab68a452db63d9f3ebdacb10f30dba1fa0d31ac64f4203d395ed1102d940" +dependencies = [ + "gix-tempfile", + "gix-utils", + "thiserror 2.0.17", +] + +[[package]] +name = "gix-negotiate" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27f830a16405386e9c83b9d5be8261fe32bbd6b3caf15bd1b284c6b2b7ef1a8" +dependencies = [ + "bitflags", + "gix-commitgraph", + "gix-date", + "gix-hash", + "gix-object", + "gix-revwalk", + "smallvec", + "thiserror 2.0.17", +] + +[[package]] +name = "gix-object" +version = "0.46.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42d58010183ef033f31088479b4eb92b44fe341b35b62d39eb8b185573d77ea" +dependencies = [ + "bstr", + "gix-actor", + "gix-date", + "gix-features", + "gix-hash", + "gix-hashtable", + "gix-path", + "gix-utils", + "gix-validate 0.9.4", + "itoa", + "smallvec", + "thiserror 2.0.17", + "winnow 0.6.26", +] + +[[package]] +name = "gix-odb" +version = "0.65.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93bed6e1b577c25a6bb8e6ecbf4df525f29a671ddf5f2221821a56a8dbeec4e3" +dependencies = [ + "arc-swap", + "gix-date", + "gix-features", + "gix-fs", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-pack", + "gix-path", + "gix-quote", + "parking_lot", + "tempfile", + "thiserror 2.0.17", +] + +[[package]] +name = "gix-pack" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b91fec04d359544fecbb8e85117ec746fbaa9046ebafcefb58cb74f20dc76d4" +dependencies = [ + "clru", + "gix-chunk", + "gix-features", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-path", + "gix-tempfile", + "memmap2", + "parking_lot", + "smallvec", + "thiserror 2.0.17", +] + +[[package]] +name = "gix-packetline" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "123844a70cf4d5352441dc06bab0da8aef61be94ec239cb631e0ba01dc6d3a04" +dependencies = [ + "bstr", + "faster-hex", + "gix-trace", + "thiserror 2.0.17", +] + +[[package]] +name = "gix-packetline-blocking" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecf3ea2e105c7e45587bac04099824301262a6c43357fad5205da36dbb233b3" +dependencies = [ + "bstr", + "faster-hex", + "gix-trace", + "thiserror 2.0.17", +] + +[[package]] +name = "gix-path" +version = "0.10.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cb06c3e4f8eed6e24fd915fa93145e28a511f4ea0e768bae16673e05ed3f366" +dependencies = [ + "bstr", + "gix-trace", + "gix-validate 0.10.1", + "thiserror 2.0.17", +] + +[[package]] +name = "gix-pathspec" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c472dfbe4a4e96fcf7efddcd4771c9037bb4fdea2faaabf2f4888210c75b81e" +dependencies = [ + "bitflags", + "bstr", + "gix-attributes", + "gix-config-value", + "gix-glob", + "gix-path", + "thiserror 2.0.17", +] + +[[package]] +name = "gix-prompt" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a7822afc4bc9c5fbbc6ce80b00f41c129306b7685cac3248dbfa14784960594" +dependencies = [ + "gix-command", + "gix-config-value", + "parking_lot", + "rustix 0.38.44", + "thiserror 2.0.17", +] + +[[package]] +name = "gix-protocol" +version = "0.46.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a7e7e51a0dea531d3448c297e2fa919b2de187111a210c324b7e9f81508b8ca" +dependencies = [ + "bstr", + "gix-credentials", + "gix-date", + "gix-features", + "gix-hash", + "gix-transport", + "gix-utils", + "maybe-async", + "thiserror 2.0.17", + "winnow 0.6.26", +] + +[[package]] +name = "gix-quote" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e49357fccdb0c85c0d3a3292a9f6db32d9b3535959b5471bb9624908f4a066c6" +dependencies = [ + "bstr", + "gix-utils", + "thiserror 2.0.17", +] + +[[package]] +name = "gix-ref" +version = "0.49.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a91b61776c839d0f1b7114901179afb0947aa7f4d30793ca1c56d335dfef485f" +dependencies = [ + "gix-actor", + "gix-features", + "gix-fs", + "gix-hash", + "gix-lock", + "gix-object", + "gix-path", + "gix-tempfile", + "gix-utils", + "gix-validate 0.9.4", + "memmap2", + "thiserror 2.0.17", + "winnow 0.6.26", +] + +[[package]] +name = "gix-refspec" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c056bb747868c7eb0aeb352c9f9181ab8ca3d0a2550f16470803500c6c413d" +dependencies = [ + "bstr", + "gix-hash", + "gix-revision", + "gix-validate 0.9.4", + "smallvec", + "thiserror 2.0.17", +] + +[[package]] +name = "gix-revision" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e1ddc474405a68d2ce8485705dd72fe6ce959f2f5fe718601ead5da2c8f9e7" +dependencies = [ + "bstr", + "gix-commitgraph", + "gix-date", + "gix-hash", + "gix-object", + "gix-revwalk", + "thiserror 2.0.17", +] + +[[package]] +name = "gix-revwalk" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "510026fc32f456f8f067d8f37c34088b97a36b2229d88a6a5023ef179fcb109d" +dependencies = [ + "gix-commitgraph", + "gix-date", + "gix-hash", + "gix-hashtable", + "gix-object", + "smallvec", + "thiserror 2.0.17", +] + +[[package]] +name = "gix-sec" +version = "0.10.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47aeb0f13de9ef2f3033f5ff218de30f44db827ac9f1286f9ef050aacddd5888" +dependencies = [ + "bitflags", + "gix-path", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "gix-submodule" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2455f8c0fcb6ebe2a6e83c8f522d30615d763eb2ef7a23c7d929f9476e89f5c" +dependencies = [ + "bstr", + "gix-config", + "gix-path", + "gix-pathspec", + "gix-refspec", + "gix-url", + "thiserror 2.0.17", +] + +[[package]] +name = "gix-tempfile" +version = "15.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2feb86ef094cc77a4a9a5afbfe5de626897351bbbd0de3cb9314baf3049adb82" +dependencies = [ + "gix-fs", + "libc", + "once_cell", + "parking_lot", + "tempfile", +] + +[[package]] +name = "gix-trace" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd971cd6961fb1ebb29a0052a4ab04d8498dbf363c122e137b04753a3bbb5c3" + +[[package]] +name = "gix-transport" +version = "0.43.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a1a41357b7236c03e0c984147f823d87c3e445a8581bac7006df141577200b" +dependencies = [ + "base64", + "bstr", + "gix-command", + "gix-credentials", + "gix-features", + "gix-packetline", + "gix-quote", + "gix-sec", + "gix-url", + "reqwest", + "thiserror 2.0.17", +] + +[[package]] +name = "gix-traverse" +version = "0.43.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed47d648619e23e93f971d2bba0d10c1100e54ef95d2981d609907a8cabac89" +dependencies = [ + "bitflags", + "gix-commitgraph", + "gix-date", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-revwalk", + "smallvec", + "thiserror 2.0.17", +] + +[[package]] +name = "gix-url" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d096fb733ba6bd3f5403dba8bd72bdd8809fe2b347b57844040b8f49c93492d9" +dependencies = [ + "bstr", + "gix-features", + "gix-path", + "percent-encoding", + "thiserror 2.0.17", + "url", +] + +[[package]] +name = "gix-utils" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff08f24e03ac8916c478c8419d7d3c33393da9bb41fa4c24455d5406aeefd35f" +dependencies = [ + "fastrand", + "unicode-normalization", +] + +[[package]] +name = "gix-validate" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b5f1253109da6c79ed7cf6e1e38437080bb6d704c76af14c93e2f255234084" +dependencies = [ + "bstr", + "thiserror 2.0.17", +] + +[[package]] +name = "gix-validate" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b1e63a5b516e970a594f870ed4571a8fdcb8a344e7bd407a20db8bd61dbfde4" +dependencies = [ + "bstr", + "thiserror 2.0.17", +] + +[[package]] +name = "gix-worktree" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756dbbe15188fa22540d5eab941f8f9cf511a5364d5aec34c88083c09f4bea13" +dependencies = [ + "bstr", + "gix-attributes", + "gix-features", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", + "gix-validate 0.9.4", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + +[[package]] +name = "indicatif" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "tokio", + "unicode-width", + "web-time", +] + +[[package]] +name = "insta" +version = "1.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b66886d14d18d420ab5052cbff544fc5d34d0b2cdd35eb5976aaa10a4a472e5" +dependencies = [ + "console", + "once_cell", + "pest", + "pest_derive", + "serde", + "similar", + "tempfile", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jiff" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a87d9b8105c23642f50cbbae03d1f75d8422c5cb98ce7ee9271f7ff7505be6b8" +dependencies = [ + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-sys 0.61.2", +] + +[[package]] +name = "jiff-static" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b787bebb543f8969132630c51fd0afab173a86c6abae56ff3b9e5e3e3f9f6e58" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68971ebff725b9e2ca27a601c5eb38a4c5d64422c4cbab0c535f248087eda5c2" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kstring" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.179" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags", + "libc", + "redox_syscall 0.7.0", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "maybe-async" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memmap2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +dependencies = [ + "libc", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "mockall" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "portable-atomic" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "predicates" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" + +[[package]] +name = "predicates-tree" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.10+spec-1.0.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prodash" +version = "29.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04bb108f648884c23b98a0e940ebc2c93c0c3b89f04dbaf7eb8256ce617d1bc" +dependencies = [ + "log", + "parking_lot", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 2.0.17", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rstest" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2c585be59b6b5dd66a9d2084aa1d8bd52fbdb806eafdeffb52791147862035" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "825ea780781b15345a146be27eaefb05085e337e869bff01b4306a4fd4a9ad5a" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn", + "unicode-ident", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.148" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678faa00651c9eb72dd2020cbdf275d92eccb2400d568e419efdd64838145cb4" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix 1.1.3", + "windows-sys 0.61.2", +] + +[[package]] +name = "terminal_size" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +dependencies = [ + "rustix 1.1.3", + "windows-sys 0.60.2", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.14", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "winnow 0.7.14", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow 0.7.14", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-bom" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "which" +version = "7.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" +dependencies = [ + "either", + "env_home", + "rustix 1.1.3", + "winsafe", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.6.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30e0d8dffbae3d840f64bda38e28391faef673a7b5a6017840f2a106c8145868" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..118b34a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,202 @@ +[package] +name = "agent-precommit" +version = "0.1.0" +edition = "2021" +rust-version = "1.75" +authors = ["agent-precommit contributors"] +description = "Smart pre-commit hooks for humans and AI coding agents" +documentation = "https://docs.rs/agent-precommit" +homepage = "https://github.com/agent-precommit/agent-precommit" +repository = "https://github.com/agent-precommit/agent-precommit" +readme = "Readme.md" +license = "MIT" +keywords = ["git", "precommit", "hooks", "ai", "agents"] +categories = ["command-line-utilities", "development-tools"] + +[[bin]] +name = "apc" +path = "src/main.rs" + +[lib] +name = "agent_precommit" +path = "src/lib.rs" + +[dependencies] +# CLI framework +clap = { version = "4.5", features = ["derive", "env", "wrap_help", "string"] } +clap_complete = "4.5" + +# Async runtime +tokio = { version = "1.40", features = ["full"] } + +# Serialization +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +toml = "0.8" + +# Error handling +thiserror = "2.0" +anyhow = "1.0" + +# Logging and output +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "ansi"] } + +# Terminal UI +console = "0.15" +indicatif = { version = "0.17", features = ["tokio"] } +dialoguer = "0.11" + +# Process execution +which = "7.0" + +# File system operations +walkdir = "2.5" +glob = "0.3" +dirs = "6.0" + +# Git operations +gix = { version = "0.68", default-features = false, features = ["blocking-http-transport-reqwest-rust-tls"] } + +# Time +humantime = "2.1" +chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } + +# Regex for pattern matching +regex = "1.11" + +[dev-dependencies] +# Testing +assert_cmd = "2.0" +predicates = "3.1" +tempfile = "3.14" +rstest = "0.23" +pretty_assertions = "1.4" +insta = { version = "1.41", features = ["yaml", "redactions"] } +mockall = "0.13" + +# Fuzzing support +arbitrary = { version = "1.4", features = ["derive"] } + +# Benchmarking +criterion = { version = "0.5", features = ["async_tokio"] } + +[[bench]] +name = "benchmarks" +harness = false + +[features] +default = [] +# Enable additional checks during development +dev = [] + +[profile.release] +lto = true +codegen-units = 1 +panic = "abort" +strip = true +opt-level = 3 + +[profile.dev] +debug = true +opt-level = 0 + +[profile.dev.package."*"] +opt-level = 2 + +[profile.bench] +debug = true + +# ============================================================================= +# Linting Configuration - Extremely Strict +# ============================================================================= + +[lints.rust] +# Deny unsafe code by default +unsafe_code = "deny" + +# Treat all warnings as errors +warnings = "deny" + +# Future compatibility +future_incompatible = { level = "deny", priority = -1 } +rust_2018_idioms = { level = "deny", priority = -1 } +rust_2021_compatibility = { level = "deny", priority = -1 } +rust_2024_compatibility = { level = "warn", priority = -1 } + +# Documentation +missing_docs = "warn" +missing_debug_implementations = "warn" + +# Unsafe patterns +let_underscore_drop = "deny" +non_ascii_idents = "deny" + +[lints.clippy] +# ============================================================================= +# Clippy Lint Groups - All set to deny/warn for maximum strictness +# ============================================================================= + +# Correctness - These are almost certainly bugs +correctness = { level = "deny", priority = -1 } + +# Suspicious - Code that is most likely wrong or useless +suspicious = { level = "deny", priority = -1 } + +# Style - Code that should be written in a more idiomatic way +style = { level = "warn", priority = -1 } + +# Complexity - Code that does something simple but in a complex way +complexity = { level = "warn", priority = -1 } + +# Performance - Code that can be written to run faster +perf = { level = "warn", priority = -1 } + +# Pedantic - Lints which are rather strict or might have false positives +pedantic = { level = "warn", priority = -1 } + +# Nursery - New lints that are still under development +nursery = { level = "warn", priority = -1 } + +# ============================================================================= +# Individual Clippy Lints - Fine-grained control +# ============================================================================= + +# Absolutely deny these +unwrap_used = "deny" +expect_used = "deny" +panic = "deny" +todo = "deny" +unimplemented = "deny" +unreachable = "deny" +dbg_macro = "deny" +print_stdout = "deny" +print_stderr = "deny" + +# Security-related +hardcoded_credentials = "deny" + +# Error handling +result_large_err = "warn" +error_impl_error = "deny" + +# Code quality +cognitive_complexity = "warn" +too_many_lines = "warn" +too_many_arguments = "warn" +fn_params_excessive_bools = "warn" +struct_excessive_bools = "warn" + +# Documentation +missing_errors_doc = "warn" +missing_panics_doc = "warn" +missing_safety_doc = "deny" + +# Allow some pedantic lints that are too noisy +module_name_repetitions = "allow" +must_use_candidate = "allow" +doc_markdown = "allow" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..09173e6 --- /dev/null +++ b/Makefile @@ -0,0 +1,109 @@ +# Makefile for agent-precommit development +# Usage: make + +.PHONY: all build release test lint fmt check clean install docs bench help + +# Default target +all: fmt lint test build + +# Build debug version +build: + cargo build + +# Build release version +release: + cargo build --release + +# Run all tests +test: + cargo test --all-features + +# Run tests with output +test-verbose: + cargo test --all-features -- --nocapture + +# Run integration tests only +test-integration: + cargo test --test '*' + +# Run lints (clippy) +lint: + cargo clippy --all-targets --all-features -- -D warnings + +# Format code +fmt: + cargo fmt --all + +# Check formatting +fmt-check: + cargo fmt --all -- --check + +# Run all checks (format + lint + test) +check: fmt-check lint test + +# Clean build artifacts +clean: + cargo clean + rm -rf target/ + rm -rf dist/ + rm -rf *.egg-info/ + find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true + +# Install locally +install: + cargo install --path . + +# Build documentation +docs: + cargo doc --no-deps --open + +# Run benchmarks +bench: + cargo bench + +# Security audit +audit: + cargo audit + +# Update dependencies +update: + cargo update + +# Generate coverage report +coverage: + cargo llvm-cov --all-features --lcov --output-path lcov.info + cargo llvm-cov report --html + +# Build Python wheel +wheel: + maturin build --release + +# Run pre-commit hooks +pre-commit: + pre-commit run --all-files + +# Install development dependencies +dev-setup: + rustup component add rustfmt clippy llvm-tools-preview + cargo install cargo-audit cargo-llvm-cov + pip install pre-commit maturin + pre-commit install + +# Show help +help: + @echo "agent-precommit development targets:" + @echo "" + @echo " make build Build debug version" + @echo " make release Build release version" + @echo " make test Run all tests" + @echo " make lint Run Clippy lints" + @echo " make fmt Format code" + @echo " make check Run all checks (fmt + lint + test)" + @echo " make clean Clean build artifacts" + @echo " make install Install locally" + @echo " make docs Build and open documentation" + @echo " make bench Run benchmarks" + @echo " make coverage Generate coverage report" + @echo " make wheel Build Python wheel" + @echo " make dev-setup Install development dependencies" + @echo " make help Show this help" diff --git a/agent-precommit.toml b/agent-precommit.toml new file mode 100644 index 0000000..9201def --- /dev/null +++ b/agent-precommit.toml @@ -0,0 +1,81 @@ +# agent-precommit configuration for the agent-precommit project itself +# This serves as both a working example and dogfooding our own tool + +[detection] +# Additional environment variables that indicate an agent +agent_env_vars = [] + +[integration] +# We use pre-commit framework for base checks +pre_commit = true +pre_commit_path = ".pre-commit-config.yaml" + +[human] +# Human mode: fast checks for quick iteration +checks = ["pre-commit"] +timeout = "30s" +fail_fast = true + +[agent] +# Agent mode: thorough checks for merge-ready commits +checks = [ + "pre-commit-all", + "no-merge-conflicts", + "fmt-check", + "clippy", + "test-unit", + "build-verify", +] +timeout = "15m" +fail_fast = false + +# Run in parallel where possible +parallel_groups = [ + ["pre-commit-all", "no-merge-conflicts"], + ["fmt-check", "clippy"], + ["test-unit"], + ["build-verify"], +] + +# ============================================================================= +# Check Definitions +# ============================================================================= + +[checks.pre-commit] +run = "pre-commit run" +description = "Run pre-commit on staged files" + +[checks.pre-commit-all] +run = "pre-commit run --all-files" +description = "Run pre-commit on all files" + +[checks.no-merge-conflicts] +run = """ +git fetch origin main --quiet 2>/dev/null || git fetch origin master --quiet 2>/dev/null || true +MAIN_BRANCH=$(git rev-parse --verify origin/main 2>/dev/null && echo "main" || echo "master") +BASE=$(git merge-base HEAD origin/$MAIN_BRANCH 2>/dev/null || echo "") +if [ -n "$BASE" ]; then + if git merge-tree $BASE HEAD origin/$MAIN_BRANCH 2>/dev/null | grep -q "^<<<<<<<"; then + echo "❌ Would conflict with $MAIN_BRANCH" + exit 1 + fi +fi +echo "✓ No conflicts with main branch" +""" +description = "Check for merge conflicts with main/master" + +[checks.fmt-check] +run = "cargo fmt --all -- --check" +description = "Check Rust code formatting" + +[checks.clippy] +run = "cargo clippy --all-targets --all-features -- -D warnings" +description = "Run Clippy lints with strict warnings" + +[checks.test-unit] +run = "cargo test --all-features" +description = "Run all unit tests" + +[checks.build-verify] +run = "cargo build --release" +description = "Verify release build succeeds" diff --git a/benches/benchmarks.rs b/benches/benchmarks.rs new file mode 100644 index 0000000..2d5358e --- /dev/null +++ b/benches/benchmarks.rs @@ -0,0 +1,34 @@ +//! Benchmarks for agent-precommit. + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +fn benchmark_mode_detection(c: &mut Criterion) { + c.bench_function("mode_detection", |b| { + b.iter(|| { + // Simple benchmark placeholder + // In a real benchmark, we'd test the detector + black_box(1 + 1) + }); + }); +} + +fn benchmark_config_parsing(c: &mut Criterion) { + let toml_content = r#" +[human] +checks = ["pre-commit"] +timeout = "30s" + +[agent] +checks = ["pre-commit-all", "test-unit"] +timeout = "15m" +"#; + + c.bench_function("config_parsing", |b| { + b.iter(|| { + let _: toml::Value = toml::from_str(black_box(toml_content)).unwrap(); + }); + }); +} + +criterion_group!(benches, benchmark_mode_detection, benchmark_config_parsing); +criterion_main!(benches); diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..9260f77 --- /dev/null +++ b/clippy.toml @@ -0,0 +1,79 @@ +# ============================================================================= +# Clippy Configuration - Strict Linting Thresholds +# ============================================================================= +# This file provides additional configuration for Clippy lints +# Main lint levels are configured in Cargo.toml under [lints.clippy] + +# Cognitive complexity threshold - functions more complex than this trigger a warning +cognitive-complexity-threshold = 25 + +# Maximum number of lines in a function before triggering too_many_lines +too-many-lines-threshold = 100 + +# Maximum number of function arguments before triggering too_many_arguments +too-many-arguments-threshold = 7 + +# Maximum number of bool parameters before triggering fn_params_excessive_bools +max-fn-params-bools = 3 + +# Maximum number of bool fields in a struct +max-struct-bools = 3 + +# MSRV - Minimum Supported Rust Version +# This ensures suggested fixes are compatible with our rust-version in Cargo.toml +msrv = "1.75" + +# Standard macros that should not trigger certain lints +standard-macro-braces = [] + +# Allowed duplicate modules for module_name_repetitions +allowed-duplicate-crates = [] + +# Type complexity threshold +type-complexity-threshold = 250 + +# Single char binding names allowed in specific contexts +single-char-binding-names-threshold = 4 + +# Trivial copy size limit (bytes) +trivial-copy-size-limit = 16 + +# Pass by value size limit (bytes) +pass-by-value-size-limit = 256 + +# Allowed wildcard imports (for tests) +# allowed-wildcard-imports = ["crate::tests::*"] + +# Disallowed names (avoid generic names) +disallowed-names = ["foo", "bar", "baz", "quux", "temp", "tmp"] + +# Enforce explicit return in closures +accept-comment-above-statement = true +accept-comment-above-attributes = true + +# Avoid assert in production code paths +avoid-breaking-exported-api = true + +# Large error variant threshold for result_large_err +large-error-threshold = 128 + +# Future size threshold for large_futures lint +future-size-threshold = 16384 + +# Verbose bit mask threshold +verbose-bit-mask-threshold = 1 + +# Literal representation +literal-representation-threshold = 16777215 + +# Array size threshold for explicit iteration +array-size-threshold = 512 + +# Stack size threshold +stack-size-threshold = 512000 + +# Vec box size threshold +vec-box-size-threshold = 4096 + +# Allowed scripts for Unicode identifiers +# We only allow ASCII identifiers (configured in Cargo.toml) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3ee25e4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,80 @@ +[build-system] +requires = ["maturin>=1.4,<2.0"] +build-backend = "maturin" + +[project] +name = "agent-precommit" +version = "0.1.0" +description = "Smart pre-commit hooks for humans and AI coding agents" +readme = "Readme.md" +license = { text = "MIT" } +authors = [ + { name = "agent-precommit contributors" } +] +keywords = ["git", "precommit", "hooks", "ai", "agents", "cli"] +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Rust", + "Topic :: Software Development :: Quality Assurance", + "Topic :: Software Development :: Version Control :: Git", +] +requires-python = ">=3.8" + +[project.urls] +Homepage = "https://github.com/agent-precommit/agent-precommit" +Documentation = "https://docs.rs/agent-precommit" +Repository = "https://github.com/agent-precommit/agent-precommit" +Changelog = "https://github.com/agent-precommit/agent-precommit/blob/main/CHANGELOG.md" + +[project.scripts] +apc = "agent_precommit:main" +agent-precommit = "agent_precommit:main" + +[tool.maturin] +# Build only the binary, not a Python extension +bindings = "bin" +# Strip the binary for smaller size +strip = true +# Features to enable +features = [] +# Module name +module-name = "agent_precommit" + +# Compatibility settings +compatibility = "manylinux2014" + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] + +[tool.ruff] +target-version = "py38" +line-length = 100 + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade +] +ignore = [ + "E501", # line too long, handled by formatter +] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" diff --git a/python/agent_precommit/__init__.py b/python/agent_precommit/__init__.py new file mode 100644 index 0000000..0674f22 --- /dev/null +++ b/python/agent_precommit/__init__.py @@ -0,0 +1,84 @@ +""" +agent-precommit: Smart pre-commit hooks for humans and AI coding agents. + +This package provides a Python wrapper for the agent-precommit (apc) CLI tool. +The actual binary is bundled with this package and called via subprocess. + +Usage: + $ apc init # Initialize configuration + $ apc install # Install git hook + $ apc run # Run checks + $ apc detect # Show detected mode +""" + +import os +import subprocess +import sys +from pathlib import Path + +__version__ = "0.1.0" +__all__ = ["main", "run_apc"] + + +def _get_binary_path() -> Path: + """Get the path to the bundled apc binary.""" + # The binary is bundled in the same directory as this module + module_dir = Path(__file__).parent + + if sys.platform == "win32": + binary_name = "apc.exe" + else: + binary_name = "apc" + + binary_path = module_dir / binary_name + + if not binary_path.exists(): + # Fall back to looking in PATH + import shutil + found = shutil.which("apc") + if found: + return Path(found) + raise FileNotFoundError( + f"Could not find apc binary. Expected at {binary_path} or in PATH." + ) + + return binary_path + + +def run_apc(*args: str) -> subprocess.CompletedProcess: + """ + Run the apc binary with the given arguments. + + Args: + *args: Command-line arguments to pass to apc. + + Returns: + CompletedProcess with the result. + + Example: + >>> result = run_apc("detect") + >>> print(result.stdout) + """ + binary = _get_binary_path() + return subprocess.run( + [str(binary), *args], + capture_output=True, + text=True, + ) + + +def main() -> int: + """ + Main entry point for the apc command. + + This function is called when running `apc` or `agent-precommit` from the command line. + """ + binary = _get_binary_path() + + # Pass through all arguments to the binary + result = subprocess.run([str(binary), *sys.argv[1:]]) + return result.returncode + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..b9da50b --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,69 @@ +# ============================================================================= +# Rustfmt Configuration - Consistent, Readable Code Style +# ============================================================================= +# Using stable options only for maximum compatibility +# Run with: cargo fmt +# Check with: cargo fmt --check + +# Edition +edition = "2021" + +# Maximum line width +max_width = 100 + +# Use 4 spaces for indentation +tab_spaces = 4 +hard_tabs = false + +# Imports +imports_granularity = "Module" +group_imports = "StdExternalCrate" +reorder_imports = true +reorder_modules = true + +# Comments +wrap_comments = true +format_code_in_doc_comments = true +doc_comment_code_block_width = 80 +normalize_comments = true +normalize_doc_attributes = true + +# Formatting choices +use_small_heuristics = "Default" +fn_single_line = false +where_single_line = false +force_multiline_blocks = false +format_strings = false + +# Struct and enum formatting +struct_lit_single_line = true +enum_discrim_align_threshold = 20 + +# Match arms +match_arm_blocks = true +match_arm_leading_pipes = "Never" +match_block_trailing_comma = true + +# Control flow +control_brace_style = "AlwaysSameLine" +brace_style = "SameLineWhere" + +# Other +newline_style = "Unix" +remove_nested_parens = true +combine_control_expr = true +overflow_delimited_expr = false +trailing_comma = "Vertical" +trailing_semicolon = true +use_field_init_shorthand = true +use_try_shorthand = true +force_explicit_abi = true +condense_wildcard_suffixes = false +format_generated_files = true +generated_marker_line_search_limit = 5 +hex_literal_case = "Lower" +space_after_colon = true +space_before_colon = false +spaces_around_ranges = false +binop_separator = "Front" +type_punctuation_density = "Wide" diff --git a/src/checks/builtin.rs b/src/checks/builtin.rs new file mode 100644 index 0000000..1cbf56d --- /dev/null +++ b/src/checks/builtin.rs @@ -0,0 +1,48 @@ +//! Built-in check definitions. +//! +//! These checks are available by default in all configurations. + +/// Names of built-in checks. +pub mod names { + /// Run pre-commit on staged files. + pub const PRE_COMMIT: &str = "pre-commit"; + /// Run pre-commit on all files. + pub const PRE_COMMIT_ALL: &str = "pre-commit-all"; + /// Check for merge conflicts with main/master. + pub const NO_MERGE_CONFLICTS: &str = "no-merge-conflicts"; + /// Run unit tests. + pub const TEST_UNIT: &str = "test-unit"; + /// Run integration tests. + pub const TEST_INTEGRATION: &str = "test-integration"; + /// Scan for secrets. + pub const SECURITY_SCAN: &str = "security-scan"; + /// Verify build works. + pub const BUILD_VERIFY: &str = "build-verify"; +} + +/// Returns true if a check name is a built-in check. +#[must_use] +pub fn is_builtin(name: &str) -> bool { + matches!( + name, + names::PRE_COMMIT + | names::PRE_COMMIT_ALL + | names::NO_MERGE_CONFLICTS + | names::TEST_UNIT + | names::TEST_INTEGRATION + | names::SECURITY_SCAN + | names::BUILD_VERIFY + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_builtin() { + assert!(is_builtin("pre-commit")); + assert!(is_builtin("no-merge-conflicts")); + assert!(!is_builtin("custom-check")); + } +} diff --git a/src/checks/mod.rs b/src/checks/mod.rs new file mode 100644 index 0000000..59c2f5e --- /dev/null +++ b/src/checks/mod.rs @@ -0,0 +1,6 @@ +//! Built-in check implementations. +//! +//! This module provides the built-in checks that come with agent-precommit. + +pub mod builtin; +pub mod precommit; diff --git a/src/checks/precommit.rs b/src/checks/precommit.rs new file mode 100644 index 0000000..a453ea0 --- /dev/null +++ b/src/checks/precommit.rs @@ -0,0 +1,77 @@ +//! Pre-commit framework integration. +//! +//! This module provides integration with the pre-commit framework. + +use crate::core::error::{Error, Result}; +use crate::core::executor::{ExecuteOptions, Executor}; +use std::path::Path; + +/// Path to the pre-commit config file. +pub const PRE_COMMIT_CONFIG: &str = ".pre-commit-config.yaml"; + +/// Checks if pre-commit is installed. +pub fn is_installed() -> bool { + Executor::command_exists("pre-commit") +} + +/// Checks if a pre-commit config exists. +pub fn config_exists(repo_root: &Path) -> bool { + repo_root.join(PRE_COMMIT_CONFIG).exists() +} + +/// Runs pre-commit on staged files. +pub async fn run_staged(repo_root: &Path) -> Result { + run_with_args(repo_root, &[]).await +} + +/// Runs pre-commit on all files. +pub async fn run_all(repo_root: &Path) -> Result { + run_with_args(repo_root, &["--all-files"]).await +} + +/// Runs pre-commit with custom arguments. +async fn run_with_args(repo_root: &Path, args: &[&str]) -> Result { + if !is_installed() { + return Err(Error::PreCommitNotFound); + } + + if !config_exists(repo_root) { + return Err(Error::PreCommitConfigNotFound { + path: repo_root.join(PRE_COMMIT_CONFIG), + }); + } + + let cmd = if args.is_empty() { + "pre-commit run".to_string() + } else { + format!("pre-commit run {}", args.join(" ")) + }; + + let executor = Executor::new(); + let output = executor + .execute( + &cmd, + ExecuteOptions::default() + .cwd(repo_root) + .capture_output(false), + ) + .await?; + + Ok(output.success()) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + #[test] + fn test_config_exists() { + let temp = TempDir::new().expect("create temp dir"); + assert!(!config_exists(temp.path())); + + std::fs::write(temp.path().join(PRE_COMMIT_CONFIG), "repos: []") + .expect("write config"); + assert!(config_exists(temp.path())); + } +} diff --git a/src/cli/commands.rs b/src/cli/commands.rs new file mode 100644 index 0000000..f9aa8fd --- /dev/null +++ b/src/cli/commands.rs @@ -0,0 +1,423 @@ +//! CLI command implementations. + +use crate::config::{Config, CONFIG_FILE_NAME}; +use crate::core::detector::{Detector, Mode}; +use crate::core::error::{Error, Result}; +use crate::core::git::GitRepo; +use crate::core::runner::Runner; +use console::style; +use std::io::Write; +use std::path::PathBuf; +use std::process::ExitCode; + +/// Hook script template. +const HOOK_SCRIPT: &str = r#"#!/bin/sh +# agent-precommit hook - installed by `apc install` +# https://github.com/agent-precommit/agent-precommit + +# Skip if APC_SKIP is set +if [ "$APC_SKIP" = "1" ]; then + exit 0 +fi + +# Run agent-precommit +exec apc run +"#; + +/// Hook marker comment. +const HOOK_MARKER: &str = "# agent-precommit hook"; + +/// Initialize configuration. +pub fn init(preset: Option<&str>, force: bool) -> Result { + let config_path = PathBuf::from(CONFIG_FILE_NAME); + + // Check if config already exists + if config_path.exists() && !force { + eprintln!( + "{} Configuration already exists: {}", + style("!").yellow(), + config_path.display() + ); + eprintln!(" Use --force to overwrite."); + return Ok(ExitCode::FAILURE); + } + + // Generate config + let config = match preset { + Some(p) => Config::for_preset(p), + None => { + // Auto-detect existing pre-commit config + let mut config = Config::default(); + if PathBuf::from(".pre-commit-config.yaml").exists() { + config.integration.pre_commit = true; + eprintln!( + "{} Detected .pre-commit-config.yaml - enabling integration", + style("•").cyan() + ); + } + config + } + }; + + // Write config + let toml = toml::to_string_pretty(&config) + .map_err(|e| Error::Internal { + message: format!("Failed to serialize config: {e}"), + })?; + + std::fs::write(&config_path, toml).map_err(|e| Error::io("write config", e))?; + + eprintln!( + "{} Created {}", + style("✓").green(), + config_path.display() + ); + + if let Some(p) = preset { + eprintln!(" Using preset: {p}"); + } + + eprintln!("\nNext steps:"); + eprintln!(" 1. Review and customize {CONFIG_FILE_NAME}"); + eprintln!(" 2. Run: apc install"); + + Ok(ExitCode::SUCCESS) +} + +/// Install git hook. +pub fn install(force: bool) -> Result { + let repo = GitRepo::discover()?; + let hooks_dir = repo.hooks_dir(); + let hook_path = hooks_dir.join("pre-commit"); + + // Create hooks directory if needed + if !hooks_dir.exists() { + std::fs::create_dir_all(&hooks_dir).map_err(|e| Error::io("create hooks dir", e))?; + } + + // Check for existing hook + if hook_path.exists() { + let content = + std::fs::read_to_string(&hook_path).map_err(|e| Error::io("read existing hook", e))?; + + // Check if it's our hook + if content.contains(HOOK_MARKER) { + eprintln!( + "{} Hook already installed at {}", + style("✓").green(), + hook_path.display() + ); + return Ok(ExitCode::SUCCESS); + } + + if !force { + return Err(Error::HookExists { path: hook_path }); + } + + // Backup existing hook + let backup_path = hooks_dir.join("pre-commit.bak"); + std::fs::rename(&hook_path, &backup_path).map_err(|e| Error::io("backup hook", e))?; + eprintln!( + "{} Backed up existing hook to {}", + style("•").cyan(), + backup_path.display() + ); + } + + // Write hook + std::fs::write(&hook_path, HOOK_SCRIPT).map_err(|e| Error::io("write hook", e))?; + + // Make executable on Unix + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = std::fs::metadata(&hook_path) + .map_err(|e| Error::io("get hook metadata", e))? + .permissions(); + perms.set_mode(0o755); + std::fs::set_permissions(&hook_path, perms).map_err(|e| Error::io("set hook perms", e))?; + } + + eprintln!( + "{} Installed pre-commit hook at {}", + style("✓").green(), + hook_path.display() + ); + + Ok(ExitCode::SUCCESS) +} + +/// Uninstall git hook. +pub fn uninstall() -> Result { + let repo = GitRepo::discover()?; + let hook_path = repo.hook_path("pre-commit"); + + if !hook_path.exists() { + eprintln!( + "{} No hook installed at {}", + style("•").cyan(), + hook_path.display() + ); + return Ok(ExitCode::SUCCESS); + } + + // Check if it's our hook + let content = + std::fs::read_to_string(&hook_path).map_err(|e| Error::io("read hook", e))?; + + if !content.contains(HOOK_MARKER) { + eprintln!( + "{} Hook at {} was not installed by agent-precommit", + style("!").yellow(), + hook_path.display() + ); + eprintln!(" Remove manually if desired."); + return Ok(ExitCode::FAILURE); + } + + std::fs::remove_file(&hook_path).map_err(|e| Error::io("remove hook", e))?; + + eprintln!( + "{} Removed pre-commit hook from {}", + style("✓").green(), + hook_path.display() + ); + + // Check for backup + let backup_path = repo.hooks_dir().join("pre-commit.bak"); + if backup_path.exists() { + eprintln!( + " Backup exists at {} - restore if needed", + backup_path.display() + ); + } + + Ok(ExitCode::SUCCESS) +} + +/// Run checks. +pub fn run(mode_override: Option<&str>, check: Option<&str>, _all: bool) -> Result { + // Check for skip + if std::env::var("APC_SKIP").ok().as_deref() == Some("1") { + eprintln!("{} Skipping checks (APC_SKIP=1)", style("•").cyan()); + return Ok(ExitCode::SUCCESS); + } + + // Load config + let config = Config::load_or_default()?; + + // Detect or override mode + let mode = if let Some(m) = mode_override { + m.parse().map_err(|e: String| Error::ConfigInvalid { + field: "mode".to_string(), + message: e, + })? + } else { + let detector = Detector::new(&config); + let detection = detector.detect(); + eprintln!( + "{} Mode: {} ({})", + style("•").cyan(), + style(detection.mode.name()).bold(), + detection.reason + ); + detection.mode + }; + + // Create runner + let runner = Runner::new(config); + + // Run checks + let result = tokio::runtime::Runtime::new() + .map_err(|e| Error::Internal { + message: format!("Failed to create runtime: {e}"), + })? + .block_on(async { + if let Some(name) = check { + let check_result = runner.run_single(name, mode).await?; + Ok(crate::core::runner::RunResult { + mode, + checks: vec![check_result], + duration: std::time::Duration::ZERO, + }) + } else { + runner.run(mode).await + } + })?; + + // Print summary + eprintln!(); + if result.success() { + eprintln!( + "{} All checks passed ({} passed, {} skipped) in {:?}", + style("✓").green().bold(), + result.passed_count(), + result.skipped_count(), + result.duration + ); + Ok(ExitCode::SUCCESS) + } else { + eprintln!( + "{} {} check(s) failed", + style("✗").red().bold(), + result.failed_count() + ); + + // Show failed check details + for check in result.failed_checks() { + eprintln!(); + eprintln!(" {} {}", style("Failed:").red(), check.name); + if !check.output.combined_output().is_empty() { + for line in check.output.combined_output().lines().take(20) { + eprintln!(" {line}"); + } + } + } + + Ok(ExitCode::FAILURE) + } +} + +/// Show detected mode. +pub fn detect() -> Result { + let config = Config::load_or_default()?; + let detector = Detector::new(&config); + let detection = detector.detect(); + + eprintln!("Detected mode: {}", style(detection.mode.name()).bold()); + eprintln!("Reason: {}", detection.reason); + + // Show environment info + eprintln!(); + eprintln!("Environment:"); + + let env_vars = ["APC_MODE", "AGENT_MODE", "CI", "GITHUB_ACTIONS"]; + for var in env_vars { + if let Ok(value) = std::env::var(var) { + eprintln!(" {var}={value}"); + } + } + + eprintln!(); + eprintln!("TTY: stdin={}, stdout={}", + std::io::stdin().is_terminal(), + std::io::stdout().is_terminal() + ); + + Ok(ExitCode::SUCCESS) +} + +/// List configured checks. +pub fn list(mode: Option<&str>) -> Result { + let config = Config::load_or_default()?; + + let mode: Option = mode + .map(|m| m.parse()) + .transpose() + .map_err(|e: String| Error::ConfigInvalid { + field: "mode".to_string(), + message: e, + })?; + + // Print checks by mode + if mode.is_none() || mode == Some(Mode::Human) { + eprintln!("{}", style("Human mode checks:").bold()); + for name in &config.human.checks { + print_check(&config, name); + } + eprintln!(); + } + + if mode.is_none() || mode == Some(Mode::Agent) || mode == Some(Mode::Ci) { + eprintln!("{}", style("Agent mode checks:").bold()); + for name in &config.agent.checks { + print_check(&config, name); + } + } + + Ok(ExitCode::SUCCESS) +} + +/// Prints a check's details. +fn print_check(config: &Config, name: &str) { + let check = config.checks.get(name); + let description = check + .map(|c| c.description.as_str()) + .filter(|d| !d.is_empty()) + .unwrap_or("(no description)"); + + eprintln!(" {} - {}", style(name).cyan(), description); +} + +/// Validate configuration. +pub fn validate() -> Result { + match Config::load() { + Ok(config) => { + match config.validate() { + Ok(()) => { + eprintln!("{} Configuration is valid", style("✓").green()); + Ok(ExitCode::SUCCESS) + } + Err(e) => { + eprintln!("{} Configuration validation failed: {e}", style("✗").red()); + Ok(ExitCode::FAILURE) + } + } + } + Err(Error::ConfigNotFound { path }) => { + eprintln!( + "{} Configuration not found: {}", + style("!").yellow(), + path.display() + ); + eprintln!(" Run: apc init"); + Ok(ExitCode::FAILURE) + } + Err(e) => { + eprintln!("{} Failed to load configuration: {e}", style("✗").red()); + Ok(ExitCode::FAILURE) + } + } +} + +/// Show configuration. +pub fn config(raw: bool) -> Result { + match Config::find_config_file() { + Ok(path) => { + eprintln!("Configuration file: {}", path.display()); + + if raw { + let content = + std::fs::read_to_string(&path).map_err(|e| Error::io("read config", e))?; + eprintln!(); + std::io::stdout() + .write_all(content.as_bytes()) + .map_err(|e| Error::io("write output", e))?; + } + + Ok(ExitCode::SUCCESS) + } + Err(Error::ConfigNotFound { .. }) => { + eprintln!( + "{} No configuration file found", + style("!").yellow() + ); + eprintln!(" Run: apc init"); + Ok(ExitCode::FAILURE) + } + Err(e) => Err(e), + } +} + +/// Generate shell completions. +pub fn completions(shell: clap_complete::Shell) { + use clap::CommandFactory; + clap_complete::generate( + shell, + &mut super::Cli::command(), + "apc", + &mut std::io::stdout(), + ); +} + +use std::io::IsTerminal; diff --git a/src/cli/mod.rs b/src/cli/mod.rs new file mode 100644 index 0000000..996f41c --- /dev/null +++ b/src/cli/mod.rs @@ -0,0 +1,243 @@ +//! Command-line interface for agent-precommit. +//! +//! This module provides the `apc` CLI with subcommands for: +//! - `init`: Initialize configuration +//! - `install`: Install git hook +//! - `uninstall`: Remove git hook +//! - `run`: Run checks manually +//! - `detect`: Show detected mode +//! - `list`: List configured checks +//! - `validate`: Validate configuration + +mod commands; + +use crate::core::error::Result; +use clap::{Parser, Subcommand}; +use std::process::ExitCode; +use tracing_subscriber::EnvFilter; + +/// Smart pre-commit hooks for humans and AI coding agents. +#[derive(Debug, Parser)] +#[command( + name = "apc", + author, + version, + about = "Smart pre-commit hooks for humans and AI coding agents", + long_about = r#" +agent-precommit (apc) provides intelligent pre-commit hooks that detect +whether a commit is being made by a human or an AI coding agent. + +Human commits get fast, staged-only checks. +Agent commits get thorough, merge-ready validation. + +Quick start: + apc init # Create configuration + apc install # Install git hook + # Done! Commits now auto-detect mode. + +Environment variables: + APC_MODE=human|agent|ci Force a specific mode + AGENT_MODE=1 Trigger agent mode + APC_SKIP=1 Skip all checks +"#, + propagate_version = true +)] +pub struct Cli { + /// Subcommand to run. + #[command(subcommand)] + pub command: Option, + + /// Enable verbose output. + #[arg(short, long, global = true)] + pub verbose: bool, + + /// Suppress non-error output. + #[arg(short, long, global = true)] + pub quiet: bool, + + /// Use color output. + #[arg(long, global = true, default_value = "auto")] + pub color: ColorChoice, +} + +/// Color output choice. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, clap::ValueEnum)] +pub enum ColorChoice { + /// Always use color. + Always, + /// Auto-detect color support. + #[default] + Auto, + /// Never use color. + Never, +} + +/// Available subcommands. +#[derive(Debug, Subcommand)] +pub enum Commands { + /// Initialize agent-precommit configuration. + #[command(visible_alias = "i")] + Init { + /// Use a preset configuration. + #[arg(short, long, value_parser = ["python", "node", "rust", "go"])] + preset: Option, + + /// Overwrite existing configuration. + #[arg(short, long)] + force: bool, + }, + + /// Install the git pre-commit hook. + Install { + /// Overwrite existing hook. + #[arg(short, long)] + force: bool, + }, + + /// Remove the git pre-commit hook. + Uninstall, + + /// Run checks manually. + #[command(visible_alias = "r")] + Run { + /// Force a specific mode. + #[arg(short, long, value_parser = ["human", "agent", "ci"])] + mode: Option, + + /// Run only a specific check. + #[arg(short, long)] + check: Option, + + /// Run all checks regardless of conditions. + #[arg(long)] + all: bool, + }, + + /// Show the detected mode and reasoning. + #[command(visible_alias = "d")] + Detect, + + /// List all configured checks. + #[command(visible_alias = "l")] + List { + /// Show checks for a specific mode. + #[arg(short, long, value_parser = ["human", "agent", "ci"])] + mode: Option, + }, + + /// Validate the configuration file. + #[command(visible_alias = "v")] + Validate, + + /// Show configuration file location and contents. + Config { + /// Output raw TOML. + #[arg(long)] + raw: bool, + }, + + /// Generate shell completions. + Completions { + /// Shell to generate completions for. + #[arg(value_enum)] + shell: clap_complete::Shell, + }, +} + +/// Runs the CLI. +pub fn run() -> Result { + let cli = Cli::parse(); + + // Set up logging + setup_logging(cli.verbose, cli.quiet); + + // Set up color + setup_color(cli.color); + + // If no subcommand, run the default action (same as `apc run`) + match cli.command { + Some(Commands::Init { preset, force }) => commands::init(preset.as_deref(), force), + Some(Commands::Install { force }) => commands::install(force), + Some(Commands::Uninstall) => commands::uninstall(), + Some(Commands::Run { mode, check, all }) => { + commands::run(mode.as_deref(), check.as_deref(), all) + } + Some(Commands::Detect) => commands::detect(), + Some(Commands::List { mode }) => commands::list(mode.as_deref()), + Some(Commands::Validate) => commands::validate(), + Some(Commands::Config { raw }) => commands::config(raw), + Some(Commands::Completions { shell }) => { + commands::completions(shell); + Ok(ExitCode::SUCCESS) + } + None => commands::run(None, None, false), + } +} + +/// Sets up logging based on verbosity flags. +fn setup_logging(verbose: bool, quiet: bool) { + let filter = if quiet { + "error" + } else if verbose { + "debug" + } else { + "info" + }; + + let env_filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new(filter)); + + tracing_subscriber::fmt() + .with_env_filter(env_filter) + .with_target(false) + .with_writer(std::io::stderr) + .init(); +} + +/// Sets up color output. +fn setup_color(choice: ColorChoice) { + match choice { + ColorChoice::Always => { + console::set_colors_enabled(true); + console::set_colors_enabled_stderr(true); + } + ColorChoice::Never => { + console::set_colors_enabled(false); + console::set_colors_enabled_stderr(false); + } + ColorChoice::Auto => { + // Let console crate auto-detect + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cli_parsing() { + // Test that CLI parses without errors + let cli = Cli::try_parse_from(["apc", "--help"]); + // --help causes early exit, so this will be an error + assert!(cli.is_err()); + } + + #[test] + fn test_cli_version() { + let cli = Cli::try_parse_from(["apc", "--version"]); + assert!(cli.is_err()); // --version causes early exit + } + + #[test] + fn test_cli_subcommands() { + let cli = Cli::try_parse_from(["apc", "init"]); + assert!(cli.is_ok()); + + let cli = Cli::try_parse_from(["apc", "run", "--mode", "human"]); + assert!(cli.is_ok()); + + let cli = Cli::try_parse_from(["apc", "detect"]); + assert!(cli.is_ok()); + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..57b1c23 --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,653 @@ +//! Configuration handling for agent-precommit. +//! +//! This module provides configuration loading and validation, +//! supporting both `agent-precommit.toml` files and sensible defaults. + +use crate::core::error::{Error, Result}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +/// Default configuration file name. +pub const CONFIG_FILE_NAME: &str = "agent-precommit.toml"; + +/// Main configuration structure. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct Config { + /// Detection settings. + pub detection: DetectionConfig, + /// Integration with other tools. + pub integration: IntegrationConfig, + /// Human mode settings. + pub human: ModeConfig, + /// Agent mode settings. + pub agent: AgentModeConfig, + /// Check definitions. + #[serde(default)] + pub checks: HashMap, +} + +impl Default for Config { + fn default() -> Self { + Self { + detection: DetectionConfig::default(), + integration: IntegrationConfig::default(), + human: ModeConfig::default_human(), + agent: AgentModeConfig::default(), + checks: default_checks(), + } + } +} + +impl Config { + /// Loads configuration from the default location. + pub fn load() -> Result { + let path = Self::find_config_file()?; + Self::load_from(&path) + } + + /// Loads configuration or returns defaults if not found. + pub fn load_or_default() -> Result { + match Self::find_config_file() { + Ok(path) => Self::load_from(&path), + Err(Error::ConfigNotFound { .. }) => Ok(Self::default()), + Err(e) => Err(e), + } + } + + /// Loads configuration from a specific path. + pub fn load_from(path: &Path) -> Result { + let content = std::fs::read_to_string(path).map_err(|e| Error::io("read config", e))?; + + let config: Self = toml::from_str(&content) + .map_err(|e| Error::config_parse_with_source("Failed to parse TOML", e))?; + + config.validate()?; + + Ok(config) + } + + /// Finds the configuration file by searching up the directory tree. + pub fn find_config_file() -> Result { + let cwd = std::env::current_dir().map_err(|e| Error::io("get current dir", e))?; + + let mut current = cwd.as_path(); + loop { + let config_path = current.join(CONFIG_FILE_NAME); + if config_path.exists() { + return Ok(config_path); + } + + match current.parent() { + Some(parent) => current = parent, + None => break, + } + } + + Err(Error::ConfigNotFound { + path: cwd.join(CONFIG_FILE_NAME), + }) + } + + /// Validates the configuration. + pub fn validate(&self) -> Result<()> { + // Validate timeouts are parseable + if humantime::parse_duration(&self.human.timeout).is_err() { + return Err(Error::ConfigInvalid { + field: "human.timeout".to_string(), + message: format!("Invalid duration: {}", self.human.timeout), + }); + } + + if humantime::parse_duration(&self.agent.timeout).is_err() { + return Err(Error::ConfigInvalid { + field: "agent.timeout".to_string(), + message: format!("Invalid duration: {}", self.agent.timeout), + }); + } + + Ok(()) + } + + /// Generates default configuration as a string. + #[must_use] + pub fn default_toml() -> String { + let config = Self::default(); + toml::to_string_pretty(&config).unwrap_or_default() + } + + /// Generates configuration for a specific preset. + #[must_use] + pub fn for_preset(preset: &str) -> Self { + let mut config = Self::default(); + + match preset { + "python" => { + config.agent.checks = vec![ + "pre-commit-all".to_string(), + "no-merge-conflicts".to_string(), + "test-unit".to_string(), + "test-integration".to_string(), + "security-scan".to_string(), + "build-verify".to_string(), + ]; + config.checks.extend(python_checks()); + } + "node" | "nodejs" | "typescript" => { + config.agent.checks = vec![ + "pre-commit-all".to_string(), + "no-merge-conflicts".to_string(), + "lint".to_string(), + "typecheck".to_string(), + "test-unit".to_string(), + "build-verify".to_string(), + ]; + config.checks.extend(node_checks()); + } + "rust" => { + config.agent.checks = vec![ + "no-merge-conflicts".to_string(), + "fmt-check".to_string(), + "clippy".to_string(), + "test-unit".to_string(), + "build-verify".to_string(), + ]; + config.checks.extend(rust_checks()); + } + "go" => { + config.agent.checks = vec![ + "no-merge-conflicts".to_string(), + "fmt-check".to_string(), + "lint".to_string(), + "test-unit".to_string(), + "build-verify".to_string(), + ]; + config.checks.extend(go_checks()); + } + _ => {} + } + + config + } +} + +/// Detection configuration. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(default)] +pub struct DetectionConfig { + /// Force a specific mode (overrides auto-detection). + pub mode: Option, + /// Additional environment variables that indicate an agent. + pub agent_env_vars: Vec, +} + +/// Integration configuration. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct IntegrationConfig { + /// Enable pre-commit framework integration. + pub pre_commit: bool, + /// Path to pre-commit config file. + pub pre_commit_path: String, +} + +impl Default for IntegrationConfig { + fn default() -> Self { + Self { + pre_commit: false, + pre_commit_path: ".pre-commit-config.yaml".to_string(), + } + } +} + +/// Mode-specific configuration. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct ModeConfig { + /// Checks to run in this mode. + pub checks: Vec, + /// Timeout for all checks. + pub timeout: String, + /// Whether to stop on first failure. + pub fail_fast: bool, +} + +impl ModeConfig { + fn default_human() -> Self { + Self { + checks: vec!["pre-commit".to_string()], + timeout: "30s".to_string(), + fail_fast: true, + } + } +} + +impl Default for ModeConfig { + fn default() -> Self { + Self::default_human() + } +} + +/// Agent mode configuration with parallel execution support. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct AgentModeConfig { + /// Checks to run in agent mode. + pub checks: Vec, + /// Timeout for all checks. + pub timeout: String, + /// Whether to stop on first failure. + pub fail_fast: bool, + /// Groups of checks that can run in parallel. + pub parallel_groups: Vec>, +} + +impl Default for AgentModeConfig { + fn default() -> Self { + Self { + checks: vec![ + "pre-commit-all".to_string(), + "no-merge-conflicts".to_string(), + "test-unit".to_string(), + ], + timeout: "15m".to_string(), + fail_fast: false, + parallel_groups: Vec::new(), + } + } +} + +/// Configuration for a single check. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct CheckConfig { + /// Command to run. + pub run: String, + /// Human-readable description. + pub description: String, + /// Condition for enabling the check. + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled_if: Option, + /// Environment variables to set. + #[serde(default)] + pub env: HashMap, +} + +impl Default for CheckConfig { + fn default() -> Self { + Self { + run: String::new(), + description: String::new(), + enabled_if: None, + env: HashMap::new(), + } + } +} + +impl CheckConfig { + /// Creates a check config from a simple command. + #[must_use] + pub fn from_command(cmd: String) -> Self { + Self { + run: cmd.clone(), + description: cmd, + enabled_if: None, + env: HashMap::new(), + } + } +} + +/// Condition for enabling a check. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(default)] +pub struct EnabledCondition { + /// Check if a file exists. + #[serde(skip_serializing_if = "Option::is_none")] + pub file_exists: Option, + /// Check if a directory exists. + #[serde(skip_serializing_if = "Option::is_none")] + pub dir_exists: Option, + /// Check if a command exists in PATH. + #[serde(skip_serializing_if = "Option::is_none")] + pub command_exists: Option, +} + +/// Default checks for all configurations. +fn default_checks() -> HashMap { + let mut checks = HashMap::new(); + + checks.insert( + "pre-commit".to_string(), + CheckConfig { + run: "pre-commit run".to_string(), + description: "Run pre-commit on staged files".to_string(), + enabled_if: Some(EnabledCondition { + file_exists: Some(".pre-commit-config.yaml".to_string()), + ..Default::default() + }), + env: HashMap::new(), + }, + ); + + checks.insert( + "pre-commit-all".to_string(), + CheckConfig { + run: "pre-commit run --all-files".to_string(), + description: "Run pre-commit on all files".to_string(), + enabled_if: Some(EnabledCondition { + file_exists: Some(".pre-commit-config.yaml".to_string()), + ..Default::default() + }), + env: HashMap::new(), + }, + ); + + checks.insert( + "no-merge-conflicts".to_string(), + CheckConfig { + run: r#" +git fetch origin main --quiet 2>/dev/null || git fetch origin master --quiet 2>/dev/null || true +MAIN_BRANCH=$(git rev-parse --verify origin/main 2>/dev/null && echo "main" || echo "master") +BASE=$(git merge-base HEAD origin/$MAIN_BRANCH 2>/dev/null || echo "") +if [ -n "$BASE" ]; then + if git merge-tree $BASE HEAD origin/$MAIN_BRANCH 2>/dev/null | grep -q "^<<<<<<<"; then + echo "❌ Would conflict with $MAIN_BRANCH" + exit 1 + fi +fi +echo "✓ No conflicts with $MAIN_BRANCH" +"# + .trim() + .to_string(), + description: "Ensure no merge conflicts with main/master".to_string(), + enabled_if: None, + env: HashMap::new(), + }, + ); + + checks +} + +/// Python-specific checks. +fn python_checks() -> HashMap { + let mut checks = HashMap::new(); + + checks.insert( + "test-unit".to_string(), + CheckConfig { + run: "pytest -x -q".to_string(), + description: "Run unit tests".to_string(), + enabled_if: Some(EnabledCondition { + file_exists: Some("pyproject.toml".to_string()), + ..Default::default() + }), + env: HashMap::new(), + }, + ); + + checks.insert( + "test-integration".to_string(), + CheckConfig { + run: "pytest tests/integration/ -v".to_string(), + description: "Run integration tests".to_string(), + enabled_if: Some(EnabledCondition { + dir_exists: Some("tests/integration".to_string()), + ..Default::default() + }), + env: HashMap::new(), + }, + ); + + checks.insert( + "security-scan".to_string(), + CheckConfig { + run: "gitleaks detect --source . --no-git".to_string(), + description: "Scan for secrets".to_string(), + enabled_if: Some(EnabledCondition { + command_exists: Some("gitleaks".to_string()), + ..Default::default() + }), + env: HashMap::new(), + }, + ); + + checks.insert( + "build-verify".to_string(), + CheckConfig { + run: "python -m build --no-isolation".to_string(), + description: "Verify package builds".to_string(), + enabled_if: Some(EnabledCondition { + file_exists: Some("pyproject.toml".to_string()), + ..Default::default() + }), + env: HashMap::new(), + }, + ); + + checks +} + +/// Node.js/TypeScript checks. +fn node_checks() -> HashMap { + let mut checks = HashMap::new(); + + checks.insert( + "lint".to_string(), + CheckConfig { + run: "npm run lint".to_string(), + description: "Run ESLint".to_string(), + enabled_if: Some(EnabledCondition { + file_exists: Some("package.json".to_string()), + ..Default::default() + }), + env: HashMap::new(), + }, + ); + + checks.insert( + "typecheck".to_string(), + CheckConfig { + run: "npm run typecheck || npx tsc --noEmit".to_string(), + description: "Run TypeScript type checking".to_string(), + enabled_if: Some(EnabledCondition { + file_exists: Some("tsconfig.json".to_string()), + ..Default::default() + }), + env: HashMap::new(), + }, + ); + + checks.insert( + "test-unit".to_string(), + CheckConfig { + run: "npm test".to_string(), + description: "Run unit tests".to_string(), + enabled_if: Some(EnabledCondition { + file_exists: Some("package.json".to_string()), + ..Default::default() + }), + env: HashMap::new(), + }, + ); + + checks.insert( + "build-verify".to_string(), + CheckConfig { + run: "npm run build".to_string(), + description: "Verify build works".to_string(), + enabled_if: Some(EnabledCondition { + file_exists: Some("package.json".to_string()), + ..Default::default() + }), + env: HashMap::new(), + }, + ); + + checks +} + +/// Rust checks. +fn rust_checks() -> HashMap { + let mut checks = HashMap::new(); + + checks.insert( + "fmt-check".to_string(), + CheckConfig { + run: "cargo fmt --all -- --check".to_string(), + description: "Check code formatting".to_string(), + enabled_if: Some(EnabledCondition { + file_exists: Some("Cargo.toml".to_string()), + ..Default::default() + }), + env: HashMap::new(), + }, + ); + + checks.insert( + "clippy".to_string(), + CheckConfig { + run: "cargo clippy --all-targets --all-features -- -D warnings".to_string(), + description: "Run Clippy lints".to_string(), + enabled_if: Some(EnabledCondition { + file_exists: Some("Cargo.toml".to_string()), + ..Default::default() + }), + env: HashMap::new(), + }, + ); + + checks.insert( + "test-unit".to_string(), + CheckConfig { + run: "cargo test".to_string(), + description: "Run unit tests".to_string(), + enabled_if: Some(EnabledCondition { + file_exists: Some("Cargo.toml".to_string()), + ..Default::default() + }), + env: HashMap::new(), + }, + ); + + checks.insert( + "build-verify".to_string(), + CheckConfig { + run: "cargo build --release".to_string(), + description: "Verify release build".to_string(), + enabled_if: Some(EnabledCondition { + file_exists: Some("Cargo.toml".to_string()), + ..Default::default() + }), + env: HashMap::new(), + }, + ); + + checks +} + +/// Go checks. +fn go_checks() -> HashMap { + let mut checks = HashMap::new(); + + checks.insert( + "fmt-check".to_string(), + CheckConfig { + run: "test -z \"$(gofmt -l .)\"".to_string(), + description: "Check code formatting".to_string(), + enabled_if: Some(EnabledCondition { + file_exists: Some("go.mod".to_string()), + ..Default::default() + }), + env: HashMap::new(), + }, + ); + + checks.insert( + "lint".to_string(), + CheckConfig { + run: "golangci-lint run".to_string(), + description: "Run golangci-lint".to_string(), + enabled_if: Some(EnabledCondition { + command_exists: Some("golangci-lint".to_string()), + ..Default::default() + }), + env: HashMap::new(), + }, + ); + + checks.insert( + "test-unit".to_string(), + CheckConfig { + run: "go test ./...".to_string(), + description: "Run unit tests".to_string(), + enabled_if: Some(EnabledCondition { + file_exists: Some("go.mod".to_string()), + ..Default::default() + }), + env: HashMap::new(), + }, + ); + + checks.insert( + "build-verify".to_string(), + CheckConfig { + run: "go build ./...".to_string(), + description: "Verify build works".to_string(), + enabled_if: Some(EnabledCondition { + file_exists: Some("go.mod".to_string()), + ..Default::default() + }), + env: HashMap::new(), + }, + ); + + checks +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_default_config() { + let config = Config::default(); + assert!(!config.human.checks.is_empty()); + assert!(!config.agent.checks.is_empty()); + } + + #[test] + fn test_config_validation() { + let config = Config::default(); + assert!(config.validate().is_ok()); + } + + #[test] + fn test_invalid_timeout() { + let mut config = Config::default(); + config.human.timeout = "invalid".to_string(); + assert!(config.validate().is_err()); + } + + #[test] + fn test_preset_python() { + let config = Config::for_preset("python"); + assert!(config.checks.contains_key("test-unit")); + assert!(config.checks.contains_key("build-verify")); + } + + #[test] + fn test_preset_rust() { + let config = Config::for_preset("rust"); + assert!(config.checks.contains_key("clippy")); + assert!(config.checks.contains_key("fmt-check")); + } + + #[test] + fn test_default_toml_generation() { + let toml = Config::default_toml(); + assert!(!toml.is_empty()); + assert!(toml.contains("[human]")); + assert!(toml.contains("[agent]")); + } +} diff --git a/src/core/detector.rs b/src/core/detector.rs new file mode 100644 index 0000000..8dcbb91 --- /dev/null +++ b/src/core/detector.rs @@ -0,0 +1,333 @@ +//! Mode detection for distinguishing human vs agent commits. +//! +//! The detector analyzes the environment to determine whether a commit +//! is being made by a human developer or an AI coding agent. + +use crate::config::Config; +use std::env; +use std::io::IsTerminal; + +/// The detected commit mode. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub enum Mode { + /// Human developer - fast checks, staged files only. + #[default] + Human, + /// AI coding agent - thorough checks, full codebase. + Agent, + /// CI environment - same as agent, possibly with extra reporting. + Ci, +} + +impl Mode { + /// Returns a human-readable name for the mode. + #[must_use] + pub const fn name(&self) -> &'static str { + match self { + Self::Human => "human", + Self::Agent => "agent", + Self::Ci => "ci", + } + } + + /// Returns whether this mode requires thorough checks. + #[must_use] + pub const fn is_thorough(&self) -> bool { + matches!(self, Self::Agent | Self::Ci) + } +} + +impl std::fmt::Display for Mode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name()) + } +} + +impl std::str::FromStr for Mode { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + match s.to_lowercase().as_str() { + "human" => Ok(Self::Human), + "agent" => Ok(Self::Agent), + "ci" => Ok(Self::Ci), + _ => Err(format!("Invalid mode: {s}. Expected: human, agent, or ci")), + } + } +} + +/// Reason for mode detection - useful for debugging. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DetectionReason { + /// Mode set via APC_MODE environment variable. + ExplicitApcMode(String), + /// Mode set via AGENT_MODE environment variable. + ExplicitAgentMode, + /// Known agent environment variable detected. + KnownAgentEnvVar(String), + /// Custom agent environment variable from config. + CustomAgentEnvVar(String), + /// CI environment detected. + CiEnvironment(String), + /// No TTY detected (non-interactive). + NoTty, + /// Default fallback to human mode. + Default, +} + +impl std::fmt::Display for DetectionReason { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::ExplicitApcMode(value) => write!(f, "APC_MODE={value}"), + Self::ExplicitAgentMode => write!(f, "AGENT_MODE=1"), + Self::KnownAgentEnvVar(var) => write!(f, "Known agent env var: {var}"), + Self::CustomAgentEnvVar(var) => write!(f, "Custom agent env var: {var}"), + Self::CiEnvironment(var) => write!(f, "CI environment: {var}"), + Self::NoTty => write!(f, "No TTY detected (non-interactive)"), + Self::Default => write!(f, "Default (no agent indicators)"), + } + } +} + +/// Result of mode detection. +#[derive(Debug, Clone)] +pub struct Detection { + /// The detected mode. + pub mode: Mode, + /// Reason for the detection. + pub reason: DetectionReason, +} + +/// Detector for determining commit mode. +#[derive(Debug)] +pub struct Detector<'a> { + config: &'a Config, +} + +/// Known environment variables that indicate an AI agent. +const KNOWN_AGENT_ENV_VARS: &[&str] = &[ + // Claude Code + "CLAUDE_CODE", + "ANTHROPIC_PROJECT_ID", + // Cursor + "CURSOR_SESSION", + "CURSOR_TRACE_ID", + // Aider + "AIDER_MODEL", + "AIDER_CHAT_HISTORY_FILE", + // OpenAI Codex / ChatGPT + "CODEX_SESSION", + "OPENAI_API_KEY_FOR_AGENT", + // Devin + "DEVIN_SESSION", + "DEVIN_API_KEY", + // Cline + "CLINE_SESSION", + "CLINE_API_KEY", + // Continue.dev + "CONTINUE_SESSION", + "CONTINUE_GLOBAL_DIR", + // GitHub Copilot Workspace + "GITHUB_COPILOT_WORKSPACE", + // Amazon CodeWhisperer / Q + "AWS_CODEWHISPERER_SESSION", + "AMAZON_Q_SESSION", + // Sourcegraph Cody + "CODY_SESSION", + "SRC_ACCESS_TOKEN", + // Tabnine + "TABNINE_SESSION", + // Replit Agent + "REPLIT_AGENT", + "REPL_ID", + // Generic + "AI_AGENT", + "CODING_AGENT", +]; + +/// Known environment variables that indicate a CI environment. +const KNOWN_CI_ENV_VARS: &[&str] = &[ + "CI", + "GITHUB_ACTIONS", + "GITLAB_CI", + "CIRCLECI", + "TRAVIS", + "JENKINS_URL", + "BUILDKITE", + "BITBUCKET_PIPELINE", + "AZURE_PIPELINES", + "TEAMCITY_VERSION", + "DRONE", + "WOODPECKER", + "SEMAPHORE", + "APPVEYOR", + "CODEBUILD_BUILD_ID", + "TF_BUILD", + "NETLIFY", + "VERCEL", + "RENDER", + "RAILWAY_ENVIRONMENT", + "FLY_APP_NAME", +]; + +impl<'a> Detector<'a> { + /// Creates a new detector with the given configuration. + #[must_use] + pub const fn new(config: &'a Config) -> Self { + Self { config } + } + + /// Detects the commit mode based on environment. + #[must_use] + pub fn detect(&self) -> Detection { + // Priority 1: Explicit APC_MODE override + if let Some(detection) = self.check_apc_mode() { + return detection; + } + + // Priority 2: AGENT_MODE=1 flag + if let Some(detection) = self.check_agent_mode_flag() { + return detection; + } + + // Priority 3: Known agent environment variables + if let Some(detection) = self.check_known_agent_env_vars() { + return detection; + } + + // Priority 4: Custom agent environment variables from config + if let Some(detection) = self.check_custom_agent_env_vars() { + return detection; + } + + // Priority 5: CI environment detection + if let Some(detection) = self.check_ci_environment() { + return detection; + } + + // Priority 6: TTY detection (fallback heuristic) + if let Some(detection) = self.check_tty() { + return detection; + } + + // Default: Human mode + Detection { + mode: Mode::Human, + reason: DetectionReason::Default, + } + } + + /// Checks for explicit APC_MODE environment variable. + fn check_apc_mode(&self) -> Option { + env::var("APC_MODE").ok().map(|value| { + let mode = value.parse().unwrap_or(Mode::Human); + Detection { + mode, + reason: DetectionReason::ExplicitApcMode(value), + } + }) + } + + /// Checks for AGENT_MODE=1 flag. + fn check_agent_mode_flag(&self) -> Option { + env::var("AGENT_MODE").ok().and_then(|value| { + if value == "1" || value.eq_ignore_ascii_case("true") { + Some(Detection { + mode: Mode::Agent, + reason: DetectionReason::ExplicitAgentMode, + }) + } else { + None + } + }) + } + + /// Checks for known agent environment variables. + fn check_known_agent_env_vars(&self) -> Option { + for var in KNOWN_AGENT_ENV_VARS { + if env::var(var).is_ok() { + return Some(Detection { + mode: Mode::Agent, + reason: DetectionReason::KnownAgentEnvVar((*var).to_string()), + }); + } + } + None + } + + /// Checks for custom agent environment variables from config. + fn check_custom_agent_env_vars(&self) -> Option { + for var in &self.config.detection.agent_env_vars { + if env::var(var).is_ok() { + return Some(Detection { + mode: Mode::Agent, + reason: DetectionReason::CustomAgentEnvVar(var.clone()), + }); + } + } + None + } + + /// Checks for CI environment variables. + fn check_ci_environment(&self) -> Option { + for var in KNOWN_CI_ENV_VARS { + if env::var(var).is_ok() { + return Some(Detection { + mode: Mode::Ci, + reason: DetectionReason::CiEnvironment((*var).to_string()), + }); + } + } + None + } + + /// Checks for TTY presence (non-interactive = likely agent). + fn check_tty(&self) -> Option { + let stdin_is_tty = std::io::stdin().is_terminal(); + let stdout_is_tty = std::io::stdout().is_terminal(); + + // Only trigger if BOTH stdin and stdout are not TTY + // This avoids false positives from piped commands + if !stdin_is_tty && !stdout_is_tty { + return Some(Detection { + mode: Mode::Agent, + reason: DetectionReason::NoTty, + }); + } + + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mode_display() { + assert_eq!(Mode::Human.to_string(), "human"); + assert_eq!(Mode::Agent.to_string(), "agent"); + assert_eq!(Mode::Ci.to_string(), "ci"); + } + + #[test] + fn test_mode_parse() { + assert_eq!("human".parse::().ok(), Some(Mode::Human)); + assert_eq!("AGENT".parse::().ok(), Some(Mode::Agent)); + assert_eq!("CI".parse::().ok(), Some(Mode::Ci)); + assert!("invalid".parse::().is_err()); + } + + #[test] + fn test_mode_is_thorough() { + assert!(!Mode::Human.is_thorough()); + assert!(Mode::Agent.is_thorough()); + assert!(Mode::Ci.is_thorough()); + } + + #[test] + fn test_detection_reason_display() { + let reason = DetectionReason::ExplicitAgentMode; + assert_eq!(reason.to_string(), "AGENT_MODE=1"); + } +} diff --git a/src/core/error.rs b/src/core/error.rs new file mode 100644 index 0000000..7982ecc --- /dev/null +++ b/src/core/error.rs @@ -0,0 +1,272 @@ +//! Error types for agent-precommit. +//! +//! This module defines all errors that can occur during operation. + +use std::path::PathBuf; + +/// Result type alias using our Error type. +pub type Result = std::result::Result; + +/// All possible errors in agent-precommit. +#[derive(Debug, thiserror::Error)] +pub enum Error { + // ========================================================================= + // Configuration errors + // ========================================================================= + /// Configuration file not found. + #[error("Configuration file not found: {path}")] + ConfigNotFound { + /// Path where config was expected. + path: PathBuf, + }, + + /// Failed to parse configuration file. + #[error("Failed to parse configuration: {message}")] + ConfigParse { + /// Description of the parse error. + message: String, + /// Optional source error. + #[source] + source: Option>, + }, + + /// Invalid configuration value. + #[error("Invalid configuration: {field} - {message}")] + ConfigInvalid { + /// Field name that is invalid. + field: String, + /// Description of why it's invalid. + message: String, + }, + + // ========================================================================= + // Git errors + // ========================================================================= + /// Not in a Git repository. + #[error("Not in a Git repository")] + NotGitRepo, + + /// Git operation failed. + #[error("Git operation failed: {operation} - {message}")] + GitOperation { + /// Name of the operation that failed. + operation: String, + /// Error message. + message: String, + }, + + /// Failed to detect Git hooks directory. + #[error("Failed to detect Git hooks directory")] + GitHooksDir, + + // ========================================================================= + // Check execution errors + // ========================================================================= + /// Check not found. + #[error("Check not found: {name}")] + CheckNotFound { + /// Name of the check that wasn't found. + name: String, + }, + + /// Check execution failed. + #[error("Check '{name}' failed: {message}")] + CheckFailed { + /// Name of the check that failed. + name: String, + /// Error message or output. + message: String, + /// Exit code if available. + exit_code: Option, + }, + + /// Check timed out. + #[error("Check '{name}' timed out after {timeout}")] + CheckTimeout { + /// Name of the check that timed out. + name: String, + /// Timeout duration as string. + timeout: String, + }, + + /// Command not found. + #[error("Command not found: {command}")] + CommandNotFound { + /// The command that wasn't found. + command: String, + }, + + // ========================================================================= + // Hook errors + // ========================================================================= + /// Failed to install hook. + #[error("Failed to install Git hook: {message}")] + HookInstall { + /// Error message. + message: String, + }, + + /// Hook already exists and wasn't created by us. + #[error("Git hook already exists at {path}. Use --force to overwrite.")] + HookExists { + /// Path to existing hook. + path: PathBuf, + }, + + // ========================================================================= + // I/O errors + // ========================================================================= + /// File I/O error. + #[error("I/O error: {message}")] + Io { + /// Description of what failed. + message: String, + /// Source error. + #[source] + source: std::io::Error, + }, + + // ========================================================================= + // Pre-commit integration errors + // ========================================================================= + /// Pre-commit framework not found. + #[error("Pre-commit framework not found. Install with: pip install pre-commit")] + PreCommitNotFound, + + /// Pre-commit config not found. + #[error("Pre-commit config not found: {path}")] + PreCommitConfigNotFound { + /// Path where config was expected. + path: PathBuf, + }, + + // ========================================================================= + // Internal errors + // ========================================================================= + /// Internal error (should never happen). + #[error("Internal error: {message}")] + Internal { + /// Error message. + message: String, + }, +} + +impl Error { + /// Creates a new configuration parse error. + pub fn config_parse(message: impl Into) -> Self { + Self::ConfigParse { + message: message.into(), + source: None, + } + } + + /// Creates a new configuration parse error with source. + pub fn config_parse_with_source( + message: impl Into, + source: impl std::error::Error + Send + Sync + 'static, + ) -> Self { + Self::ConfigParse { + message: message.into(), + source: Some(Box::new(source)), + } + } + + /// Creates a new I/O error with context. + pub fn io(message: impl Into, source: std::io::Error) -> Self { + Self::Io { + message: message.into(), + source, + } + } + + /// Creates a new Git operation error. + pub fn git(operation: impl Into, message: impl Into) -> Self { + Self::GitOperation { + operation: operation.into(), + message: message.into(), + } + } + + /// Creates a new check failed error. + pub fn check_failed( + name: impl Into, + message: impl Into, + exit_code: Option, + ) -> Self { + Self::CheckFailed { + name: name.into(), + message: message.into(), + exit_code, + } + } + + /// Returns true if this is a user-correctable error. + pub const fn is_user_error(&self) -> bool { + matches!( + self, + Self::ConfigNotFound { .. } + | Self::ConfigInvalid { .. } + | Self::NotGitRepo + | Self::HookExists { .. } + | Self::PreCommitNotFound + | Self::PreCommitConfigNotFound { .. } + ) + } + + /// Returns an exit code appropriate for this error. + #[must_use] + pub fn exit_code(&self) -> i32 { + match self { + Self::CheckFailed { exit_code, .. } => exit_code.unwrap_or(1), + Self::CheckTimeout { .. } => 124, // Standard timeout exit code + Self::ConfigNotFound { .. } + | Self::ConfigParse { .. } + | Self::ConfigInvalid { .. } => 78, // EX_CONFIG + Self::NotGitRepo | Self::GitOperation { .. } | Self::GitHooksDir => 65, // EX_DATAERR + _ => 1, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_error_display() { + let err = Error::CheckNotFound { + name: "test".to_string(), + }; + assert_eq!(err.to_string(), "Check not found: test"); + } + + #[test] + fn test_exit_codes() { + assert_eq!( + Error::CheckTimeout { + name: "test".into(), + timeout: "30s".into() + } + .exit_code(), + 124 + ); + + assert_eq!( + Error::ConfigNotFound { + path: PathBuf::from("/test") + } + .exit_code(), + 78 + ); + } + + #[test] + fn test_is_user_error() { + assert!(Error::NotGitRepo.is_user_error()); + assert!(Error::PreCommitNotFound.is_user_error()); + assert!(!Error::Internal { + message: "test".into() + } + .is_user_error()); + } +} diff --git a/src/core/executor.rs b/src/core/executor.rs new file mode 100644 index 0000000..6d549eb --- /dev/null +++ b/src/core/executor.rs @@ -0,0 +1,317 @@ +//! Command execution for running checks. +//! +//! This module provides utilities for executing shell commands +//! with timeout support, output capture, and error handling. + +use crate::core::error::{Error, Result}; +use std::path::Path; +use std::process::Stdio; +use std::time::Duration; +use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::process::Command; +use tokio::time::timeout; + +/// Output from a command execution. +#[derive(Debug, Clone)] +pub struct CommandOutput { + /// Exit code of the command. + pub exit_code: i32, + /// Standard output. + pub stdout: String, + /// Standard error. + pub stderr: String, + /// Whether the command was killed due to timeout. + pub timed_out: bool, + /// Duration the command took to run. + pub duration: Duration, +} + +impl CommandOutput { + /// Returns true if the command succeeded (exit code 0). + #[must_use] + pub const fn success(&self) -> bool { + self.exit_code == 0 && !self.timed_out + } + + /// Returns combined stdout and stderr output. + #[must_use] + pub fn combined_output(&self) -> String { + if self.stderr.is_empty() { + self.stdout.clone() + } else if self.stdout.is_empty() { + self.stderr.clone() + } else { + format!("{}\n{}", self.stdout, self.stderr) + } + } +} + +/// Options for command execution. +#[derive(Debug, Clone)] +pub struct ExecuteOptions { + /// Working directory for the command. + pub cwd: Option, + /// Timeout for the command. + pub timeout: Option, + /// Environment variables to set. + pub env: Vec<(String, String)>, + /// Whether to capture output (vs streaming to console). + pub capture_output: bool, + /// Shell to use (default: sh on Unix, cmd on Windows). + pub shell: Option, +} + +impl Default for ExecuteOptions { + fn default() -> Self { + Self { + cwd: None, + timeout: Some(Duration::from_secs(300)), // 5 minutes default + env: Vec::new(), + capture_output: true, + shell: None, + } + } +} + +impl ExecuteOptions { + /// Sets the working directory. + #[must_use] + pub fn cwd(mut self, path: impl AsRef) -> Self { + self.cwd = Some(path.as_ref().to_path_buf()); + self + } + + /// Sets the timeout. + #[must_use] + pub const fn timeout(mut self, duration: Duration) -> Self { + self.timeout = Some(duration); + self + } + + /// Sets an environment variable. + #[must_use] + pub fn env(mut self, key: impl Into, value: impl Into) -> Self { + self.env.push((key.into(), value.into())); + self + } + + /// Sets whether to capture output. + #[must_use] + pub const fn capture_output(mut self, capture: bool) -> Self { + self.capture_output = capture; + self + } +} + +/// Executor for running shell commands. +#[derive(Debug, Default)] +pub struct Executor; + +impl Executor { + /// Creates a new executor. + #[must_use] + pub const fn new() -> Self { + Self + } + + /// Executes a shell command. + pub async fn execute(&self, command: &str, options: ExecuteOptions) -> Result { + let start = std::time::Instant::now(); + + // Determine shell + let (shell, shell_arg) = if cfg!(windows) { + ( + options.shell.as_deref().unwrap_or("cmd"), + "/C", + ) + } else { + ( + options.shell.as_deref().unwrap_or("sh"), + "-c", + ) + }; + + // Build command + let mut cmd = Command::new(shell); + cmd.arg(shell_arg).arg(command); + + // Set working directory + if let Some(ref cwd) = options.cwd { + cmd.current_dir(cwd); + } + + // Set environment variables + for (key, value) in &options.env { + cmd.env(key, value); + } + + // Configure output handling + cmd.stdin(Stdio::null()); + + if options.capture_output { + cmd.stdout(Stdio::piped()); + cmd.stderr(Stdio::piped()); + } else { + cmd.stdout(Stdio::inherit()); + cmd.stderr(Stdio::inherit()); + } + + // Spawn the process + let mut child = cmd.spawn().map_err(|e| Error::io("spawn command", e))?; + + // Handle timeout + let result = if let Some(timeout_duration) = options.timeout { + match timeout(timeout_duration, async { + self.wait_for_output(&mut child, options.capture_output).await + }) + .await + { + Ok(result) => result, + Err(_) => { + // Kill the process on timeout - ignore result since we're returning anyway + drop(child.kill().await); + return Ok(CommandOutput { + exit_code: 124, + stdout: String::new(), + stderr: "Command timed out".to_string(), + timed_out: true, + duration: start.elapsed(), + }); + } + } + } else { + self.wait_for_output(&mut child, options.capture_output) + .await + }; + + let (exit_code, stdout, stderr) = result?; + + Ok(CommandOutput { + exit_code, + stdout, + stderr, + timed_out: false, + duration: start.elapsed(), + }) + } + + /// Waits for the command to complete and captures output. + async fn wait_for_output( + &self, + child: &mut tokio::process::Child, + capture: bool, + ) -> Result<(i32, String, String)> { + if capture { + let stdout = child.stdout.take(); + let stderr = child.stderr.take(); + + let stdout_handle = tokio::spawn(async move { + let mut output = String::new(); + if let Some(stdout) = stdout { + let mut reader = BufReader::new(stdout).lines(); + while let Ok(Some(line)) = reader.next_line().await { + output.push_str(&line); + output.push('\n'); + } + } + output + }); + + let stderr_handle = tokio::spawn(async move { + let mut output = String::new(); + if let Some(stderr) = stderr { + let mut reader = BufReader::new(stderr).lines(); + while let Ok(Some(line)) = reader.next_line().await { + output.push_str(&line); + output.push('\n'); + } + } + output + }); + + let status = child.wait().await.map_err(|e| Error::io("wait for command", e))?; + + let stdout = stdout_handle + .await + .map_err(|e| Error::Internal { + message: format!("stdout task failed: {e}"), + })?; + let stderr = stderr_handle + .await + .map_err(|e| Error::Internal { + message: format!("stderr task failed: {e}"), + })?; + + Ok((status.code().unwrap_or(1), stdout, stderr)) + } else { + let status = child.wait().await.map_err(|e| Error::io("wait for command", e))?; + Ok((status.code().unwrap_or(1), String::new(), String::new())) + } + } + + /// Checks if a command exists in PATH. + #[must_use] + pub fn command_exists(command: &str) -> bool { + which::which(command).is_ok() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_execute_simple_command() { + let executor = Executor::new(); + let result = executor + .execute("echo hello", ExecuteOptions::default()) + .await; + + assert!(result.is_ok()); + let output = result.expect("should succeed"); + assert!(output.success()); + assert!(output.stdout.contains("hello")); + } + + #[tokio::test] + async fn test_execute_failing_command() { + let executor = Executor::new(); + let result = executor + .execute("exit 1", ExecuteOptions::default()) + .await; + + assert!(result.is_ok()); + let output = result.expect("should complete"); + assert!(!output.success()); + assert_eq!(output.exit_code, 1); + } + + #[tokio::test] + async fn test_execute_timeout() { + let executor = Executor::new(); + let result = executor + .execute( + "sleep 10", + ExecuteOptions::default().timeout(Duration::from_millis(100)), + ) + .await; + + assert!(result.is_ok()); + let output = result.expect("should complete"); + assert!(output.timed_out); + assert_eq!(output.exit_code, 124); + } + + #[test] + fn test_command_exists() { + // 'sh' should exist on Unix, 'cmd' on Windows + if cfg!(unix) { + assert!(Executor::command_exists("sh")); + } else { + assert!(Executor::command_exists("cmd")); + } + + // This should not exist + assert!(!Executor::command_exists("definitely_not_a_real_command_12345")); + } +} diff --git a/src/core/git.rs b/src/core/git.rs new file mode 100644 index 0000000..bf6cdd8 --- /dev/null +++ b/src/core/git.rs @@ -0,0 +1,262 @@ +//! Git repository operations. +//! +//! This module provides utilities for interacting with Git repositories, +//! including finding the repository root, hooks directory, and staged files. + +use crate::core::error::{Error, Result}; +use std::path::{Path, PathBuf}; +use std::process::Command; + +/// Represents a Git repository. +#[derive(Debug, Clone)] +pub struct GitRepo { + /// Root directory of the repository (where .git is). + root: PathBuf, + /// Path to the .git directory (or file for worktrees). + git_dir: PathBuf, +} + +impl GitRepo { + /// Discovers the Git repository from the current directory. + pub fn discover() -> Result { + Self::discover_from(&std::env::current_dir().map_err(|e| Error::io("get current dir", e))?) + } + + /// Discovers the Git repository from a specific path. + pub fn discover_from(path: &Path) -> Result { + let output = Command::new("git") + .args(["rev-parse", "--show-toplevel", "--git-dir"]) + .current_dir(path) + .output() + .map_err(|e| Error::io("run git rev-parse", e))?; + + if !output.status.success() { + return Err(Error::NotGitRepo); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let mut lines = stdout.lines(); + + let root = lines + .next() + .map(PathBuf::from) + .ok_or(Error::NotGitRepo)?; + + let git_dir = lines + .next() + .map(|s| { + let p = PathBuf::from(s); + if p.is_absolute() { + p + } else { + root.join(p) + } + }) + .ok_or(Error::NotGitRepo)?; + + Ok(Self { root, git_dir }) + } + + /// Returns the root directory of the repository. + #[must_use] + pub fn root(&self) -> &Path { + &self.root + } + + /// Returns the .git directory path. + #[must_use] + pub fn git_dir(&self) -> &Path { + &self.git_dir + } + + /// Returns the hooks directory path. + #[must_use] + pub fn hooks_dir(&self) -> PathBuf { + // Check for custom hooks path first + if let Ok(output) = Command::new("git") + .args(["config", "--get", "core.hooksPath"]) + .current_dir(&self.root) + .output() + { + if output.status.success() { + let path = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if !path.is_empty() { + let hooks_path = PathBuf::from(&path); + if hooks_path.is_absolute() { + return hooks_path; + } + return self.root.join(hooks_path); + } + } + } + + // Default to .git/hooks + self.git_dir.join("hooks") + } + + /// Returns the path to a specific hook. + #[must_use] + pub fn hook_path(&self, hook_name: &str) -> PathBuf { + self.hooks_dir().join(hook_name) + } + + /// Returns the list of staged files. + pub fn staged_files(&self) -> Result> { + let output = Command::new("git") + .args(["diff", "--cached", "--name-only", "--diff-filter=ACMR"]) + .current_dir(&self.root) + .output() + .map_err(|e| Error::io("get staged files", e))?; + + if !output.status.success() { + return Err(Error::git("diff --cached", "Failed to get staged files")); + } + + let files = String::from_utf8_lossy(&output.stdout) + .lines() + .filter(|s| !s.is_empty()) + .map(|s| self.root.join(s)) + .collect(); + + Ok(files) + } + + /// Returns the current branch name. + pub fn current_branch(&self) -> Result { + let output = Command::new("git") + .args(["rev-parse", "--abbrev-ref", "HEAD"]) + .current_dir(&self.root) + .output() + .map_err(|e| Error::io("get current branch", e))?; + + if !output.status.success() { + return Err(Error::git("rev-parse", "Failed to get current branch")); + } + + Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) + } + + /// Returns the main branch name (main or master). + pub fn main_branch(&self) -> Result { + // Try 'main' first + let output = Command::new("git") + .args(["rev-parse", "--verify", "origin/main"]) + .current_dir(&self.root) + .output() + .map_err(|e| Error::io("verify main branch", e))?; + + if output.status.success() { + return Ok("main".to_string()); + } + + // Fall back to 'master' + let output = Command::new("git") + .args(["rev-parse", "--verify", "origin/master"]) + .current_dir(&self.root) + .output() + .map_err(|e| Error::io("verify master branch", e))?; + + if output.status.success() { + return Ok("master".to_string()); + } + + // Default to 'main' if neither exists + Ok("main".to_string()) + } + + /// Fetches updates from the remote for a specific branch. + pub fn fetch_branch(&self, branch: &str) -> Result<()> { + let output = Command::new("git") + .args(["fetch", "origin", branch, "--quiet"]) + .current_dir(&self.root) + .output() + .map_err(|e| Error::io("fetch branch", e))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(Error::git("fetch", stderr.trim().to_string())); + } + + Ok(()) + } + + /// Checks if the repository has uncommitted changes. + pub fn has_uncommitted_changes(&self) -> Result { + let output = Command::new("git") + .args(["status", "--porcelain"]) + .current_dir(&self.root) + .output() + .map_err(|e| Error::io("check uncommitted changes", e))?; + + if !output.status.success() { + return Err(Error::git("status", "Failed to check status")); + } + + Ok(!output.stdout.is_empty()) + } + + /// Checks if a file exists in the repository. + #[must_use] + pub fn file_exists(&self, relative_path: &str) -> bool { + self.root.join(relative_path).exists() + } + + /// Checks if a directory exists in the repository. + #[must_use] + pub fn dir_exists(&self, relative_path: &str) -> bool { + self.root.join(relative_path).is_dir() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + fn create_test_repo() -> (TempDir, GitRepo) { + let temp = TempDir::new().expect("create temp dir"); + let path = temp.path(); + + Command::new("git") + .args(["init"]) + .current_dir(path) + .output() + .expect("init repo"); + + Command::new("git") + .args(["config", "user.email", "test@test.com"]) + .current_dir(path) + .output() + .expect("set email"); + + Command::new("git") + .args(["config", "user.name", "Test"]) + .current_dir(path) + .output() + .expect("set name"); + + let repo = GitRepo::discover_from(path).expect("discover repo"); + (temp, repo) + } + + #[test] + fn test_discover_repo() { + let (_temp, repo) = create_test_repo(); + assert!(repo.root().exists()); + assert!(repo.git_dir().exists()); + } + + #[test] + fn test_hooks_dir() { + let (_temp, repo) = create_test_repo(); + let hooks_dir = repo.hooks_dir(); + assert!(hooks_dir.ends_with("hooks")); + } + + #[test] + fn test_not_git_repo() { + let temp = TempDir::new().expect("create temp dir"); + let result = GitRepo::discover_from(temp.path()); + assert!(matches!(result, Err(Error::NotGitRepo))); + } +} diff --git a/src/core/mod.rs b/src/core/mod.rs new file mode 100644 index 0000000..8cee063 --- /dev/null +++ b/src/core/mod.rs @@ -0,0 +1,13 @@ +//! Core functionality for agent-precommit. +//! +//! This module contains the main components: +//! - [`detector`]: Mode detection (human, agent, CI) +//! - [`runner`]: Check execution engine +//! - [`error`]: Error types and result handling +//! - [`git`]: Git repository operations + +pub mod detector; +pub mod error; +pub mod executor; +pub mod git; +pub mod runner; diff --git a/src/core/runner.rs b/src/core/runner.rs new file mode 100644 index 0000000..c95c64e --- /dev/null +++ b/src/core/runner.rs @@ -0,0 +1,494 @@ +//! Check runner for executing pre-commit checks. +//! +//! This module orchestrates the execution of checks based on the detected mode. + +// Allow this for Rust 2024 compatibility - the drop order change is harmless here +#![allow(tail_expr_drop_order)] + +use crate::config::{CheckConfig, Config}; +use crate::core::detector::Mode; +use crate::core::error::{Error, Result}; +use crate::core::executor::{CommandOutput, ExecuteOptions, Executor}; +use crate::core::git::GitRepo; +use console::style; +use indicatif::{ProgressBar, ProgressStyle}; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::Semaphore; + +/// Result of running a single check. +#[derive(Debug, Clone)] +pub struct CheckResult { + /// Name of the check. + pub name: String, + /// Whether the check passed. + pub passed: bool, + /// Output from the check. + pub output: CommandOutput, + /// Whether the check was skipped. + pub skipped: bool, + /// Reason for skipping (if skipped). + pub skip_reason: Option, +} + +impl CheckResult { + /// Creates a skipped check result. + fn skipped(name: String, reason: String) -> Self { + Self { + name, + passed: true, // Skipped checks don't fail + output: CommandOutput { + exit_code: 0, + stdout: String::new(), + stderr: String::new(), + timed_out: false, + duration: Duration::ZERO, + }, + skipped: true, + skip_reason: Some(reason), + } + } +} + +/// Result of running all checks. +#[derive(Debug)] +pub struct RunResult { + /// Mode that was used. + pub mode: Mode, + /// Individual check results. + pub checks: Vec, + /// Total duration. + pub duration: Duration, +} + +impl RunResult { + /// Returns true if all checks passed. + #[must_use] + pub fn success(&self) -> bool { + self.checks.iter().all(|c| c.passed) + } + + /// Returns the number of passed checks. + #[must_use] + pub fn passed_count(&self) -> usize { + self.checks.iter().filter(|c| c.passed && !c.skipped).count() + } + + /// Returns the number of failed checks. + #[must_use] + pub fn failed_count(&self) -> usize { + self.checks.iter().filter(|c| !c.passed).count() + } + + /// Returns the number of skipped checks. + #[must_use] + pub fn skipped_count(&self) -> usize { + self.checks.iter().filter(|c| c.skipped).count() + } + + /// Returns failed check results. + pub fn failed_checks(&self) -> impl Iterator { + self.checks.iter().filter(|c| !c.passed) + } +} + +/// Runner for executing checks. +#[derive(Debug)] +pub struct Runner { + config: Config, + repo: Option, +} + +impl Runner { + /// Creates a new runner with the given configuration. + #[must_use] + pub fn new(config: Config) -> Self { + Self { + config, + repo: GitRepo::discover().ok(), + } + } + + /// Creates a new runner with a specific repository. + #[must_use] + pub fn with_repo(config: Config, repo: GitRepo) -> Self { + Self { + config, + repo: Some(repo), + } + } + + /// Runs checks for the given mode. + pub async fn run(&self, mode: Mode) -> Result { + let start = std::time::Instant::now(); + + // Get checks for this mode + let check_names = self.get_checks_for_mode(mode); + + if check_names.is_empty() { + return Ok(RunResult { + mode, + checks: Vec::new(), + duration: start.elapsed(), + }); + } + + // Resolve check configurations + let checks = self.resolve_checks(&check_names)?; + + // Run checks based on mode settings + let results = if mode.is_thorough() { + self.run_parallel_groups(mode, &checks).await? + } else { + self.run_sequential(mode, &checks).await? + }; + + Ok(RunResult { + mode, + checks: results, + duration: start.elapsed(), + }) + } + + /// Runs a single check by name. + pub async fn run_single(&self, name: &str, mode: Mode) -> Result { + let check = self.config.checks.get(name).ok_or_else(|| Error::CheckNotFound { + name: name.to_string(), + })?; + + self.run_check(name, check, mode).await + } + + /// Gets the list of checks for a mode. + fn get_checks_for_mode(&self, mode: Mode) -> Vec { + match mode { + Mode::Human => self.config.human.checks.clone(), + Mode::Agent | Mode::Ci => self.config.agent.checks.clone(), + } + } + + /// Resolves check names to configurations. + fn resolve_checks(&self, names: &[String]) -> Result> { + let mut checks = Vec::with_capacity(names.len()); + + for name in names { + let check = self + .config + .checks + .get(name) + .cloned() + .unwrap_or_else(|| CheckConfig::from_command(name.clone())); + + checks.push((name.clone(), check)); + } + + Ok(checks) + } + + /// Runs checks sequentially (for human mode). + async fn run_sequential( + &self, + mode: Mode, + checks: &[(String, CheckConfig)], + ) -> Result> { + let mut results = Vec::with_capacity(checks.len()); + + for (name, check) in checks { + let result = self.run_check(name, check, mode).await?; + + let failed = !result.passed; + results.push(result); + + // Fail fast in human mode + if failed && self.config.human.fail_fast { + break; + } + } + + Ok(results) + } + + /// Runs checks in parallel groups (for agent mode). + async fn run_parallel_groups( + &self, + mode: Mode, + checks: &[(String, CheckConfig)], + ) -> Result> { + let check_map: HashMap<_, _> = checks.iter().cloned().collect(); + + // Get parallel groups or create default groups + let groups = if self.config.agent.parallel_groups.is_empty() { + // Default: run all checks in parallel + vec![checks.iter().map(|(n, _)| n.clone()).collect()] + } else { + self.config.agent.parallel_groups.clone() + }; + + let mut all_results = Vec::new(); + let semaphore = Arc::new(Semaphore::new(num_cpus::get())); + + for group in groups { + let group_checks: Vec<_> = group + .iter() + .filter_map(|name| check_map.get(name).map(|c| (name.clone(), c.clone()))) + .collect(); + + if group_checks.is_empty() { + continue; + } + + let mut handles = Vec::new(); + + for (name, check) in group_checks { + let sem = Arc::clone(&semaphore); + let config = self.config.clone(); + let repo = self.repo.clone(); + + handles.push(tokio::spawn(async move { + let _permit = sem.acquire().await; + run_check_async(&name, &check, mode, &config, repo.as_ref()).await + })); + } + + for handle in handles { + match handle.await { + Ok(result) => all_results.push(result?), + Err(e) => { + return Err(Error::Internal { + message: format!("Task join error: {e}"), + }); + } + } + } + + // Check for failures if not running all checks + if !self.config.agent.fail_fast { + continue; + } + + if all_results.iter().any(|r: &CheckResult| !r.passed) { + break; + } + } + + Ok(all_results) + } + + /// Runs a single check. + async fn run_check(&self, name: &str, check: &CheckConfig, mode: Mode) -> Result { + run_check_async(name, check, mode, &self.config, self.repo.as_ref()).await + } +} + +/// Runs a check asynchronously (for parallel execution). +async fn run_check_async( + name: &str, + check: &CheckConfig, + mode: Mode, + config: &Config, + repo: Option<&GitRepo>, +) -> Result { + // Check if the check is enabled + if !check_enabled(check, repo) { + return Ok(CheckResult::skipped( + name.to_string(), + "Condition not met".to_string(), + )); + } + + // Build execution options + let timeout_str = match mode { + Mode::Human => &config.human.timeout, + Mode::Agent | Mode::Ci => &config.agent.timeout, + }; + + let timeout = parse_duration(timeout_str).unwrap_or(Duration::from_secs(300)); + + let mut options = ExecuteOptions::default().timeout(timeout); + + if let Some(ref repo) = repo { + options = options.cwd(repo.root()); + } + + // Add environment variables from check config + for (key, value) in &check.env { + options = options.env(key.clone(), value.clone()); + } + + // Execute the command + let executor = Executor::new(); + + // Show progress + let pb = ProgressBar::new_spinner(); + pb.set_style( + ProgressStyle::default_spinner() + .template("{spinner:.cyan} {msg}") + .ok() + .unwrap_or_else(ProgressStyle::default_spinner), + ); + pb.set_message(format!("Running {name}...")); + pb.enable_steady_tick(Duration::from_millis(100)); + + let output = executor.execute(&check.run, options).await?; + + pb.finish_and_clear(); + + // Format result + if output.success() { + eprintln!("{} {name}", style("✓").green()); + } else if output.timed_out { + eprintln!("{} {name} (timed out)", style("✗").red()); + } else { + eprintln!("{} {name}", style("✗").red()); + } + + Ok(CheckResult { + name: name.to_string(), + passed: output.success(), + output, + skipped: false, + skip_reason: None, + }) +} + +/// Checks if a check is enabled based on its conditions. +fn check_enabled(check: &CheckConfig, repo: Option<&GitRepo>) -> bool { + let Some(ref condition) = check.enabled_if else { + return true; + }; + + // Check file_exists condition + if let Some(ref path) = condition.file_exists { + if let Some(repo) = repo { + if !repo.file_exists(path) { + return false; + } + } + } + + // Check dir_exists condition + if let Some(ref path) = condition.dir_exists { + if let Some(repo) = repo { + if !repo.dir_exists(path) { + return false; + } + } + } + + // Check command_exists condition + if let Some(ref cmd) = condition.command_exists { + if !Executor::command_exists(cmd) { + return false; + } + } + + true +} + +/// Parses a duration string like "30s", "5m", "1h". +fn parse_duration(s: &str) -> Option { + humantime::parse_duration(s).ok() +} + +/// Gets the number of CPUs for parallel execution. +mod num_cpus { + pub fn get() -> usize { + std::thread::available_parallelism() + .map(|p| p.get()) + .unwrap_or(4) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_duration() { + assert_eq!(parse_duration("30s"), Some(Duration::from_secs(30))); + assert_eq!(parse_duration("5m"), Some(Duration::from_secs(300))); + assert_eq!(parse_duration("1h"), Some(Duration::from_secs(3600))); + assert_eq!(parse_duration("invalid"), None); + } + + #[test] + fn test_run_result_success() { + let result = RunResult { + mode: Mode::Human, + checks: vec![ + CheckResult { + name: "test1".to_string(), + passed: true, + output: CommandOutput { + exit_code: 0, + stdout: String::new(), + stderr: String::new(), + timed_out: false, + duration: Duration::ZERO, + }, + skipped: false, + skip_reason: None, + }, + CheckResult { + name: "test2".to_string(), + passed: true, + output: CommandOutput { + exit_code: 0, + stdout: String::new(), + stderr: String::new(), + timed_out: false, + duration: Duration::ZERO, + }, + skipped: false, + skip_reason: None, + }, + ], + duration: Duration::ZERO, + }; + + assert!(result.success()); + assert_eq!(result.passed_count(), 2); + assert_eq!(result.failed_count(), 0); + } + + #[test] + fn test_run_result_failure() { + let result = RunResult { + mode: Mode::Agent, + checks: vec![ + CheckResult { + name: "test1".to_string(), + passed: true, + output: CommandOutput { + exit_code: 0, + stdout: String::new(), + stderr: String::new(), + timed_out: false, + duration: Duration::ZERO, + }, + skipped: false, + skip_reason: None, + }, + CheckResult { + name: "test2".to_string(), + passed: false, + output: CommandOutput { + exit_code: 1, + stdout: String::new(), + stderr: "Error".to_string(), + timed_out: false, + duration: Duration::ZERO, + }, + skipped: false, + skip_reason: None, + }, + ], + duration: Duration::ZERO, + }; + + assert!(!result.success()); + assert_eq!(result.passed_count(), 1); + assert_eq!(result.failed_count(), 1); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..33d3596 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,55 @@ +//! # agent-precommit +//! +//! Smart pre-commit hooks for humans and AI coding agents. +//! +//! Humans commit often and need fast checks. Agents commit once and need thorough checks. +//! `agent-precommit` auto-detects which is which and runs the right checks. +//! +//! ## Features +//! +//! - **Automatic mode detection**: Detects human vs agent commits via environment variables, +//! TTY detection, and heuristics +//! - **Pre-commit integration**: Works with existing `.pre-commit-config.yaml` setups +//! - **Configurable checks**: Define custom checks for each mode in `agent-precommit.toml` +//! - **Parallel execution**: Run independent checks concurrently for faster agent-mode runs +//! +//! ## Example +//! +//! ```rust,no_run +//! use agent_precommit::{Config, Detector, Mode, Runner}; +//! +//! #[tokio::main] +//! async fn main() -> anyhow::Result<()> { +//! // Load configuration +//! let config = Config::load_or_default()?; +//! +//! // Detect mode (human, agent, or ci) +//! let detector = Detector::new(&config); +//! let mode = detector.detect(); +//! +//! // Run appropriate checks +//! let runner = Runner::new(config); +//! let result = runner.run(mode).await?; +//! +//! if result.success() { +//! Ok(()) +//! } else { +//! std::process::exit(1); +//! } +//! } +//! ``` + +#![doc(html_root_url = "https://docs.rs/agent-precommit/0.1.0")] +#![cfg_attr(docsrs, feature(doc_cfg))] + +pub mod checks; +pub mod cli; +pub mod config; +pub mod core; +pub mod presets; + +// Re-export main types for convenience +pub use config::Config; +pub use core::detector::{Detector, Mode}; +pub use core::error::{Error, Result}; +pub use core::runner::{CheckResult, Runner, RunResult}; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..f77c0a4 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,14 @@ +//! Main entry point for the `apc` CLI. + +use agent_precommit::cli; +use std::process::ExitCode; + +fn main() -> ExitCode { + match cli::run() { + Ok(code) => code, + Err(e) => { + eprintln!("Error: {e}"); + ExitCode::FAILURE + } + } +} diff --git a/src/presets/mod.rs b/src/presets/mod.rs new file mode 100644 index 0000000..21700fa --- /dev/null +++ b/src/presets/mod.rs @@ -0,0 +1,64 @@ +//! Configuration presets for common project types. +//! +//! Presets provide sensible default configurations for different tech stacks. + +/// Available preset names. +pub mod names { + /// Python projects (pytest, ruff, mypy). + pub const PYTHON: &str = "python"; + /// Node.js/TypeScript projects (npm, eslint, jest). + pub const NODE: &str = "node"; + /// Rust projects (cargo, clippy). + pub const RUST: &str = "rust"; + /// Go projects (go test, golangci-lint). + pub const GO: &str = "go"; +} + +/// Returns a list of available preset names. +#[must_use] +pub const fn available() -> &'static [&'static str] { + &[names::PYTHON, names::NODE, names::RUST, names::GO] +} + +/// Returns true if the preset name is valid. +#[must_use] +pub fn is_valid(name: &str) -> bool { + available().contains(&name) +} + +/// Returns a description for a preset. +#[must_use] +pub fn description(name: &str) -> &'static str { + match name { + names::PYTHON => "Python projects (pytest, ruff, mypy, pre-commit integration)", + names::NODE => "Node.js/TypeScript projects (npm, eslint, jest, tsc)", + names::RUST => "Rust projects (cargo fmt, clippy, cargo test)", + names::GO => "Go projects (gofmt, golangci-lint, go test)", + _ => "Unknown preset", + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_available() { + assert!(!available().is_empty()); + assert!(available().contains(&"python")); + assert!(available().contains(&"rust")); + } + + #[test] + fn test_is_valid() { + assert!(is_valid("python")); + assert!(is_valid("node")); + assert!(!is_valid("invalid")); + } + + #[test] + fn test_description() { + assert!(!description("python").is_empty()); + assert!(!description("rust").is_empty()); + } +} diff --git a/tests/integration_test.rs b/tests/integration_test.rs new file mode 100644 index 0000000..0b435e1 --- /dev/null +++ b/tests/integration_test.rs @@ -0,0 +1,261 @@ +//! Integration tests for agent-precommit CLI. + +use assert_cmd::Command; +use predicates::prelude::*; +use tempfile::TempDir; + +/// Creates a test git repository. +fn create_test_repo() -> TempDir { + let temp = TempDir::new().expect("create temp dir"); + + std::process::Command::new("git") + .args(["init"]) + .current_dir(temp.path()) + .output() + .expect("init repo"); + + std::process::Command::new("git") + .args(["config", "user.email", "test@test.com"]) + .current_dir(temp.path()) + .output() + .expect("set email"); + + std::process::Command::new("git") + .args(["config", "user.name", "Test"]) + .current_dir(temp.path()) + .output() + .expect("set name"); + + temp +} + +#[test] +fn test_help() { + Command::cargo_bin("apc") + .unwrap() + .arg("--help") + .assert() + .success() + .stdout(predicate::str::contains("Smart pre-commit hooks")); +} + +#[test] +fn test_version() { + Command::cargo_bin("apc") + .unwrap() + .arg("--version") + .assert() + .success() + .stdout(predicate::str::contains(env!("CARGO_PKG_VERSION"))); +} + +#[test] +fn test_detect_default_human() { + let temp = create_test_repo(); + + Command::cargo_bin("apc") + .unwrap() + .arg("detect") + .current_dir(temp.path()) + .assert() + .success() + .stderr(predicate::str::contains("human")); +} + +#[test] +fn test_detect_agent_mode() { + let temp = create_test_repo(); + + Command::cargo_bin("apc") + .unwrap() + .arg("detect") + .env("AGENT_MODE", "1") + .current_dir(temp.path()) + .assert() + .success() + .stderr(predicate::str::contains("agent")); +} + +#[test] +fn test_init_creates_config() { + let temp = create_test_repo(); + + Command::cargo_bin("apc") + .unwrap() + .arg("init") + .current_dir(temp.path()) + .assert() + .success() + .stderr(predicate::str::contains("Created agent-precommit.toml")); + + assert!(temp.path().join("agent-precommit.toml").exists()); +} + +#[test] +fn test_init_with_preset() { + let temp = create_test_repo(); + + Command::cargo_bin("apc") + .unwrap() + .args(["init", "--preset", "rust"]) + .current_dir(temp.path()) + .assert() + .success(); + + let config = std::fs::read_to_string(temp.path().join("agent-precommit.toml")) + .expect("read config"); + + assert!(config.contains("clippy")); +} + +#[test] +fn test_init_already_exists() { + let temp = create_test_repo(); + std::fs::write(temp.path().join("agent-precommit.toml"), "").expect("create config"); + + Command::cargo_bin("apc") + .unwrap() + .arg("init") + .current_dir(temp.path()) + .assert() + .failure() + .stderr(predicate::str::contains("already exists")); +} + +#[test] +fn test_init_force() { + let temp = create_test_repo(); + std::fs::write(temp.path().join("agent-precommit.toml"), "").expect("create config"); + + Command::cargo_bin("apc") + .unwrap() + .args(["init", "--force"]) + .current_dir(temp.path()) + .assert() + .success(); +} + +#[test] +fn test_validate_no_config() { + let temp = create_test_repo(); + + Command::cargo_bin("apc") + .unwrap() + .arg("validate") + .current_dir(temp.path()) + .assert() + .failure() + .stderr(predicate::str::contains("not found")); +} + +#[test] +fn test_validate_valid_config() { + let temp = create_test_repo(); + + // Initialize first + Command::cargo_bin("apc") + .unwrap() + .arg("init") + .current_dir(temp.path()) + .output() + .expect("init"); + + Command::cargo_bin("apc") + .unwrap() + .arg("validate") + .current_dir(temp.path()) + .assert() + .success() + .stderr(predicate::str::contains("valid")); +} + +#[test] +fn test_list_checks() { + let temp = create_test_repo(); + + Command::cargo_bin("apc") + .unwrap() + .arg("init") + .current_dir(temp.path()) + .output() + .expect("init"); + + Command::cargo_bin("apc") + .unwrap() + .arg("list") + .current_dir(temp.path()) + .assert() + .success() + .stderr(predicate::str::contains("Human mode checks")) + .stderr(predicate::str::contains("Agent mode checks")); +} + +#[test] +fn test_install_hook() { + let temp = create_test_repo(); + + Command::cargo_bin("apc") + .unwrap() + .arg("install") + .current_dir(temp.path()) + .assert() + .success() + .stderr(predicate::str::contains("Installed pre-commit hook")); + + let hook_path = temp.path().join(".git/hooks/pre-commit"); + assert!(hook_path.exists()); + + let hook_content = std::fs::read_to_string(&hook_path).expect("read hook"); + assert!(hook_content.contains("agent-precommit")); +} + +#[test] +fn test_uninstall_hook() { + let temp = create_test_repo(); + + // Install first + Command::cargo_bin("apc") + .unwrap() + .arg("install") + .current_dir(temp.path()) + .output() + .expect("install"); + + // Then uninstall + Command::cargo_bin("apc") + .unwrap() + .arg("uninstall") + .current_dir(temp.path()) + .assert() + .success() + .stderr(predicate::str::contains("Removed")); + + assert!(!temp.path().join(".git/hooks/pre-commit").exists()); +} + +#[test] +fn test_skip_with_env_var() { + let temp = create_test_repo(); + + Command::cargo_bin("apc") + .unwrap() + .arg("run") + .env("APC_SKIP", "1") + .current_dir(temp.path()) + .assert() + .success() + .stderr(predicate::str::contains("Skipping")); +} + +#[test] +fn test_not_git_repo() { + let temp = TempDir::new().expect("create temp dir"); + + Command::cargo_bin("apc") + .unwrap() + .arg("detect") + .current_dir(temp.path()) + .assert() + .failure() + .stderr(predicate::str::contains("Not in a Git repository")); +}