[
  {
    "path": ".cargo/config.toml",
    "content": "# Copyright 2024 tison <wander4096@gmail.com>\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n# See also https://github.com/rust-lang/rust/issues/44991\n\n[target.x86_64-unknown-linux-musl]\nrustflags = [\"-C\", \"target-feature=-crt-static\"]\n\n[target.aarch64-unknown-linux-musl]\nrustflags = [\"-C\", \"target-feature=-crt-static\"]\n"
  },
  {
    "path": ".dockerignore",
    "content": "target/\n!**/src/main/**/target/\n!**/src/test/**/target/\ndependency-reduced-pom.xml\n\n### IntelliJ IDEA ###\n.idea/modules.xml\n.idea/jarRepositories.xml\n.idea/compiler.xml\n.idea/libraries/\n*.iws\n*.iml\n*.ipr\n\n### Mac OS ###\n.DS_Store\n"
  },
  {
    "path": ".editorconfig",
    "content": "# Copyright 2024 tison <wander4096@gmail.com>\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nroot = true\n\n[*]\nend_of_line = lf\nindent_style = space\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\n[*.txt]\ninsert_final_newline = false\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# Copyright 2024 tison <wander4096@gmail.com>\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\ngithub: tisonkun\n"
  },
  {
    "path": ".github/actions/docker-push-by-digest/action.yml",
    "content": "# Copyright 2024 tison <wander4096@gmail.com>\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nname: Docker push by digest\ndescription: Build and push Docker image by digest\n\ninputs:\n  name:\n    description: The name of Docker image\n    required: true\n  file:\n    description: The name of Dockerfile in use\n    required: true\n\noutputs:\n  digest:\n    description: Docker image digest if pushed\n    value: ${{ steps.push.outputs.digest }}\n\nruns:\n  using: composite\n  steps:\n    - uses: docker/setup-buildx-action@v2\n    - uses: docker/login-action@v2\n      with:\n        registry: ghcr.io\n        username: ${{ github.actor }}\n        password: ${{ github.token }}\n    - name: Docker meta\n      id: meta\n      uses: docker/metadata-action@v4\n      with:\n        images: ghcr.io/${{ github.repository_owner }}/${{ inputs.name }}\n    - name: Build and push\n      id: push\n      uses: docker/build-push-action@v5\n      with:\n        context: .\n        file: ${{ inputs.file }}\n        tags: ghcr.io/${{ github.repository_owner }}/${{ inputs.name }}\n        labels: ${{ steps.meta.outputs.labels }}\n        outputs: type=image,push=true,push-by-digest=true\n"
  },
  {
    "path": ".github/actions/docker-release/action.yml",
    "content": "# Copyright 2024 tison <wander4096@gmail.com>\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nname: Docker release\ndescription: Release Docker Images\n\ninputs:\n  name:\n    description: The name of Docker image\n    required: true\n  digests:\n    description: The digest of images to be merged\n    required: true\n\nruns:\n  using: composite\n  steps:\n    - uses: docker/setup-buildx-action@v2\n    - uses: docker/login-action@v2\n      with:\n        registry: ghcr.io\n        username: ${{ github.actor }}\n        password: ${{ github.token }}\n    - name: Docker meta\n      id: meta\n      uses: docker/metadata-action@v4\n      with:\n        images: ghcr.io/${{ github.repository_owner }}/${{ inputs.name }}\n        sep-tags: ' '\n        tags: |\n          type=semver,pattern={{raw}}\n          type=semver,pattern=v{{major}}\n          type=edge,branch=main\n    - name: Push manifest\n      shell: bash\n      run: |\n        for tag in ${{ steps.meta.outputs.tags }}; do\n          echo \"Preparing manifest for tag: $tag...\"\n          docker buildx imagetools create -t $tag ${{ inputs.digests }}\n        done\n"
  },
  {
    "path": ".github/semantic.yml",
    "content": "# Copyright 2024 tison <wander4096@gmail.com>\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n# The pull request's title should be fulfilled the following pattern:\n#\n#     <type>[optional scope]: <description>\n#\n# ... where valid types and scopes can be found below; for example:\n#\n#     build(maven): One level down for native profile\n#\n# More about configurations on https://github.com/Ezard/semantic-prs#configuration\n\nenabled: true\n\ntitleOnly: true\n\ntypes:\n  - feat\n  - fix\n  - docs\n  - style\n  - refactor\n  - perf\n  - test\n  - build\n  - ci\n  - chore\n  - revert\n\ntargetUrl: https://github.com/korandoru/hawkeye/blob/main/.github/semantic.yml\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "# Copyright 2024 tison <wander4096@gmail.com>\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nname: CI\non:\n  pull_request:\n    branches: [ main ]\n  push:\n    branches: [ main ]\n\n# Concurrency strategy:\n#   github.workflow: distinguish this workflow from others\n#   github.event_name: distinguish `push` event from `pull_request` event\n#   github.event.number: set to the number of the pull request if `pull_request` event\n#   github.run_id: otherwise, it's a `push` event, only cancel if we rerun the workflow\n#\n# Reference:\n#   https://docs.github.com/en/actions/using-jobs/using-concurrency\n#   https://docs.github.com/en/actions/learn-github-actions/contexts#github-context\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.number || github.run_id }}\n  cancel-in-progress: true\n\njobs:\n  check:\n    name: Check\n    runs-on: ubuntu-24.04\n    steps:\n      - uses: actions/checkout@v4\n      - uses: Swatinem/rust-cache@v2\n      - uses: dtolnay/rust-toolchain@nightly\n      - name: Check Clippy\n        run: cargo clippy --tests --all-features --all-targets --workspace -- -D warnings\n      - name: Check format\n        run: cargo fmt --all --check\n\n  test:\n    strategy:\n      matrix:\n        rust-version: [\"1.90.0\", \"stable\"]\n    name: Build and test\n    runs-on: ubuntu-24.04\n    steps:\n      - uses: actions/checkout@v4\n      - uses: Swatinem/rust-cache@v2\n      - name: Delete rust-toolchain.toml\n        run: rm rust-toolchain.toml\n      - run: cargo build --workspace --all-features --bins --tests --examples --benches\n      - name: Run tests\n        run: cargo test --workspace -- --nocapture\n      - name: Run benches\n        run: cargo bench --workspace -- --test\n\n  docker:\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ ubuntu-24.04 ]\n    runs-on: ${{matrix.os}}\n    name: Docker sanity check on ${{ matrix.os }}\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - name: Build and load\n        uses: docker/build-push-action@v5\n        with:\n          context: .\n          file: Dockerfile\n          tags: hawkeye:ci\n          outputs: type=docker\n      - name: Save image\n        run: docker save hawkeye:ci -o /tmp/hawkeye-${{matrix.os}}.tar\n      - name: Upload artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: hawkeye-${{matrix.os}}\n          path: /tmp/hawkeye-${{matrix.os}}.tar\n      - name: Sanity check\n        run: |\n          cp action.yml action.yml.bak\n          docker image inspect hawkeye:ci --format='{{.Size}}'\n          docker run --rm -v $(pwd):/github/workspace hawkeye:ci -V\n          docker run --rm -v $(pwd):/github/workspace hawkeye:ci check --fail-if-unknown\n\n  smoke:\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-24.04, windows-2022, macos-14]\n    runs-on: ${{ matrix.os }}\n    name: Smoke test on ${{ matrix.os }}\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - uses: Swatinem/rust-cache@v2\n      - name: Dog feed license check\n        shell: bash\n        run: |\n          cargo run --bin hawkeye -- -V\n          cargo run --bin hawkeye -- check --fail-if-unknown\n      - uses: actions/setup-python@v5\n        with:\n          python-version: '3.12'\n      - name: Run integration tests\n        shell: bash\n        run: ./tests/it.py\n\n  gha:\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-24.04, windows-2022, macos-14]\n    runs-on: ${{ matrix.os }}\n    name: Smoke test for GitHub Actions on ${{ matrix.os }}\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - name: Check and fail if unknown\n        uses: ./\n        with:\n          args: --fail-if-unknown\n      - name: Check default config works\n        uses: ./\n\n  required:\n    name: Required\n    runs-on: ubuntu-24.04\n    if: ${{ always() }}\n    needs:\n      - check\n      - docker\n      - gha\n      - smoke\n      - test\n    steps:\n      - name: Guardian\n        run: |\n          if [[ ! ( \\\n                 \"${{ needs.check.result }}\" == \"success\" \\\n              && \"${{ needs.docker.result }}\" == \"success\" \\\n              && \"${{ needs.gha.result }}\" == \"success\" \\\n              && \"${{ needs.smoke.result }}\" == \"success\" \\\n              && \"${{ needs.test.result }}\" == \"success\" \\\n              ) ]]; then\n            echo \"Required jobs haven't been completed successfully.\"\n            exit -1\n          fi\n"
  },
  {
    "path": ".github/workflows/docker.yml",
    "content": "# Copyright 2024 tison <wander4096@gmail.com>\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nname: Build and Push Docker Images\n\non:\n  push:\n    branches: ['main']\n    tags: ['v*.*']\n  workflow_dispatch:\n\njobs:\n  build-and-push-hawkeye-amd64:\n    runs-on: ubuntu-24.04\n    permissions:\n      packages: write\n    steps:\n      - uses: actions/checkout@v4\n      - name: Build and push by digest\n        uses: ./.github/actions/docker-push-by-digest\n        id: build\n        with:\n          name: hawkeye\n          file: Dockerfile\n    outputs:\n      digest: ${{ steps.build.outputs.digest }}\n\n  build-and-push-hawkeye-arm64:\n    runs-on: ubuntu-24.04-arm\n    permissions:\n      packages: write\n    steps:\n      - uses: actions/checkout@v4\n      - name: Build and push by digest\n        uses: ./.github/actions/docker-push-by-digest\n        id: build\n        with:\n          name: hawkeye\n          file: Dockerfile\n    outputs:\n      digest: ${{ steps.build.outputs.digest }}\n\n  release-hawkeye:\n    runs-on: ubuntu-24.04\n    permissions:\n      packages: write\n    needs:\n      - build-and-push-hawkeye-amd64\n      - build-and-push-hawkeye-arm64\n    steps:\n      - uses: actions/checkout@v4\n      - name: Merge and push manifest\n        uses: ./.github/actions/docker-release\n        with:\n          name: hawkeye\n          digests: >\n            ${{needs.build-and-push-hawkeye-amd64.outputs.digest}}\n            ${{needs.build-and-push-hawkeye-arm64.outputs.digest}}\n\n  release-native:\n    runs-on: ubuntu-24.04\n    permissions:\n      packages: write\n    needs:\n      - build-and-push-hawkeye-amd64\n      - build-and-push-hawkeye-arm64\n    steps:\n      - uses: actions/checkout@v4\n      - name: Merge and push manifest\n        uses: ./.github/actions/docker-release\n        with:\n          name: hawkeye-native\n          digests: >\n            ghcr.io/${{ github.repository_owner }}/hawkeye@${{needs.build-and-push-hawkeye-amd64.outputs.digest}}\n            ghcr.io/${{ github.repository_owner }}/hawkeye@${{needs.build-and-push-hawkeye-arm64.outputs.digest}}\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "# This file was autogenerated by cargo-dist: https://opensource.axo.dev/cargo-dist/\n#\n# Copyright 2022-2024, axodotdev\n# SPDX-License-Identifier: MIT or Apache-2.0\n#\n# CI that:\n#\n# * checks for a Git Tag that looks like a release\n# * builds artifacts with cargo-dist (archives, installers, hashes)\n# * uploads those artifacts to temporary workflow zip\n# * on success, uploads the artifacts to a GitHub Release\n#\n# Note that the GitHub Release will be created with a generated\n# title/body based on your changelogs.\n\nname: Release\npermissions:\n  \"contents\": \"write\"\n\n# This task will run whenever you push a git tag that looks like a version\n# like \"1.0.0\", \"v0.1.0-prerelease.1\", \"my-app/0.1.0\", \"releases/v1.0.0\", etc.\n# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where\n# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION\n# must be a Cargo-style SemVer Version (must have at least major.minor.patch).\n#\n# If PACKAGE_NAME is specified, then the announcement will be for that\n# package (erroring out if it doesn't have the given version or isn't cargo-dist-able).\n#\n# If PACKAGE_NAME isn't specified, then the announcement will be for all\n# (cargo-dist-able) packages in the workspace with that version (this mode is\n# intended for workspaces with only one dist-able package, or with all dist-able\n# packages versioned/released in lockstep).\n#\n# If you push multiple tags at once, separate instances of this workflow will\n# spin up, creating an independent announcement for each one. However, GitHub\n# will hard limit this to 3 tags per commit, as it will assume more tags is a\n# mistake.\n#\n# If there's a prerelease-style suffix to the version, then the release(s)\n# will be marked as a prerelease.\non:\n  pull_request:\n  push:\n    tags:\n      - '**[0-9]+.[0-9]+.[0-9]+*'\n\njobs:\n  # Run 'cargo dist plan' (or host) to determine what tasks we need to do\n  plan:\n    runs-on: \"ubuntu-22.04\"\n    outputs:\n      val: ${{ steps.plan.outputs.manifest }}\n      tag: ${{ !github.event.pull_request && github.ref_name || '' }}\n      tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}\n      publishing: ${{ !github.event.pull_request }}\n    env:\n      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          submodules: recursive\n      - name: Install cargo-dist\n        # we specify bash to get pipefail; it guards against the `curl` command\n        # failing. otherwise `sh` won't catch that `curl` returned non-0\n        shell: bash\n        run: \"curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.22.1/cargo-dist-installer.sh | sh\"\n      - name: Cache cargo-dist\n        uses: actions/upload-artifact@v4\n        with:\n          name: cargo-dist-cache\n          path: ~/.cargo/bin/cargo-dist\n      # sure would be cool if github gave us proper conditionals...\n      # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible\n      # functionality based on whether this is a pull_request, and whether it's from a fork.\n      # (PRs run on the *source* but secrets are usually on the *target* -- that's *good*\n      # but also really annoying to build CI around when it needs secrets to work right.)\n      - id: plan\n        run: |\n          cargo dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json\n          echo \"cargo dist ran successfully\"\n          cat plan-dist-manifest.json\n          echo \"manifest=$(jq -c \".\" plan-dist-manifest.json)\" >> \"$GITHUB_OUTPUT\"\n      - name: \"Upload dist-manifest.json\"\n        uses: actions/upload-artifact@v4\n        with:\n          name: artifacts-plan-dist-manifest\n          path: plan-dist-manifest.json\n\n  # Build and packages all the platform-specific things\n  build-local-artifacts:\n    name: build-local-artifacts (${{ join(matrix.targets, ', ') }})\n    # Let the initial task tell us to not run (currently very blunt)\n    needs:\n      - plan\n    if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}\n    strategy:\n      fail-fast: false\n      # Target platforms/runners are computed by cargo-dist in create-release.\n      # Each member of the matrix has the following arguments:\n      #\n      # - runner: the github runner\n      # - dist-args: cli flags to pass to cargo dist\n      # - install-dist: expression to run to install cargo-dist on the runner\n      #\n      # Typically there will be:\n      # - 1 \"global\" task that builds universal installers\n      # - N \"local\" tasks that build each platform's binaries and platform-specific installers\n      matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}\n    runs-on: ${{ matrix.runner }}\n    env:\n      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json\n    steps:\n      - name: enable windows longpaths\n        run: |\n          git config --global core.longpaths true\n      - uses: actions/checkout@v4\n        with:\n          submodules: recursive\n      - name: Install cargo-dist\n        run: ${{ matrix.install_dist }}\n      # Get the dist-manifest\n      - name: Fetch local artifacts\n        uses: actions/download-artifact@v4\n        with:\n          pattern: artifacts-*\n          path: target/distrib/\n          merge-multiple: true\n      - name: Install dependencies\n        run: |\n          ${{ matrix.packages_install }}\n      - name: Build artifacts\n        run: |\n          # Actually do builds and make zips and whatnot\n          cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json\n          echo \"cargo dist ran successfully\"\n      - id: cargo-dist\n        name: Post-build\n        # We force bash here just because github makes it really hard to get values up\n        # to \"real\" actions without writing to env-vars, and writing to env-vars has\n        # inconsistent syntax between shell and powershell.\n        shell: bash\n        run: |\n          # Parse out what we just built and upload it to scratch storage\n          echo \"paths<<EOF\" >> \"$GITHUB_OUTPUT\"\n          jq --raw-output \".upload_files[]\" dist-manifest.json >> \"$GITHUB_OUTPUT\"\n          echo \"EOF\" >> \"$GITHUB_OUTPUT\"\n\n          cp dist-manifest.json \"$BUILD_MANIFEST_NAME\"\n      - name: \"Upload artifacts\"\n        uses: actions/upload-artifact@v4\n        with:\n          name: artifacts-build-local-${{ join(matrix.targets, '_') }}\n          path: |\n            ${{ steps.cargo-dist.outputs.paths }}\n            ${{ env.BUILD_MANIFEST_NAME }}\n\n  # Build and package all the platform-agnostic(ish) things\n  build-global-artifacts:\n    needs:\n      - plan\n      - build-local-artifacts\n    runs-on: \"ubuntu-22.04\"\n    env:\n      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          submodules: recursive\n      - name: Install cached cargo-dist\n        uses: actions/download-artifact@v4\n        with:\n          name: cargo-dist-cache\n          path: ~/.cargo/bin/\n      - run: chmod +x ~/.cargo/bin/cargo-dist\n      # Get all the local artifacts for the global tasks to use (for e.g. checksums)\n      - name: Fetch local artifacts\n        uses: actions/download-artifact@v4\n        with:\n          pattern: artifacts-*\n          path: target/distrib/\n          merge-multiple: true\n      - id: cargo-dist\n        shell: bash\n        run: |\n          cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json \"--artifacts=global\" > dist-manifest.json\n          echo \"cargo dist ran successfully\"\n\n          # Parse out what we just built and upload it to scratch storage\n          echo \"paths<<EOF\" >> \"$GITHUB_OUTPUT\"\n          jq --raw-output \".upload_files[]\" dist-manifest.json >> \"$GITHUB_OUTPUT\"\n          echo \"EOF\" >> \"$GITHUB_OUTPUT\"\n\n          cp dist-manifest.json \"$BUILD_MANIFEST_NAME\"\n      - name: \"Upload artifacts\"\n        uses: actions/upload-artifact@v4\n        with:\n          name: artifacts-build-global\n          path: |\n            ${{ steps.cargo-dist.outputs.paths }}\n            ${{ env.BUILD_MANIFEST_NAME }}\n  # Determines if we should publish/announce\n  host:\n    needs:\n      - plan\n      - build-local-artifacts\n      - build-global-artifacts\n    # Only run if we're \"publishing\", and only if local and global didn't fail (skipped is fine)\n    if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }}\n    env:\n      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n    runs-on: \"ubuntu-22.04\"\n    outputs:\n      val: ${{ steps.host.outputs.manifest }}\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          submodules: recursive\n      - name: Install cached cargo-dist\n        uses: actions/download-artifact@v4\n        with:\n          name: cargo-dist-cache\n          path: ~/.cargo/bin/\n      - run: chmod +x ~/.cargo/bin/cargo-dist\n      # Fetch artifacts from scratch-storage\n      - name: Fetch artifacts\n        uses: actions/download-artifact@v4\n        with:\n          pattern: artifacts-*\n          path: target/distrib/\n          merge-multiple: true\n      - id: host\n        shell: bash\n        run: |\n          cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json\n          echo \"artifacts uploaded and released successfully\"\n          cat dist-manifest.json\n          echo \"manifest=$(jq -c \".\" dist-manifest.json)\" >> \"$GITHUB_OUTPUT\"\n      - name: \"Upload dist-manifest.json\"\n        uses: actions/upload-artifact@v4\n        with:\n          # Overwrite the previous copy\n          name: artifacts-dist-manifest\n          path: dist-manifest.json\n      # Create a GitHub Release while uploading all files to it\n      - name: \"Download GitHub Artifacts\"\n        uses: actions/download-artifact@v4\n        with:\n          pattern: artifacts-*\n          path: artifacts\n          merge-multiple: true\n      - name: Cleanup\n        run: |\n          # Remove the granular manifests\n          rm -f artifacts/*-dist-manifest.json\n      - name: Create GitHub Release\n        env:\n          PRERELEASE_FLAG: \"${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}\"\n          ANNOUNCEMENT_TITLE: \"${{ fromJson(steps.host.outputs.manifest).announcement_title }}\"\n          ANNOUNCEMENT_BODY: \"${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}\"\n          RELEASE_COMMIT: \"${{ github.sha }}\"\n        run: |\n          # Write and read notes from a file to avoid quoting breaking things\n          echo \"$ANNOUNCEMENT_BODY\" > $RUNNER_TEMP/notes.txt\n\n          gh release create \"${{ needs.plan.outputs.tag }}\" --target \"$RELEASE_COMMIT\" $PRERELEASE_FLAG --title \"$ANNOUNCEMENT_TITLE\" --notes-file \"$RUNNER_TEMP/notes.txt\" artifacts/*\n\n  announce:\n    needs:\n      - plan\n      - host\n    # use \"always() && ...\" to allow us to wait for all publish jobs while\n    # still allowing individual publish jobs to skip themselves (for prereleases).\n    # \"host\" however must run to completion, no skipping allowed!\n    if: ${{ always() && needs.host.result == 'success' }}\n    runs-on: \"ubuntu-22.04\"\n    env:\n      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          submodules: recursive\n"
  },
  {
    "path": ".gitignore",
    "content": "/target\n*.bak\n"
  },
  {
    "path": ".pre-commit-hooks.yaml",
    "content": "# Copyright 2024 tison <wander4096@gmail.com>\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n- id: hawkeye-format\n  name: hawkeye-format\n  description: \"Run `hawkeye format` for license header formatting\"\n  entry: hawkeye format\n  language: python\n  args: []\n  pass_filenames: false\n  require_serial: true\n  additional_dependencies: []\n  minimum_pre_commit_version: \"2.9.2\"\n- id: hawkeye-format-docker\n  name: hawkeye-format\n  description: \"Run `hawkeye format` for license header formatting\"\n  entry: hawkeye format\n  language: docker\n  args: []\n  pass_filenames: false\n  require_serial: true\n  additional_dependencies: []\n  minimum_pre_commit_version: \"2.9.2\"\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# CHANGELOG\n\nAll notable changes to this project will be documented in this file.\n\n## Unreleased\n\n## [6.5.1] 2026-02-14\n\n### Bug fixes\n\n* Properly resolve relative paths when populating Git attributes for untracked folders.\n\n## [6.5.0] 2026-02-09\n\n### Notable changes\n\n* Minimal Supported Rust Version (MSRV) is now 1.90.0.\n\n### Bug fixes\n\n* `hawkeye` CLI now uses hawkeye-fmt of exactly the same version to format headers, instead of using the latest version of `hawkeye-fmt` that may not be compatible with the current version of `hawkeye`.\n\n### Improvements\n\n* Replace `anyhow` with `exn` for more informative error messages.\n\n## [6.4.2] 2026-02-07\n\n## Bug fixes\n\n* Set Git attributes for untracked folders as if it were committed now.\n\n## [6.4.1] 2026-01-13\n\n## Improvements\n\n* Use `TextLayout` for logging output to improve formatting and readability.\n\n## [6.4.0] 2026-01-12\n\n### Notable changes\n\n* `attrs.disk_file_created_year`, `attrs.git_file_created_year`, and `attrs.git_file_modified_year` are now integers instead of strings. Most use cases should not be affected.\n* `attrs.git_file_created_year` is now set even if the file is not tracked by Git. In this case, it will be set to the current year (as if it were committed now).\n* `attrs.git_file_modified_year` is now overwritten if the file is modified but not committed by Git. In this case, it will be set to the current year (as if it were committed now).\n* `attrs.disk_file_created_year` is then soft-deprecated. It can still be set, but it is recommended to use `attrs.git_file_created_year` and `attrs.git_file_modified_year` directly instead.\n\nThe semantic changes above are breaking, but they should not affect most users and should always be what you want.\n\n* `additionalHeaders` and `headerPath` now search from the following paths in order:\n  1. The directory of the configuration file, a.k.a., config_dir.\n  2. The configured baseDir.\n  3. The current working directory.\n\n## Improvements\n\n* If `--config` is not specified, HawkEye will now search for `.licenserc.toml` in addition to `licenserc.toml`.\n\n## [6.3.0] 2025-10-09\n\n### New features\n\n* Add distribution against musl libc ([#196](https://github.com/korandoru/hawkeye/pull/196)).\n\n## [6.2.0] 2025-08-25\n\n### New features\n\n* Supports format Vue files: pattern = \"vue\" and headerType = \"XML_STYLE\".\n* Supports format Containerfile files: pattern = \"Containerfile\" and headerType = \"SCRIPT_STYLE\".\n* Add a shared flag to store lists of files to change ([#194](https://github.com/korandoru/hawkeye/pull/194)).\n\n## [6.1.1] 2025-06-11\n\n### New features\n\n* Supports format CommonJS files: pattern = \"cjs\" and headerType = \"SLASHSTAR_STYLE\".\n* Supports format Verilog files: pattern = \"v\" and headerType = \"SLASHSTAR_STYLE\".\n* Supports format SystemVerilog files: pattern = \"sv\" and headerType = \"SLASHSTAR_STYLE\".\n\n## [6.1.0] 2025-06-06\n\n### New features\n\n* `attrs.disk_file_created_year` can be used to replace nonexisting Git attrs like `{{attrs.git_file_created_year if attrs.git_file_created_year else attrs.disk_file_created_year }}`\n\n## [6.0.0] 2025-01-28\n\n### Breaking changes\n\nNow, HawkEye uses MiniJinja as the template engine.\n\nAll the `properties` configured will be passed to the template engine as the `props` value, and thus:\n\n* Previous `${property}` should be replaced with `{{ props[\"property\"] }}`.\n* Previous built-in variables `hawkeye.core.filename` is now `attrs.filename`.\n* Previous built-in variables `hawkeye.git.fileCreatedYear` is now `attrs.git_file_created_year`.\n* Previous built-in variables `hawkeye.git.fileModifiedYear` is now `attrs.git_file_modified_year`.\n\nNew properties:\n\n* `attrs.git_authors` is a collection of authors of the file. You can join them with `, ` to get a string by `{{ attrs.git_authors | join(\", \") }}`.\n\n### Notable changes\n\nNow, HawkEye would detect a leading BOM (Byte Order Mark) and remove it if it exists (#166). I tend to treat this as a bug fix, but it may affect the output of the header.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing Guide\n\nThanks for your help in improving the project!\n\nThere are two typical contributions to this project:\n\n1. Add support for new languages. You can refer to https://github.com/korandoru/hawkeye/pull/124 as an example.\n2. Add support for new license templates. You can refer to https://github.com/korandoru/hawkeye/pull/148 as an example.\n\nYou can find the core concepts with names listed below to understand the design better:\n\n* `DocumentType` defines how a file in a specific type should be handled.\n* `HeaderDef` defines the format of a specific header; for example, `SCRIPT_STYLE` will construct the header format for scripts like `.sh` or `.py` files.\n* `HeaderStyle` is the serde layer for `HeaderDef`.\n* `Selection` describes how to find the files to be handled.\n* `HeaderParser` extracts the header slice from a source file.\n"
  },
  {
    "path": "Cargo.toml",
    "content": "# Copyright 2024 tison <wander4096@gmail.com>\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n[workspace]\nmembers = [\"cli\", \"fmt\"]\nresolver = \"2\"\n\n[workspace.package]\nversion = \"6.5.1\"\nedition = \"2021\"\nauthors = [\"tison <wander4096@gmail.com>\"]\nreadme = \"README.md\"\nlicense = \"Apache-2.0\"\nrepository = \"https://github.com/korandoru/hawkeye/\"\nrust-version = \"1.90.0\"\n\n[workspace.dependencies]\nhawkeye-fmt = { version = \"=6.5.1\", path = \"fmt\" }\n\nbuild-data = { version = \"0.3.0\" }\nclap = { version = \"4.5.23\", features = [\"derive\"] }\nconst_format = { version = \"0.2.34\" }\nexn = { version = \"0.3.0\" }\nlog = { version = \"0.4.22\", features = [\"kv_serde\", \"serde\"] }\nshadow-rs = { version = \"1.7.0\", default-features = false }\ntoml = { version = \"1.0.1\" }\n\n[workspace.metadata.release]\nsign-tag = true\nshared-version = true\ntag-name = \"v{{version}}\"\npre-release-commit-message = \"chore: release v{{version}}\"\n\n# Config for 'cargo dist'\n[workspace.metadata.dist]\n# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax)\ncargo-dist-version = \"0.22.1\"\n# CI backends to support\nci = \"github\"\n# The installers to generate for each app\ninstallers = [\"shell\"]\n# Target platforms to build apps for (Rust target-triple syntax)\ntargets = [\n  \"aarch64-apple-darwin\",\n  \"aarch64-unknown-linux-gnu\",\n  \"aarch64-unknown-linux-musl\",\n  \"x86_64-apple-darwin\",\n  \"x86_64-unknown-linux-gnu\",\n  \"x86_64-unknown-linux-musl\",\n  \"x86_64-pc-windows-msvc\",\n]\n# Which actions to run on pull requests\npr-run-mode = \"plan\"\n# Whether to install an updater program\ninstall-updater = false\n# Path that installers should place binaries in\ninstall-path = \"CARGO_HOME\"\n\n[workspace.metadata.dist.github-custom-runners]\nx86_64-unknown-linux-gnu = \"ubuntu-22.04\"\naarch64-unknown-linux-gnu = \"ubuntu-22.04-arm\"\nx86_64-unknown-linux-musl = \"ubuntu-22.04\"\naarch64-unknown-linux-musl = \"ubuntu-22.04-arm\"\naarch64-apple-darwin = \"macos-14\"\nx86_64-apple-darwin = \"macos-14\"\nx86_64-pc-windows-msvc = \"windows-2022\"\nglobal = \"ubuntu-22.04\"\n\n# 'cargo dist' build with this profile\n[profile.dist]\ninherits = \"release\"\nlto = \"thin\"\n"
  },
  {
    "path": "Dockerfile",
    "content": "# Copyright 2024 tison <wander4096@gmail.com>\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nFROM public.ecr.aws/docker/library/rust:1.81.0-alpine3.20 as build\nENV RUSTFLAGS=\"-C target-feature=-crt-static\"\nWORKDIR /build\nCOPY . .\nRUN apk fix && \\\n    apk --no-cache --update add git musl-dev && \\\n    cargo build --release --bin hawkeye\n\nFROM public.ecr.aws/docker/library/alpine:3.20.0\nCOPY --from=build /build/target/release/hawkeye /bin/\nRUN apk fix && apk --no-cache --update add libgcc git\nWORKDIR /github/workspace/\nENTRYPOINT [\"/bin/hawkeye\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# HawkEye\n\nSimple license header checker and formatter, in multiple distribution forms.\n\n## Usage\n\nYou can use HawkEye in GitHub Actions or in your local machine. HawkEye provides three basic commands:\n\n```bash\n# check license headers\nhawkeye check\n\n# format license headers (auto-fix all files that failed the check)\nhawkeye format\n\n# remove license headers\nhawkeye remove\n```\n\nYou can use `-h` or `--help` to list out all config options.\n\n### GitHub Actions\n\nThe HawkEye GitHub Action enables users to run license header check by HawkEye with a config file.\n\nFirst of all, add a `licenserc.toml` file in the root of your project. The simplest config for projects licensed under Apache License 2.0 is as below:\n\n> [!NOTE]\n> The full configurations can be found in [the configuration section](#configurations).\n\n```toml\nheaderPath = \"Apache-2.0.txt\"\n\n[properties]\ninceptionYear = 2023\ncopyrightOwner = \"tison <wander4096@gmail.com>\"\n```\n\nYou should change the copyright line according to your information.\n\nTo check license headers in GitHub Actions, add a step in your GitHub workflow:\n\n```yaml\n- name: Check License Header\n  uses: korandoru/hawkeye@v6\n```\n\n### Docker\n\nAlpine image (~18MB):\n\n```shell\ndocker run -it --rm -v $(pwd):/github/workspace ghcr.io/korandoru/hawkeye check\n```\n\n### Arch Linux\n\n> [!NOTE]\n> Reach out to the maintainer ([@orhun](https://github.com/orhun)) of the [package](https://archlinux.org/packages/extra/x86_64/hawkeye/) or report issues on [Arch Linux GitLab](https://gitlab.archlinux.org/archlinux/packaging/packages/hawkeye) in the case of packaging-related problems.\n\n`hawkeye` can be installed with [pacman](https://wiki.archlinux.org/title/Pacman):\n\n```shell\npacman -S hawkeye\n```\n\n### Cargo Install\n\nThe `hawkeye` executable can be installed by:\n\n```shell\ncargo install hawkeye\n```\n\n### Prebuilt Binary\n\nInstead of `cargo install`, you can install `hawkeye` as a prebuilt binary by:\n\n```shell\nexport VERSION=v6.0.0\ncurl --proto '=https' --tlsv1.2 -LsSf https://github.com/korandoru/hawkeye/releases/download/$VERSION/hawkeye-installer.sh | sh\n```\n\nIt would retain more build info (output by `hawkeye -V`) than `cargo install`.\n\n## Build\n\nThis steps requires Rust toolchain.\n\n```shell\ncargo build --workspace --all-features --bin --tests --examples --benches\n```\n\nBuild Docker image:\n\n```shell\ndocker build . -t hawkeye\n```\n\n## Configurations\n\n### Config file\n\n```toml\n# Base directory for the whole execution.\n# All relative paths is based on this path.\n# default: current working directory\nbaseDir = \".\"\n\n# Inline header template.\n# Either inlineHeader or headerPath should be configured, and inlineHeader is prioritized.\ninlineHeader = \"...\"\n\n# Path to the header template file.\n# Either inlineHeader or headerPath should be configured, and inlineHeader is prioritized.\n# This path is resolved by the ResourceFinder. Check ResourceFinder for the concrete strategy.\n# The header template file is skipped on any execution.\nheaderPath = \"path/to/header.txt\"\n\n# On enabled, check the license header matches exactly with whitespace.\n# Otherwise, strip the header in one line and check.\n# default: true\nstrictCheck = true\n\n# Whether you use the default excludes. Check Default.EXCLUDES for the completed list.\n# To suppress part of excludes in the list, declare exact the same pattern in `includes` list.\n# default: true\nuseDefaultExcludes = true\n\n# The supported patterns of includes and excludes follow gitignore pattern format, plus that:\n# 1. `includes` does not support `!`\n# 2. backslash does not escape letter\n# 3. whitespaces and `#` are normal since we configure line by line\n# See also https://git-scm.com/docs/gitignore#_pattern_format\n\n# Patterns of path to be included on execution.\n# default: all the files under `baseDir`.\nincludes = [\"...\"]\n\n# Patterns of path to be excluded on execution. A leading bang (!) indicates an invert exclude rule.\n# default: empty; if useDefaultExcludes is true, append default excludes.\nexcludes = [\"...\"]\n\n# Keywords that should occur in the header, case-insensitive.\n# default: [\"copyright\"]\nkeywords = [\"copyright\", \"...\"]\n\n# Whether you use the default mapping. Check DocumentType.defaultMapping() for the completed list.\n# default: true\nuseDefaultMapping = true\n\n# Paths to additional header style files. The model of user-defined header style can be found below.\n# default: empty\nadditionalHeaders = [\"...\"]\n\n# Mapping rules (repeated).\n#\n# The key of a mapping rule is a header style type (case-insensitive).\n#\n# Available header style types consist of those defined at `HeaderType` and user-defined ones in `additionalHeaders`.\n# The name of header style type is case-insensitive.\n#\n# If useDefaultMapping is true, the mapping rules defined here can override the default one.\n[mapping.STYLE_NAME]\nfilenames = [\"...\"]  # e.g. \"Dockerfile.native\"\nextensions = [\"...\"] # e.g. \"cc\"\n\n# Properties to fulfill the template.\n# For a defined key-value pair, you can use {{props[\"key\"]}} in the header template, which will be\n# substituted with the corresponding value.\n[properties]\ninceptionYear = 2023\n\n# There are also preset attributes that can be used in the header template (no need to surround them with `props[]`).:\n# * 'attrs.filename' is the current file name, like: pom.xml.\n# * 'attrs.disk_file_created_year'\n\n# Options to configure Git features.\n[git]\n# If enabled, do not process files that are ignored by Git; possible value: ['auto', 'enable', 'disable']\n# 'auto' means this feature tries to be enabled with:\n#   * gix - if `basedir` is in a Git repository.\n#   * ignore crate's gitignore rules - if `basedir` is not in a Git repository.\n# 'enable' means always enabled with gix; failed if it is impossible.\n# default: 'auto'\nignore = 'auto'\n# If enabled, populate file attrs determinated by Git; possible value: ['auto', 'enable', 'disable']\n# Attributes contains:\n#   * 'attrs.git_file_created_year'\n#   * 'attrs.git_file_modified_year'\n# 'auto' means this feature tries to be enabled with:\n#   * gix - if `basedir` is in a Git repository.\n# 'enable' means always enabled with gix; failed if it is impossible.\n# default: 'disable'\nattrs = 'disable'\n```\n\n### Header style file\n\n```toml\n# [REQUIRED] The name of this header.\n[my_header_style]\n\n# The first fixed line of this header. Default to none.\nfirstLine = \"...\"\n\n# The last fixed line of this header. Default to none.\nendLine = \"...\"\n\n# The characters to prepend before each license header lines. Default to empty.\nbeforeEachLine = \"...\"\n\n# The characters to append after each license header lines. Default to empty.\nafterEachLine = \"...\"\n\n# Only for multi-line comments: specify if blank lines are allowed.\n# Default to false because most of the time, a header has some characters on each line.\nallowBlankLines = false\n\n# Specify whether this is a multi-line comment style or not.\n#\n# A multi-line comment style is equivalent to what we have in Java, where a first line and line will delimit\n# a whole multi-line comment section.\n#\n# A style that is not multi-line is usually repeating in each line the characters before and after each line\n# to delimit a one-line comment.\n#\n# Defaulut to true.\nmultipleLines = true\n\n# Only for non multi-line comments: specify if some spaces should be added after the header line and before\n# the `afterEachLine` characters so that all the lines are aligned.\n#\n# Default to false.\npadLines = false\n\n# A regex to define a first line in a file that should be skipped and kept untouched, like the XML declaration\n# at the top of XML documents.\n#\n# Default to none.\nskipLinePattern = \"...\"\n\n# [REQUIRED] The regex used to detect the start of a header section or line.\nfirstLineDetectionPattern = \"...\"\n\n# [REQUIRED] The regex used to detect the end of a header section or line.\nlastLineDetectionPattern = \"...\"\n```\n\n## License\n\n[Apache License 2.0](LICENSE)\n\n## History\n\nThis software is originally from [license-maven-plugin](https://github.com/mathieucarbou/license-maven-plugin), with an initial motivation to bring it beyond a Maven plugin. The core abstractions like `Document`, `Header`, and `HeaderParser` are originally copied from the license-maven-plugin sources under the terms of Apache License 2.0.\n\nLater, when I started focusing on the Docker image's size and integration with Git, I found that Rust is better than Java (GraalVM Native Image) for this purpose. So, I rewrote the core logic in Rust while keeping a slim image. (The old Java implementation is achieved at the [archive-native-image](https://github.com/korandoru/hawkeye/tree/archive-native-image) branch)\n"
  },
  {
    "path": "action.yml",
    "content": "# Copyright 2024 tison <wander4096@gmail.com>\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nname: HawkEye Action\ndescription: 'Check, format, or remove license headers.'\nauthor: 'tison <wander4096@gmail.com>'\nbranding:\n  icon: 'code'\n  color: 'blue'\n\ninputs:\n  config:\n    description: The configuration file\n    required: false\n    default: licenserc.toml\n  mode:\n    description: |\n      Which mode License Eye should be run in. Choices are `check`, `format`, or `remove`. The\n      default value is `check`.\n    required: false\n    default: check\n  args:\n    description: |\n      Other arguments to be passed to the command, such as `--dry-run`, `--fail-if-unknown`,\n      `--fail-if-updated`, etc. The default value is empty.\n    required: false\n\nruns:\n  using: \"composite\"\n  steps:\n    - name: Build HawkEye CLI\n      run: cargo install --path cli\n      shell: bash\n      working-directory: ${{ github.action_path }}\n    - shell: bash\n      run: hawkeye ${{ inputs.mode }} --config ${{ inputs.config }} ${{ inputs.args }}\n"
  },
  {
    "path": "cli/Cargo.toml",
    "content": "# Copyright 2024 tison <wander4096@gmail.com>\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n[package]\nname = \"hawkeye\"\ndescription = \"Simple license header checker and formatter, in multiple distribution forms.\"\nversion.workspace = true\nedition.workspace = true\nauthors.workspace = true\nreadme.workspace = true\nlicense.workspace = true\nrepository.workspace = true\n\n[dependencies]\nclap = { workspace = true }\nconst_format = { workspace = true }\nexn = { workspace = true }\nhawkeye-fmt = { workspace = true }\nlog = { workspace = true }\nlogforth = { version = \"0.29.1\", features = [\"starter-log\"] }\nserde = { version = \"1.0.219\", features = [\"derive\"] }\nserde_json = { version = \"1.0.143\" }\nshadow-rs = { workspace = true }\n\n[build-dependencies]\nbuild-data = { workspace = true }\nshadow-rs = { workspace = true, features = [\"build\"] }\ngix-discover = { version = \"0.47.0\" }\n"
  },
  {
    "path": "cli/build.rs",
    "content": "// Copyright 2024 tison <wander4096@gmail.com>\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nuse std::collections::BTreeSet;\nuse std::env;\nuse std::path::Path;\n\nuse build_data::format_timestamp;\nuse build_data::get_source_time;\nuse shadow_rs::CARGO_METADATA;\nuse shadow_rs::CARGO_TREE;\n\nfn configure_rerun_if_head_commit_changed() {\n    let mut current = Path::new(env!(\"CARGO_MANIFEST_DIR\")).to_path_buf();\n\n    // skip if no valid-looking git repository could be found\n    while let Ok((dir, _)) = gix_discover::upwards(current.as_path()) {\n        match dir {\n            gix_discover::repository::Path::Repository(git_dir) => {\n                unreachable!(\n                    \"build.rs should never be placed in a git bare repository: {}\",\n                    git_dir.display()\n                );\n            }\n            gix_discover::repository::Path::WorkTree(work_dir) => {\n                let git_refs_heads = work_dir.join(\".git/refs/heads\");\n                println!(\"cargo:rerun-if-changed={}\", git_refs_heads.display());\n                break;\n            }\n            gix_discover::repository::Path::LinkedWorkTree { work_dir, .. } => {\n                current = work_dir\n                    .parent()\n                    .expect(\"submodule's work_dir must have parent\")\n                    .to_path_buf();\n                continue;\n            }\n        };\n    }\n}\n\nfn main() -> shadow_rs::SdResult<()> {\n    configure_rerun_if_head_commit_changed();\n\n    println!(\n        \"cargo::rustc-env=SOURCE_TIMESTAMP={}\",\n        if let Ok(t) = get_source_time() {\n            format_timestamp(t)?\n        } else {\n            \"\".to_string()\n        }\n    );\n    build_data::set_BUILD_TIMESTAMP();\n\n    // The \"CARGO_WORKSPACE_DIR\" is set manually (not by Rust itself) in Cargo config file, to\n    // solve the problem where the \"CARGO_MANIFEST_DIR\" is not what we want when this repo is\n    // made as a submodule in another repo.\n    let src_path = env::var(\"CARGO_WORKSPACE_DIR\").or_else(|_| env::var(\"CARGO_MANIFEST_DIR\"))?;\n    let out_path = env::var(\"OUT_DIR\")?;\n    shadow_rs::ShadowBuilder::builder()\n        .src_path(src_path)\n        .out_path(out_path)\n        // exclude these two large constants that we don't need\n        .deny_const(BTreeSet::from([CARGO_METADATA, CARGO_TREE]))\n        .build()?;\n    Ok(())\n}\n"
  },
  {
    "path": "cli/src/main.rs",
    "content": "// Copyright 2024 tison <wander4096@gmail.com>\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nuse clap::FromArgMatches;\nuse clap::Subcommand;\nuse logforth::filter::env_filter::EnvFilterBuilder;\n\nuse crate::subcommand::SubCommand;\n\npub mod subcommand;\npub mod version;\n\nfn main() {\n    logforth::starter_log::stderr()\n        .filter(EnvFilterBuilder::from_default_env_or(\"info\").build())\n        .apply();\n\n    let build_info = version::build_info();\n    let command = clap::Command::new(\"hawkeye\")\n        .subcommand_required(true)\n        .about(build_info.description)\n        .version(build_info.version)\n        .long_version(version::version());\n    let command = SubCommand::augment_subcommands(command);\n    let args = command.get_matches();\n    match SubCommand::from_arg_matches(&args) {\n        Ok(cmd) => cmd.run(),\n        Err(e) => e.exit(),\n    }\n}\n"
  },
  {
    "path": "cli/src/subcommand.rs",
    "content": "// Copyright 2024 tison <wander4096@gmail.com>\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nuse std::io::Write;\nuse std::path::Path;\nuse std::path::PathBuf;\n\nuse clap::Args;\nuse clap::Parser;\nuse exn::Result;\nuse hawkeye_fmt::document::Document;\nuse hawkeye_fmt::error::Error;\nuse hawkeye_fmt::header::matcher::HeaderMatcher;\nuse hawkeye_fmt::processor::check_license_header;\nuse hawkeye_fmt::processor::Callback;\nuse serde::Serialize;\n\n#[derive(Parser)]\npub enum SubCommand {\n    #[clap(name = \"check\", about = \"check license header\")]\n    Check(CommandCheck),\n    #[clap(name = \"format\", about = \"format license header\")]\n    Format(CommandFormat),\n    #[clap(name = \"remove\", about = \"remove license header\")]\n    Remove(CommandRemove),\n}\n\n#[derive(Args)]\nstruct SharedOptions {\n    #[arg(long, help = \"path to the config file\")]\n    config: Option<PathBuf>,\n    #[arg(\n        short = 'o',\n        long = \"output\",\n        help = \"Write the output as JSON object to the specified file\"\n    )]\n    output_file: Option<PathBuf>,\n    #[arg(long, help = \"fail if process unknown files\", default_value_t = false)]\n    fail_if_unknown: bool,\n}\n\n#[derive(Args)]\nstruct SharedEditOptions {\n    #[arg(long, help = \"whether update file in place\", default_value_t = false)]\n    dry_run: bool,\n    #[arg(\n        long,\n        help = \"whether to exit with non-zero code if files updated\",\n        action = clap::ArgAction::Set,\n        default_value_t = true\n    )]\n    fail_if_updated: bool,\n}\n\nimpl SubCommand {\n    pub fn run(self) {\n        match self {\n            SubCommand::Check(cmd) => cmd.run(),\n            SubCommand::Format(cmd) => cmd.run(),\n            SubCommand::Remove(cmd) => cmd.run(),\n        }\n    }\n}\n\n#[derive(Parser)]\npub struct CommandCheck {\n    #[command(flatten)]\n    shared: SharedOptions,\n    #[arg(\n        long,\n        help = \"whether to exit with non-zero code if missing headers\",\n        action = clap::ArgAction::Set,\n        default_value_t = true\n    )]\n    fail_if_missing: bool,\n}\n\n#[derive(Serialize)]\nstruct CheckContext {\n    unknown: Vec<String>,\n    missing: Vec<String>,\n}\n\nimpl Callback for CheckContext {\n    fn on_unknown(&mut self, path: &Path) {\n        self.unknown.push(path.display().to_string());\n    }\n\n    fn on_matched(&mut self, _: &HeaderMatcher, _: Document) -> Result<(), Error> {\n        Ok(())\n    }\n\n    fn on_not_matched(&mut self, _: &HeaderMatcher, document: Document) -> Result<(), Error> {\n        self.missing.push(document.filepath.display().to_string());\n        Ok(())\n    }\n}\n\nimpl CommandCheck {\n    fn run(self) {\n        let config = self.shared.config.unwrap_or_else(default_config);\n        let mut context = CheckContext {\n            unknown: vec![],\n            missing: vec![],\n        };\n        check_license_header(config, &mut context).unwrap();\n\n        let mut failed = check_unknown_files(&context.unknown, self.shared.fail_if_unknown);\n        if !context.missing.is_empty() {\n            log::error!(\"Found header missing in files: {:?}\", context.missing);\n            failed |= self.fail_if_missing;\n        }\n        if let Some(f) = self.shared.output_file {\n            write_to_file(&f, &context);\n        }\n        if failed {\n            std::process::exit(1);\n        }\n        log::info!(\"No missing header file has been found.\");\n    }\n}\n\n#[derive(Parser)]\npub struct CommandFormat {\n    #[command(flatten)]\n    shared: SharedOptions,\n    #[command(flatten)]\n    shared_edit: SharedEditOptions,\n}\n\n#[derive(Serialize)]\nstruct FormatContext {\n    dry_run: bool,\n    unknown: Vec<String>,\n    updated: Vec<String>,\n}\n\nimpl Callback for FormatContext {\n    fn on_unknown(&mut self, path: &Path) {\n        self.unknown.push(path.display().to_string());\n    }\n\n    fn on_matched(&mut self, _: &HeaderMatcher, _: Document) -> Result<(), Error> {\n        Ok(())\n    }\n\n    fn on_not_matched(&mut self, header: &HeaderMatcher, mut doc: Document) -> Result<(), Error> {\n        if doc.header_detected() {\n            doc.remove_header();\n            doc.update_header(header)?;\n            self.updated\n                .push(format!(\"{}=replaced\", doc.filepath.display()));\n        } else {\n            doc.update_header(header)?;\n            self.updated\n                .push(format!(\"{}=added\", doc.filepath.display()));\n        }\n\n        if self.dry_run {\n            let mut extension = doc.filepath.extension().unwrap_or_default().to_os_string();\n            extension.push(\".formatted\");\n            let copied = doc.filepath.with_extension(extension);\n            doc.save(Some(&copied))\n        } else {\n            doc.save(None)\n        }\n    }\n}\n\nimpl CommandFormat {\n    fn run(self) {\n        let config = self.shared.config.unwrap_or_else(default_config);\n        let mut context = FormatContext {\n            dry_run: self.shared_edit.dry_run,\n            unknown: vec![],\n            updated: vec![],\n        };\n        check_license_header(config, &mut context).unwrap();\n\n        let mut failed = check_unknown_files(&context.unknown, self.shared.fail_if_unknown);\n        if !context.updated.is_empty() {\n            log::info!(\n                \"Updated header for files (dryRun={}): {:?}\",\n                self.shared_edit.dry_run,\n                context.updated\n            );\n            failed |= self.shared_edit.fail_if_updated;\n        }\n        if let Some(f) = self.shared.output_file {\n            write_to_file(&f, &context);\n        }\n        if failed {\n            std::process::exit(1);\n        }\n        log::info!(\"All files have proper header.\");\n    }\n}\n\n#[derive(Parser)]\npub struct CommandRemove {\n    #[command(flatten)]\n    shared: SharedOptions,\n    #[command(flatten)]\n    shared_edit: SharedEditOptions,\n}\n\n#[derive(Serialize)]\nstruct RemoveContext {\n    dry_run: bool,\n    unknown: Vec<String>,\n    removed: Vec<String>,\n}\n\nimpl RemoveContext {\n    fn remove(&mut self, doc: &mut Document) -> Result<(), Error> {\n        if !doc.header_detected() {\n            return Ok(());\n        }\n        doc.remove_header();\n        self.removed\n            .push(format!(\"{}=removed\", doc.filepath.display()));\n        if self.dry_run {\n            let mut extension = doc.filepath.extension().unwrap_or_default().to_os_string();\n            extension.push(\".removed\");\n            let copied = doc.filepath.with_extension(extension);\n            doc.save(Some(&copied))\n        } else {\n            doc.save(None)\n        }\n    }\n}\n\nimpl Callback for RemoveContext {\n    fn on_unknown(&mut self, path: &Path) {\n        self.unknown.push(path.display().to_string());\n    }\n\n    fn on_matched(&mut self, _: &HeaderMatcher, mut doc: Document) -> Result<(), Error> {\n        self.remove(&mut doc)\n    }\n\n    fn on_not_matched(&mut self, _: &HeaderMatcher, mut doc: Document) -> Result<(), Error> {\n        self.remove(&mut doc)\n    }\n}\n\nimpl CommandRemove {\n    fn run(self) {\n        let config = self.shared.config.unwrap_or_else(default_config);\n        let mut context = RemoveContext {\n            dry_run: self.shared_edit.dry_run,\n            unknown: vec![],\n            removed: vec![],\n        };\n        check_license_header(config, &mut context).unwrap();\n\n        let mut failed = check_unknown_files(&context.unknown, self.shared.fail_if_unknown);\n        if !context.removed.is_empty() {\n            log::info!(\n                \"Removed header for files (dryRun={}): {:?}\",\n                self.shared_edit.dry_run,\n                context.removed\n            );\n            failed |= self.shared_edit.fail_if_updated;\n        }\n        if let Some(f) = self.shared.output_file {\n            write_to_file(&f, &context);\n        }\n        if failed {\n            std::process::exit(1);\n        }\n        log::info!(\"No file has been removed header.\");\n    }\n}\n\nfn write_to_file(path: &Path, result: impl Serialize) {\n    fn do_write_to_file(path: &Path, result: impl Serialize) -> std::io::Result<()> {\n        let mut file = std::fs::File::create(path)?;\n        serde_json::to_writer(&file, &result)?;\n        file.flush()\n    }\n\n    do_write_to_file(path, result)\n        .unwrap_or_else(|err| panic!(\"failed to write output to file {}: {}\", path.display(), err));\n}\n\nfn check_unknown_files(unknown: &[String], fail_if_unknown: bool) -> bool {\n    if !unknown.is_empty() {\n        if fail_if_unknown {\n            log::error!(\"Processing unknown files: {unknown:?}\");\n            return true;\n        } else {\n            log::warn!(\"Processing unknown files: {unknown:?}\");\n        }\n    }\n    false\n}\n\nfn default_config() -> PathBuf {\n    let candidates = [\n        PathBuf::from(\"licenserc.toml\"),\n        PathBuf::from(\".licenserc.toml\"),\n    ];\n\n    for path in &candidates {\n        if path.exists() {\n            return path.clone();\n        }\n    }\n\n    panic!(\n        \"cannot find config file in any of the default locations: {:?}\",\n        candidates.iter().map(|p| p.display()).collect::<Vec<_>>()\n    );\n}\n"
  },
  {
    "path": "cli/src/version.rs",
    "content": "// Copyright 2024 tison <wander4096@gmail.com>\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nuse shadow_rs::shadow;\n\nshadow!(build);\n\n#[derive(Clone, Debug, PartialEq)]\npub struct BuildInfo {\n    pub branch: &'static str,\n    pub commit: &'static str,\n    pub commit_short: &'static str,\n    pub clean: bool,\n    pub description: &'static str,\n    pub source_time: &'static str,\n    pub build_time: &'static str,\n    pub rustc: &'static str,\n    pub target: &'static str,\n    pub version: &'static str,\n}\n\npub const fn build_info() -> BuildInfo {\n    BuildInfo {\n        branch: build::BRANCH,\n        commit: build::COMMIT_HASH,\n        commit_short: build::SHORT_COMMIT,\n        clean: build::GIT_CLEAN,\n        description: build::PKG_DESCRIPTION,\n        source_time: env!(\"SOURCE_TIMESTAMP\"),\n        build_time: env!(\"BUILD_TIMESTAMP\"),\n        rustc: build::RUST_VERSION,\n        target: build::BUILD_TARGET,\n        version: build::PKG_VERSION,\n    }\n}\n\npub const fn version() -> &'static str {\n    const BUILD_INFO: BuildInfo = build_info();\n\n    const_format::formatcp!(\n        \"\\nversion: {}\\nbranch: {}\\ncommit: {}\\nclean: {}\\nsource_time: {}\\nbuild_time: {}\\nrustc: {}\\ntarget: {}\",\n        BUILD_INFO.version,\n        BUILD_INFO.branch,\n        BUILD_INFO.commit,\n        BUILD_INFO.clean,\n        BUILD_INFO.source_time,\n        BUILD_INFO.build_time,\n        BUILD_INFO.rustc,\n        BUILD_INFO.target,\n    )\n}\n"
  },
  {
    "path": "fmt/Cargo.toml",
    "content": "# Copyright 2024 tison <wander4096@gmail.com>\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n[package]\nname = \"hawkeye-fmt\"\ndescription = \"The formatter library for hawkeye cli.\"\nversion.workspace = true\nedition.workspace = true\nauthors.workspace = true\nreadme.workspace = true\nlicense.workspace = true\nrepository.workspace = true\n\n[dependencies]\nexn = { workspace = true }\ngix = { version = \"0.79.0\", default-features = false, features = [\n  \"blob-diff\",\n  \"excludes\",\n  \"status\",\n  \"parallel\",\n] }\njiff = { version = \"0.2.14\" }\nignore = { version = \"0.4.23\" }\nlog = { workspace = true }\nminijinja = { version = \"2.15.1\" }\nregex = { version = \"1.11.1\" }\nserde = { version = \"1.0.216\", features = [\"derive\"] }\ntoml = { workspace = true }\nwalkdir = { version = \"2.5.0\" }\n"
  },
  {
    "path": "fmt/src/config/mod.rs",
    "content": "// Copyright 2024 tison <wander4096@gmail.com>\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nuse std::collections::HashMap;\nuse std::collections::HashSet;\nuse std::hash::Hash;\nuse std::hash::Hasher;\nuse std::path::PathBuf;\n\nuse serde::de::Error;\nuse serde::Deserialize;\nuse serde::Deserializer;\nuse toml::Value;\n\nuse crate::default_true;\n\n#[derive(Debug, Clone, Default, Deserialize)]\n#[serde(default, deny_unknown_fields, rename_all = \"camelCase\")]\npub struct Config {\n    #[serde(default = \"default_cwd\")]\n    pub base_dir: PathBuf,\n\n    pub inline_header: Option<String>,\n    pub header_path: Option<String>,\n\n    #[serde(default = \"default_true\")]\n    pub strict_check: bool,\n    #[serde(default = \"default_true\")]\n    pub use_default_excludes: bool,\n    #[serde(default = \"default_true\")]\n    pub use_default_mapping: bool,\n    #[serde(default = \"default_keywords\")]\n    pub keywords: Vec<String>,\n\n    pub includes: Vec<String>,\n    pub excludes: Vec<String>,\n\n    #[serde(deserialize_with = \"de_properties\")]\n    pub properties: HashMap<String, String>,\n    #[serde(deserialize_with = \"de_mapping\")]\n    pub mapping: HashSet<Mapping>,\n\n    pub git: Git,\n\n    pub additional_headers: Vec<String>,\n}\n\n#[derive(Debug, Clone, Copy, Deserialize)]\npub struct Git {\n    pub attrs: FeatureGate,\n    pub ignore: FeatureGate,\n}\n\nimpl Default for Git {\n    fn default() -> Self {\n        Git {\n            attrs: FeatureGate::Disable, // expensive\n            ignore: FeatureGate::Auto,\n        }\n    }\n}\n\n#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Deserialize)]\n#[serde(rename_all = \"snake_case\")]\npub enum FeatureGate {\n    /// Determinate whether turn on the feature.\n    Auto,\n    /// Force enable the feature.\n    Enable,\n    /// Force disable the feature.\n    Disable,\n}\n\nimpl FeatureGate {\n    pub fn is_enable(&self) -> bool {\n        match self {\n            FeatureGate::Auto => false,\n            FeatureGate::Enable => true,\n            FeatureGate::Disable => false,\n        }\n    }\n\n    pub fn is_disable(&self) -> bool {\n        match self {\n            FeatureGate::Auto => false,\n            FeatureGate::Enable => false,\n            FeatureGate::Disable => true,\n        }\n    }\n\n    pub fn is_auto(&self) -> bool {\n        match self {\n            FeatureGate::Auto => true,\n            FeatureGate::Enable => false,\n            FeatureGate::Disable => false,\n        }\n    }\n}\n\n#[derive(Debug, Clone)]\npub enum Mapping {\n    Filename {\n        pattern: String,\n        header_type: String,\n    },\n    Extension {\n        pattern: String,\n        header_type: String,\n    },\n}\n\nimpl PartialEq for Mapping {\n    fn eq(&self, other: &Self) -> bool {\n        match (self, other) {\n            (Mapping::Filename { pattern: p1, .. }, Mapping::Filename { pattern: p2, .. }) => {\n                p1 == p2\n            }\n            (Mapping::Extension { pattern: p1, .. }, Mapping::Extension { pattern: p2, .. }) => {\n                p1 == p2\n            }\n            _ => false,\n        }\n    }\n}\n\nimpl Eq for Mapping {}\n\nimpl Hash for Mapping {\n    fn hash<H: Hasher>(&self, state: &mut H) {\n        state.write(match self {\n            Mapping::Filename { pattern, .. } => pattern.as_bytes(),\n            Mapping::Extension { pattern, .. } => pattern.as_bytes(),\n        });\n    }\n}\n\nimpl Mapping {\n    pub fn header_type(&self, filename: &str) -> Option<String> {\n        let filename = filename.to_lowercase();\n        match self {\n            Mapping::Filename {\n                header_type,\n                pattern,\n            } => {\n                let pattern = pattern.to_lowercase();\n                (filename == pattern).then(|| header_type.clone())\n            }\n            Mapping::Extension {\n                header_type,\n                pattern,\n            } => {\n                let pattern = format!(\".{pattern}\").to_lowercase();\n                filename.ends_with(&pattern).then(|| header_type.clone())\n            }\n        }\n    }\n}\n\nfn default_cwd() -> PathBuf {\n    \".\".into()\n}\n\nfn default_keywords() -> Vec<String> {\n    vec![\"copyright\".to_string()]\n}\n\nfn de_properties<'de, D>(de: D) -> Result<HashMap<String, String>, D::Error>\nwhere\n    D: Deserializer<'de>,\n{\n    HashMap::<String, Value>::deserialize(de)?\n        .into_iter()\n        .map(|(k, v)| {\n            let v = match v {\n                Value::String(v) => Ok(v),\n                Value::Integer(v) => Ok(v.to_string()),\n                Value::Float(v) => Ok(v.to_string()),\n                Value::Boolean(v) => Ok(v.to_string()),\n                Value::Datetime(v) => Ok(v.to_string()),\n                Value::Array(_) => Err(Error::custom(\"array cannot be property value\")),\n                Value::Table(_) => Err(Error::custom(\"table cannot be property value\")),\n            }?;\n            Ok((k, v))\n        })\n        .collect()\n}\n\nfn de_mapping<'de, D>(de: D) -> Result<HashSet<Mapping>, D::Error>\nwhere\n    D: Deserializer<'de>,\n{\n    #[derive(Debug, Default, Clone, Deserialize)]\n    #[serde(default)]\n    struct MappingModel {\n        extensions: Vec<String>,\n        filenames: Vec<String>,\n    }\n\n    let mappings = HashMap::<String, MappingModel>::deserialize(de)?;\n    let mut set = HashSet::new();\n    for (header_type, model) in mappings {\n        for pattern in model.extensions {\n            set.insert(Mapping::Extension {\n                pattern,\n                header_type: header_type.clone(),\n            });\n        }\n        for pattern in model.filenames {\n            set.insert(Mapping::Filename {\n                pattern,\n                header_type: header_type.clone(),\n            });\n        }\n    }\n    Ok(set)\n}\n"
  },
  {
    "path": "fmt/src/document/defaults.toml",
    "content": "# Copyright 2024 tison <wander4096@gmail.com>\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n[ACTIONSCRIPT]\npattern = \"as\"\nheaderType = \"SLASHSTAR_STYLE\"\nextension = true\nfilename = false\n\n[ADA_BODY]\npattern = \"adb\"\nheaderType = \"DOUBLEDASHES_STYLE\"\nextension = true\nfilename = false\n\n[ADA_SPEC]\npattern = \"ads\"\nheaderType = \"DOUBLEDASHES_STYLE\"\nextension = true\nfilename = false\n\n[ASCII_DOC]\npattern = \"adoc\"\nheaderType = \"ASCIIDOC_STYLE\"\nextension = true\nfilename = false\n\n[ASP]\npattern = \"asp\"\nheaderType = \"ASP\"\nextension = true\nfilename = false\n\n[ASPECTJ]\npattern = \"aj\"\nheaderType = \"SLASHSTAR_STYLE\"\nextension = true\nfilename = false\n\n[ASSEMBLER]\npattern = \"asm\"\nheaderType = \"SEMICOLON_STYLE\"\nextension = true\nfilename = false\n\n[C]\npattern = \"c\"\nheaderType = \"SLASHSTAR_STYLE\"\nextension = true\nfilename = false\n\n[CC]\npattern = \"cc\"\nheaderType = \"SLASHSTAR_STYLE\"\nextension = true\nfilename = false\n\n[CLOJURE]\npattern = \"clj\"\nheaderType = \"SEMICOLON_STYLE\"\nextension = true\nfilename = false\n\n[CLOJURESCRIPT]\npattern = \"cljs\"\nheaderType = \"SEMICOLON_STYLE\"\nextension = true\nfilename = false\n\n[CMAKELISTS]\npattern = \"CMakeLists.txt\"\nheaderType = \"SCRIPT_STYLE\"\nextension = false\nfilename = true\n\n[COLDFUSION_COMPONENT]\npattern = \"cfc\"\nheaderType = \"DYNASCRIPT3_STYLE\"\nextension = true\nfilename = false\n\n[COLDFUSION_ML]\npattern = \"cfm\"\nheaderType = \"DYNASCRIPT3_STYLE\"\nextension = true\nfilename = false\n\n[CONTAINERFILE]\npattern = \"Containerfile\"\nheaderType = \"SCRIPT_STYLE\"\nextension = false\nfilename = true\n\n[CPP]\npattern = \"cpp\"\nheaderType = \"SLASHSTAR_STYLE\"\nextension = true\nfilename = false\n\n[CSHARP]\npattern = \"cs\"\nheaderType = \"SLASHSTAR_STYLE\"\nextension = true\nfilename = false\n\n[CSS]\npattern = \"css\"\nheaderType = \"SLASHSTAR_STYLE\"\nextension = true\nfilename = false\n\n[DELPHI]\npattern = \"pas\"\nheaderType = \"BRACESSTAR_STYLE\"\nextension = true\nfilename = false\n\n[DOCKERFILE]\npattern = \"Dockerfile\"\nheaderType = \"SCRIPT_STYLE\"\nextension = false\nfilename = true\n\n[DOXIA_APT]\npattern = \"apt\"\nheaderType = \"DOUBLETILDE_STYLE\"\nextension = true\nfilename = false\n\n[DOXIA_FAQ]\npattern = \"fml\"\nheaderType = \"XML_STYLE\"\nextension = true\nfilename = false\n\n[DTD]\npattern = \"dtd\"\nheaderType = \"XML_STYLE\"\nextension = true\nfilename = false\n\n[EDITORCONFIG]\npattern = \".editorconfig\"\nheaderType = \"SCRIPT_STYLE\"\nextension = false\nfilename = true\n\n[EIFFEL]\npattern = \"e\"\nheaderType = \"DOUBLEDASHES_STYLE\"\nextension = true\nfilename = false\n\n[ERLANG]\npattern = \"erl\"\nheaderType = \"PERCENT3_STYLE\"\nextension = true\nfilename = false\n\n[ERLANG_HEADER]\npattern = \"hrl\"\nheaderType = \"PERCENT3_STYLE\"\nextension = true\nfilename = false\n\n[FORTRAN]\npattern = \"f\"\nheaderType = \"EXCLAMATION_STYLE\"\nextension = true\nfilename = false\n\n[FREEMARKER]\npattern = \"ftl\"\nheaderType = \"FTL\"\nextension = true\nfilename = false\n\n[GO]\npattern = \"go\"\nheaderType = \"SLASHSTAR_STYLE\"\nextension = true\nfilename = false\n\n[GO_MOD]\npattern = \"go.mod\"\nheaderType = \"DOUBLESLASH_STYLE\"\nextension = false\nfilename = true\n\n[GRADLE]\npattern = \"gradle\"\nheaderType = \"SLASHSTAR_STYLE\"\nextension = true\nfilename = false\n\n[GROOVY]\npattern = \"groovy\"\nheaderType = \"SLASHSTAR_STYLE\"\nextension = true\nfilename = false\n\n[GSP]\npattern = \"GSP\"\nheaderType = \"XML_STYLE\"\nextension = true\nfilename = false\n\n[H]\npattern = \"h\"\nheaderType = \"SLASHSTAR_STYLE\"\nextension = true\nfilename = false\n\n[HH]\npattern = \"hh\"\nheaderType = \"SLASHSTAR_STYLE\"\nextension = true\nfilename = false\n\n[HPP]\npattern = \"hpp\"\nheaderType = \"SLASHSTAR_STYLE\"\nextension = true\nfilename = false\n\n[HAML]\npattern = \"haml\"\nheaderType = \"HAML_STYLE\"\nextension = true\nfilename = false\n\n[HTM]\npattern = \"htm\"\nheaderType = \"XML_STYLE\"\nextension = true\nfilename = false\n\n[HTML]\npattern = \"html\"\nheaderType = \"XML_STYLE\"\nextension = true\nfilename = false\n\n[JAVA]\npattern = \"java\"\nheaderType = \"SLASHSTAR_STYLE\"\nextension = true\nfilename = false\n\n[JAVAFX]\npattern = \"fx\"\nheaderType = \"SLASHSTAR_STYLE\"\nextension = true\nfilename = false\n\n[JAVASCRIPT]\npattern = \"js\"\nheaderType = \"SLASHSTAR_STYLE\"\nextension = true\nfilename = false\n\n[COMMONJS]\npattern = \"cjs\"\nheaderType = \"SLASHSTAR_STYLE\"\nextension = true\nfilename = false\n\n[JSP]\npattern = \"jsp\"\nheaderType = \"DYNASCRIPT_STYLE\"\nextension = true\nfilename = false\n\n[JSPX]\npattern = \"jspx\"\nheaderType = \"XML_STYLE\"\nextension = true\nfilename = false\n\n[JSX]\npattern = \"jsx\"\nheaderType = \"SLASHSTAR_STYLE\"\nextension = true\nfilename = false\n\n[KML]\npattern = \"kml\"\nheaderType = \"XML_STYLE\"\nextension = true\nfilename = false\n\n[KOTLIN]\npattern = \"kt\"\nheaderType = \"SLASHSTAR_STYLE\"\nextension = true\nfilename = false\n\n[KOTLIN_SCRIPT]\npattern = \"kts\"\nheaderType = \"SLASHSTAR_STYLE\"\nextension = true\nfilename = false\n\n[LISP]\npattern = \"el\"\nheaderType = \"EXCLAMATION3_STYLE\"\nextension = true\nfilename = false\n\n[LUA]\npattern = \"lua\"\nheaderType = \"LUA\"\nextension = true\nfilename = false\n\n[MAKEFILE]\npattern = \"Makefile\"\nheaderType = \"SCRIPT_STYLE\"\nextension = true\nfilename = true\n\n[MOONBIT]\npattern = \"mbt\"\nheaderType = \"DOUBLESLASH_STYLE\"\nextension = true\nfilename = false\n\n[MUSTACHE]\npattern = \"mustache\"\nheaderType = \"MUSTACHE_STYLE\"\nextension = true\nfilename = false\n\n[MVEL]\npattern = \"mv\"\nheaderType = \"MVEL_STYLE\"\nextension = true\nfilename = false\n\n[MXML]\npattern = \"mxml\"\nheaderType = \"XML_STYLE\"\nextension = true\nfilename = false\n\n[PERL]\npattern = \"pl\"\nheaderType = \"SCRIPT_STYLE\"\nextension = true\nfilename = false\n\n[PERL_MODULE]\npattern = \"pm\"\nheaderType = \"SCRIPT_STYLE\"\nextension = true\nfilename = false\n\n[PKL]\npattern = \"pkl\"\nheaderType = \"LINE_BLOCK_STYLE\"\nextension = true\nfilename = false\n\n[PKL_PROJECT]\npattern = \"PklProject\"\nheaderType = \"LINE_BLOCK_STYLE\"\nextension = false\nfilename = true\n\n[PHP]\npattern = \"php\"\nheaderType = \"PHP\"\nextension = true\nfilename = false\n\n[POM]\npattern = \"pom\"\nheaderType = \"XML_STYLE\"\nextension = true\nfilename = false\n\n[PROPERTIES]\npattern = \"properties\"\nheaderType = \"SCRIPT_STYLE\"\nextension = true\nfilename = false\n\n[PROTO]\npattern = \"proto\"\nheaderType = \"SLASHSTAR_STYLE\"\nextension = true\nfilename = false\n\n[PYTHON]\npattern = \"py\"\nheaderType = \"SCRIPT_STYLE\"\nextension = true\nfilename = false\n\n[PYTHON_STUBS]\npattern = \"pyi\"\nheaderType = \"SCRIPT_STYLE\"\nextension = true\nfilename = false\n\n[RUBY]\npattern = \"rb\"\nheaderType = \"SCRIPT_STYLE\"\nextension = true\nfilename = false\n\n[RUST]\npattern = \"rs\"\nheaderType = \"DOUBLESLASH_STYLE\"\nextension = true\nfilename = false\n\n[SCALA]\npattern = \"scala\"\nheaderType = \"SLASHSTAR_STYLE\"\nextension = true\nfilename = false\n\n[SCAML]\npattern = \"scaml\"\nheaderType = \"HAML_STYLE\"\nextension = true\nfilename = false\n\n[SCSS]\npattern = \"scss\"\nheaderType = \"SLASHSTAR_STYLE\"\nextension = true\nfilename = false\n\n[SHELL]\npattern = \"sh\"\nheaderType = \"SCRIPT_STYLE\"\nextension = true\nfilename = false\n\n[SPRING_FACTORIES]\npattern = \"spring.factories\"\nheaderType = \"SCRIPT_STYLE\"\nextension = false\nfilename = false\n\n[SVELTE]\npattern = \"svelte\"\nheaderType = \"XML_STYLE\"\nextension = true\nfilename = false\n\n[SQL]\npattern = \"sql\"\nheaderType = \"DOUBLEDASHES_STYLE\"\nextension = true\nfilename = false\n\n[TAGX]\npattern = \"tagx\"\nheaderType = \"XML_STYLE\"\nextension = true\nfilename = false\n\n[TEX_CLASS]\npattern = \"cls\"\nheaderType = \"PERCENT_STYLE\"\nextension = true\nfilename = false\n\n[TEX_STYLE]\npattern = \"sty\"\nheaderType = \"PERCENT_STYLE\"\nextension = true\nfilename = false\n\n[TEX]\npattern = \"tex\"\nheaderType = \"PERCENT_STYLE\"\nextension = true\nfilename = false\n\n[TLD]\npattern = \"tld\"\nheaderType = \"XML_STYLE\"\nextension = true\nfilename = false\n\n[TOML]\npattern = \"toml\"\nheaderType = \"SCRIPT_STYLE\"\nextension = true\nfilename = false\n\n[TS]\npattern = \"ts\"\nheaderType = \"SLASHSTAR_STYLE\"\nextension = true\nfilename = false\n\n[TSX]\npattern = \"tsx\"\nheaderType = \"SLASHSTAR_STYLE\"\nextension = true\nfilename = false\n\n[VB]\npattern = \"bas\"\nheaderType = \"HAML_STYLE\"\nextension = true\nfilename = false\n\n[VBA]\npattern = \"vba\"\nheaderType = \"APOSTROPHE_STYLE\"\nextension = true\nfilename = false\n\n[VELOCITY]\npattern = \"vm\"\nheaderType = \"SHARPSTAR_STYLE\"\nextension = true\nfilename = false\n\n[VUE]\npattern = \"vue\"\nheaderType = \"XML_STYLE\"\nextension = true\nfilename = false\n\n[WINDOWS_BATCH]\npattern = \"bat\"\nheaderType = \"BATCH\"\nextension = true\nfilename = false\n\n[WINDOWS_SHELL]\npattern = \"cmd\"\nheaderType = \"BATCH\"\nextension = true\nfilename = false\n\n[WSDL]\npattern = \"wsdl\"\nheaderType = \"XML_STYLE\"\nextension = true\nfilename = false\n\n[XHTML]\npattern = \"xhtml\"\nheaderType = \"XML_STYLE\"\nextension = true\nfilename = false\n\n[XML]\npattern = \"xml\"\nheaderType = \"XML_STYLE\"\nextension = true\nfilename = false\n\n[XSD]\npattern = \"xsd\"\nheaderType = \"XML_STYLE\"\nextension = true\nfilename = false\n\n[XSL]\npattern = \"xsl\"\nheaderType = \"XML_STYLE\"\nextension = true\nfilename = false\n\n[XSLT]\npattern = \"xslt\"\nheaderType = \"XML_STYLE\"\nextension = true\nfilename = false\n\n[YAML]\npattern = \"yaml\"\nheaderType = \"SCRIPT_STYLE\"\nextension = true\nfilename = false\n\n[YML]\npattern = \"yml\"\nheaderType = \"SCRIPT_STYLE\"\nextension = true\nfilename = false\n\n[ZIG]\npattern = \"zig\"\nheaderType = \"DOUBLESLASH_STYLE\"\nextension = true\nfilename = false\n\n[VERILOG]\npattern = \"v\"\nheaderType = \"SLASHSTAR_STYLE\"\nextension = true\nfilename = false\n\n[SYSTEM_VERILOG]\npattern = \"sv\"\nheaderType = \"SLASHSTAR_STYLE\"\nextension = true\nfilename = false\n"
  },
  {
    "path": "fmt/src/document/factory.rs",
    "content": "// Copyright 2024 tison <wander4096@gmail.com>\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nuse std::collections::HashMap;\nuse std::collections::HashSet;\nuse std::fs;\nuse std::path::Path;\nuse std::path::PathBuf;\nuse std::time::SystemTime;\n\nuse exn::OptionExt;\nuse exn::Result;\n\nuse crate::config::Mapping;\nuse crate::document::Attributes;\nuse crate::document::Document;\nuse crate::error::Error;\nuse crate::git::GitFileAttrs;\nuse crate::header::model::HeaderDef;\n\npub struct DocumentFactory {\n    mapping: HashSet<Mapping>,\n    definitions: HashMap<String, HeaderDef>,\n    properties: HashMap<String, String>,\n\n    keywords: Vec<String>,\n    git_file_attrs: HashMap<PathBuf, GitFileAttrs>,\n}\n\nimpl DocumentFactory {\n    pub fn new(\n        mapping: HashSet<Mapping>,\n        definitions: HashMap<String, HeaderDef>,\n        properties: HashMap<String, String>,\n        keywords: Vec<String>,\n        git_file_attrs: HashMap<PathBuf, GitFileAttrs>,\n    ) -> Self {\n        Self {\n            mapping,\n            definitions,\n            properties,\n            keywords,\n            git_file_attrs,\n        }\n    }\n\n    pub fn create_document(&self, filepath: &Path) -> Result<Option<Document>, Error> {\n        let lower_file_name = filepath\n            .file_name()\n            .map(|n| n.to_string_lossy().to_lowercase())\n            .unwrap_or_default();\n        let header_type = self\n            .mapping\n            .iter()\n            .find_map(|m| m.header_type(&lower_file_name))\n            .unwrap_or_else(|| \"unknown\".to_string())\n            .to_lowercase();\n        let header_def = self.definitions.get(&header_type).ok_or_raise(|| {\n            Error::new(format!(\n                \"cannot create document: {}, header type {} not found\",\n                filepath.display(),\n                header_type\n            ))\n        })?;\n\n        let props = self.properties.clone();\n\n        let filemeta = fs::metadata(filepath).ok();\n        let attrs = Attributes {\n            filename: filepath\n                .file_name()\n                .map(|s| s.to_string_lossy().to_string()),\n            disk_file_created_year: filemeta\n                .as_ref()\n                .and_then(|m| m.created().ok())\n                .and_then(file_time_to_year),\n            git_file_created_year: self\n                .git_file_attrs\n                .get(filepath)\n                .and_then(|attrs| git_time_to_year(attrs.created_time)),\n            git_file_modified_year: self\n                .git_file_attrs\n                .get(filepath)\n                .and_then(|attrs| git_time_to_year(attrs.modified_time)),\n            git_authors: self\n                .git_file_attrs\n                .get(filepath)\n                .map(|attrs| attrs.authors.clone())\n                .unwrap_or_default(),\n        };\n\n        Document::new(\n            filepath.to_path_buf(),\n            header_def.clone(),\n            &self.keywords,\n            props,\n            attrs,\n        )\n    }\n}\n\nfn file_time_to_year(time: SystemTime) -> Option<i16> {\n    let ts = jiff::Timestamp::try_from(time).ok()?;\n    Some(ts.to_zoned(jiff::tz::TimeZone::system()).year())\n}\n\nfn git_time_to_year(t: gix::date::Time) -> Option<i16> {\n    let offset = jiff::tz::Offset::from_seconds(t.offset).expect(\"valid offset\");\n    let zoned = jiff::Timestamp::from_second(t.seconds)\n        .expect(\"always valid unix time\")\n        .to_zoned(offset.to_time_zone());\n    Some(zoned.year())\n}\n"
  },
  {
    "path": "fmt/src/document/mod.rs",
    "content": "// Copyright 2024 tison <wander4096@gmail.com>\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nuse std::collections::BTreeSet;\nuse std::collections::HashMap;\nuse std::fs;\nuse std::fs::File;\nuse std::io::BufRead;\nuse std::path::PathBuf;\n\nuse exn::ErrorExt;\nuse exn::Result;\nuse exn::ResultExt;\nuse minijinja::context;\nuse minijinja::Environment;\nuse serde::Deserialize;\nuse serde::Serialize;\n\nuse crate::error::Error;\nuse crate::header::matcher::HeaderMatcher;\nuse crate::header::model::HeaderDef;\nuse crate::header::parser::parse_header;\nuse crate::header::parser::FileContent;\nuse crate::header::parser::HeaderParser;\n\npub mod factory;\npub mod model;\n\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Attributes {\n    pub filename: Option<String>,\n    pub disk_file_created_year: Option<i16>,\n    pub git_file_created_year: Option<i16>,\n    pub git_file_modified_year: Option<i16>,\n    pub git_authors: BTreeSet<String>,\n}\n\n#[derive(Debug)]\npub struct Document {\n    pub filepath: PathBuf,\n\n    header_def: HeaderDef,\n    props: HashMap<String, String>,\n    attrs: Attributes,\n    parser: HeaderParser,\n}\n\nimpl Document {\n    pub fn new(\n        filepath: PathBuf,\n        header_def: HeaderDef,\n        keywords: &[String],\n        props: HashMap<String, String>,\n        attrs: Attributes,\n    ) -> Result<Option<Self>, Error> {\n        match FileContent::new(&filepath) {\n            Ok(content) => Ok(Some(Self {\n                parser: parse_header(content, &header_def, keywords),\n                filepath,\n                header_def,\n                props,\n                attrs,\n            })),\n            Err(err) => {\n                if matches!(err.kind(), std::io::ErrorKind::InvalidData) {\n                    log::debug!(\"skip non-textual file: {}\", filepath.display());\n                    Ok(None)\n                } else {\n                    Err(err.raise().raise(Error::new(format!(\n                        \"cannot create document: {}\",\n                        filepath.display()\n                    ))))\n                }\n            }\n        }\n    }\n\n    pub fn is_unsupported(&self) -> bool {\n        self.header_def.name.eq_ignore_ascii_case(\"unknown\")\n    }\n\n    /// Detected but not necessarily a valid header\n    pub fn header_detected(&self) -> bool {\n        self.parser.end_pos.is_some()\n    }\n\n    /// Detected and valid header\n    pub fn header_matched(\n        &self,\n        header: &HeaderMatcher,\n        strict_check: bool,\n    ) -> Result<bool, Error> {\n        if strict_check {\n            let file_header = {\n                let mut lines = self.read_file_first_lines(header)?.join(\"\\n\");\n                lines.push_str(\"\\n\\n\");\n                lines.replace(\" *\\r?\\n\", \"\\n\")\n            };\n            let expected_header = {\n                let raw_header = header.build_for_definition(&self.header_def);\n                let resolved_header = self.merge_properties(&raw_header)?;\n                resolved_header.replace(\" *\\r?\\n\", \"\\n\")\n            };\n            Ok(file_header.contains(expected_header.as_str()))\n        } else {\n            let file_header = self.read_file_header_on_one_line(header)?;\n            let expected_header = self.merge_properties(header.header_content_one_line())?;\n            Ok(file_header.contains(expected_header.as_str()))\n        }\n    }\n\n    #[track_caller]\n    fn read_file_first_lines(&self, header: &HeaderMatcher) -> Result<Vec<String>, Error> {\n        let make_error = || Error::new(\"cannot read file first line\");\n        let file = File::open(&self.filepath).or_raise(make_error)?;\n        std::io::BufReader::new(file)\n            .lines()\n            .take(header.header_content_lines_count() + 10)\n            .collect::<std::io::Result<Vec<_>>>()\n            .or_raise(make_error)\n    }\n\n    #[track_caller]\n    fn read_file_header_on_one_line(&self, header: &HeaderMatcher) -> Result<String, Error> {\n        let first_lines = self.read_file_first_lines(header)?;\n        let file_header = first_lines\n            .join(\"\")\n            .trim()\n            .replace(self.header_def.first_line.trim(), \"\")\n            .replace(self.header_def.end_line.trim(), \"\")\n            .replace(self.header_def.before_each_line.trim(), \"\")\n            .replace(self.header_def.after_each_line.trim(), \"\")\n            .split_whitespace()\n            .collect();\n        Ok(file_header)\n    }\n\n    pub fn update_header(&mut self, header: &HeaderMatcher) -> Result<(), Error> {\n        let header_str = header.build_for_definition(&self.header_def);\n        let header_str = self.merge_properties(&header_str)?;\n        let begin_pos = self.parser.begin_pos;\n        self.parser\n            .file_content\n            .insert(begin_pos, header_str.as_str());\n        Ok(())\n    }\n\n    pub fn remove_header(&mut self) {\n        if let Some(end_pos) = self.parser.end_pos {\n            self.parser\n                .file_content\n                .delete(self.parser.begin_pos, end_pos);\n        }\n    }\n\n    pub fn save(&mut self, filepath: Option<&PathBuf>) -> Result<(), Error> {\n        let filepath = filepath.unwrap_or(&self.filepath);\n        fs::write(filepath, self.parser.file_content.content())\n            .or_raise(|| Error::new(format!(\"cannot save document {}\", filepath.display())))\n    }\n\n    pub(crate) fn merge_properties(&self, s: &str) -> Result<String, Error> {\n        let mut env = Environment::new();\n        env.add_template(\"template\", s)\n            .or_raise(|| Error::new(\"malformed template\"))?;\n\n        let tmpl = env.get_template(\"template\").expect(\"template must exist\");\n        let mut result = tmpl\n            .render(context! {\n                props => &self.props,\n                attrs => &self.attrs,\n            })\n            .or_raise(|| Error::new(\"cannot render template\"))?;\n        result.push('\\n');\n        Ok(result)\n    }\n}\n"
  },
  {
    "path": "fmt/src/document/model.rs",
    "content": "// Copyright 2024 tison <wander4096@gmail.com>\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nuse std::collections::HashMap;\n\nuse serde::Deserialize;\nuse serde::Serialize;\n\nuse crate::config::Mapping;\n\n#[derive(Debug, Clone, Default, Serialize, Deserialize)]\n#[serde(default, rename_all = \"camelCase\")]\npub struct DocumentType {\n    pub pattern: String,\n    pub header_type: String,\n    pub extension: bool,\n    pub filename: bool,\n}\n\npub fn default_mapping() -> Vec<Mapping> {\n    let defaults = include_str!(\"defaults.toml\");\n    let mapping: HashMap<String, DocumentType> =\n        toml::from_str(defaults).expect(\"default mapping must be valid\");\n\n    mapping\n        .into_values()\n        .flat_map(|doctype| {\n            let mut ms = vec![];\n            if doctype.extension {\n                ms.push(Mapping::Extension {\n                    pattern: doctype.pattern.clone(),\n                    header_type: doctype.header_type.clone(),\n                })\n            }\n            if doctype.filename {\n                ms.push(Mapping::Filename {\n                    pattern: doctype.pattern,\n                    header_type: doctype.header_type,\n                })\n            }\n            ms\n        })\n        .collect()\n}\n"
  },
  {
    "path": "fmt/src/error.rs",
    "content": "// Copyright 2024 tison <wander4096@gmail.com>\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n#[derive(Debug)]\npub struct Error {\n    message: String,\n}\n\nimpl Error {\n    pub fn new(message: impl Into<String>) -> Self {\n        Self {\n            message: message.into(),\n        }\n    }\n}\n\nimpl std::fmt::Display for Error {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"{}\", self.message)\n    }\n}\n\nimpl std::error::Error for Error {}\n"
  },
  {
    "path": "fmt/src/git.rs",
    "content": "// Copyright 2024 tison <wander4096@gmail.com>\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nuse std::collections::hash_map::Entry;\nuse std::collections::BTreeSet;\nuse std::collections::HashMap;\nuse std::path::Path;\nuse std::path::PathBuf;\n\nuse exn::bail;\nuse exn::ErrorExt;\nuse exn::Result;\nuse exn::ResultExt;\nuse gix::diff::tree_with_rewrites::Change;\nuse gix::status::Item;\nuse gix::Repository;\nuse walkdir::WalkDir;\n\nuse crate::config;\nuse crate::config::FeatureGate;\nuse crate::error::Error;\n\n#[derive(Debug, Clone)]\npub struct GitContext {\n    pub repo: Option<Repository>,\n    pub config: config::Git,\n}\n\npub fn discover(basedir: &Path, config: config::Git) -> Result<GitContext, Error> {\n    let feature = resolve_features(&config);\n\n    if feature.is_disable() {\n        return Ok(GitContext { repo: None, config });\n    }\n\n    match gix::discover(basedir) {\n        Ok(repo) => match repo.worktree() {\n            None => {\n                let message = \"bare repository detected\";\n                if feature.is_auto() {\n                    log::info!(config:?; \"git config is resolved to disabled; {message}\");\n                    Ok(GitContext { repo: None, config })\n                } else {\n                    bail!(Error::new(format!(\"invalid config: {message}\")))\n                }\n            }\n            Some(_) => {\n                log::info!(\"git config is resolved to enabled\");\n                Ok(GitContext {\n                    repo: Some(repo),\n                    config,\n                })\n            }\n        },\n        Err(err) => {\n            if feature.is_auto() {\n                log::info!(err:?, config:?; \"git config is resolved to disabled\");\n                Ok(GitContext { repo: None, config })\n            } else {\n                Err(err\n                    .raise()\n                    .raise(Error::new(\"cannot discover git repository with gix\")))\n            }\n        }\n    }\n}\n\nfn resolve_features(config: &config::Git) -> FeatureGate {\n    let features = [config.attrs, config.ignore];\n    for feature in features.iter() {\n        if feature.is_enable() {\n            return FeatureGate::Enable;\n        }\n    }\n    for feature in features.iter() {\n        if feature.is_auto() {\n            return FeatureGate::Auto;\n        }\n    }\n    FeatureGate::Disable\n}\n\n#[derive(Debug)]\npub struct GitFileAttrs {\n    pub created_time: gix::date::Time,\n    pub modified_time: gix::date::Time,\n    pub authors: BTreeSet<String>,\n}\n\npub fn resolve_file_attrs(\n    git_context: GitContext,\n) -> Result<HashMap<PathBuf, GitFileAttrs>, Error> {\n    let mut attrs = HashMap::new();\n\n    if git_context.config.attrs.is_disable() {\n        return Ok(attrs);\n    }\n\n    let repo = match git_context.repo {\n        Some(repo) => repo,\n        None => return Ok(attrs),\n    };\n\n    let current_username = match repo.committer() {\n        Some(Ok(username)) => username.name.to_string(),\n        _ => \"<unknown>\".to_string(),\n    };\n\n    let worktree = repo.worktree().expect(\"worktree cannot be absent\");\n    let workdir = repo.workdir().expect(\"workdir cannot be absent\");\n    let workdir = workdir.canonicalize().or_raise(|| {\n        Error::new(format!(\n            \"cannot resolve absolute path: {}\",\n            workdir.display()\n        ))\n    })?;\n\n    let mut excludes = worktree\n        .excludes(None)\n        .or_raise(|| Error::new(\"cannot create gix exclude stack\"))?;\n\n    let mut update_attrs = |rela_path: &Path, time: gix::date::Time, author: &str| {\n        let filepath = workdir.join(rela_path);\n        match attrs.entry(filepath) {\n            Entry::Occupied(mut ent) => {\n                let attrs: &mut GitFileAttrs = ent.get_mut();\n                attrs.created_time = time.min(attrs.created_time);\n                attrs.modified_time = time.max(attrs.modified_time);\n                attrs.authors.insert(author.to_string());\n            }\n            Entry::Vacant(ent) => {\n                ent.insert(GitFileAttrs {\n                    created_time: time,\n                    modified_time: time,\n                    authors: {\n                        let mut authors = BTreeSet::new();\n                        authors.insert(author.to_string());\n                        authors\n                    },\n                });\n            }\n        }\n    };\n\n    let mut process_changes = |changes: Vec<Change>, time: gix::date::Time, author: &str| {\n        for change in changes {\n            match change {\n                Change::Addition { location, .. } | Change::Modification { location, .. } => {\n                    update_attrs(&gix::path::from_bstring(location), time, author);\n                }\n                Change::Deletion { .. } => continue, // skip deletion\n                Change::Rewrite { .. } => unreachable!(\"rewrite has been disabled\"),\n            }\n        }\n    };\n\n    let option = {\n        let mut option = gix::diff::Options::default();\n        option.track_path();\n        option\n    };\n\n    let make_error = || Error::new(\"cannot resolve git file attributes\");\n\n    let head = repo.head_commit().or_raise(make_error)?;\n    let mut next_commit = head.clone();\n\n    for info in head.ancestors().all().or_raise(make_error)? {\n        let info = info.or_raise(make_error)?;\n        let this_commit = info.object().or_raise(make_error)?;\n        let time = next_commit.time().or_raise(make_error)?;\n        let author = next_commit.author().or_raise(make_error)?.name.to_string();\n\n        let this_tree = this_commit.tree().or_raise(make_error)?;\n        let next_tree = next_commit.tree().or_raise(make_error)?;\n\n        let changes = repo\n            .diff_tree_to_tree(Some(&this_tree), Some(&next_tree), Some(option))\n            .or_raise(make_error)?;\n        process_changes(changes, time, &author);\n\n        next_commit = this_commit;\n    }\n\n    // process the root commit\n    let time = next_commit.time().or_raise(make_error)?;\n    let author = next_commit.author().or_raise(make_error)?.name.to_string();\n    let next_tree = next_commit.tree().or_raise(make_error)?;\n    let changes = repo\n        .diff_tree_to_tree(None, Some(&next_tree), Some(option))\n        .or_raise(make_error)?;\n    process_changes(changes, time, &author);\n\n    // process dirty working tree\n    let index = repo.index_or_empty().or_raise(make_error)?;\n    let status_platform = repo.status(gix::progress::Discard).or_raise(make_error)?;\n    let status_iter = status_platform.into_iter(None).or_raise(make_error)?;\n    let now = gix::date::Time::now_local_or_utc();\n    for item in status_iter {\n        match item.or_raise(|| Error::new(\"failed to check git status item\"))? {\n            Item::IndexWorktree(item) => match item {\n                gix::status::index_worktree::Item::Modification { rela_path, .. } => {\n                    let rela_path = gix::path::from_bstring(rela_path);\n                    update_attrs(&rela_path, now, current_username.as_str());\n                }\n                gix::status::index_worktree::Item::DirectoryContents { entry, .. } => {\n                    if entry.disk_kind.is_some_and(|k| k.is_dir()) {\n                        let dirpath = workdir.join(gix::path::from_bstr(&entry.rela_path));\n                        let mut it = WalkDir::new(dirpath).follow_links(false).into_iter();\n                        while let Some(entry) = it.next() {\n                            let entry =\n                                entry.or_raise(|| Error::new(\"cannot traverse directory\"))?;\n                            let path = entry.path();\n                            let file_type = entry.file_type();\n                            if !file_type.is_file() && !file_type.is_dir() {\n                                log::debug!(file_type:?; \"skip file: {path:?}\");\n                                continue;\n                            }\n\n                            let rela_path = path\n                                .strip_prefix(&workdir)\n                                .expect(\"git repository encloses iteration\");\n                            let mode = Some(if file_type.is_dir() {\n                                gix::index::entry::Mode::DIR\n                            } else {\n                                gix::index::entry::Mode::FILE\n                            });\n                            let platform = excludes\n                                .at_path(rela_path, mode)\n                                .or_raise(|| Error::new(\"cannot check gix exclude\"))?;\n\n                            if file_type.is_dir() {\n                                if platform.is_excluded() {\n                                    let rela =\n                                        gix::path::try_into_bstr(rela_path).or_raise(|| {\n                                            Error::new(\"cannot convert path to git path\")\n                                        })?;\n\n                                    if !index.path_is_directory(rela.as_ref()) {\n                                        log::debug!(path:?, rela_path:?; \"skip git ignored directory\");\n                                        it.skip_current_dir();\n                                        continue;\n                                    }\n                                }\n                            } else if file_type.is_file() {\n                                if platform.is_excluded() {\n                                    let rela =\n                                        gix::path::try_into_bstr(rela_path).or_raise(|| {\n                                            Error::new(\"cannot convert path to git path\")\n                                        })?;\n\n                                    if index.entry_by_path(rela.as_ref()).is_none() {\n                                        log::debug!(path:?, rela_path:?; \"skip git ignored file\");\n                                        continue;\n                                    }\n                                }\n                                update_attrs(rela_path, now, current_username.as_str());\n                            }\n                        }\n                    } else {\n                        let rela_path = gix::path::from_bstring(entry.rela_path);\n                        update_attrs(&rela_path, now, current_username.as_str());\n                    }\n                }\n                gix::status::index_worktree::Item::Rewrite { .. } => {\n                    unreachable!(\"rewrite has been disabled\")\n                }\n            },\n            Item::TreeIndex(item) => {\n                let rela_path = gix::path::from_bstr(item.location());\n                update_attrs(&rela_path, now, current_username.as_str());\n            }\n        }\n    }\n\n    Ok(attrs)\n}\n"
  },
  {
    "path": "fmt/src/header/defaults.toml",
    "content": "# Copyright 2024 tison <wander4096@gmail.com>\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n[SHARPSTAR_STYLE]\nfirstLine = '#*'\nendLine = ' *#'\nbeforeEachLine = ' * '\nafterEachLine = ''\nallowBlankLines = false\nmultipleLines = true\npadLines = false\nfirstLineDetectionPattern = '(\\s|\\t)*#\\*.*$'\nlastLineDetectionPattern = '.*\\*#(\\s|\\t)*$'\n\n[PERCENT_STYLE]\nfirstLine = ''\nendLine = \"\\n\"\nbeforeEachLine = '% '\nafterEachLine = ''\nallowBlankLines = false\nmultipleLines = false\npadLines = false\nfirstLineDetectionPattern = '^% .*$'\nlastLineDetectionPattern = '^% .*$'\n\n[BRACESSTAR_STYLE]\nfirstLine = '{*'\nendLine = ' *}'\nbeforeEachLine = ' * '\nafterEachLine = ''\nallowBlankLines = false\nmultipleLines = true\npadLines = false\nfirstLineDetectionPattern = '(\\s|\\t)*\\{\\*.*$'\nlastLineDetectionPattern = '.*\\*\\}(\\s|\\t)*$'\n\n[EXCLAMATION_STYLE]\nfirstLine = '!'\nendLine = \"!\\n\"\nbeforeEachLine = '! '\nafterEachLine = ''\nallowBlankLines = false\nmultipleLines = false\npadLines = false\nfirstLineDetectionPattern = '!.*$'\nlastLineDetectionPattern = '!.*$'\n\n[MVEL_STYLE]\nfirstLine = '@comment{'\nendLine = '}'\nbeforeEachLine = '  '\nafterEachLine = ''\nallowBlankLines = true\nmultipleLines = true\npadLines = false\nfirstLineDetectionPattern = '@comment\\{$'\nlastLineDetectionPattern = '\\}$'\n\n[APOSTROPHE_STYLE]\nfirstLine = \"'\"\nendLine = \"'\\n\"\nbeforeEachLine = \"' \"\nafterEachLine = ''\nallowBlankLines = false\nmultipleLines = false\npadLines = false\nfirstLineDetectionPattern = \"'.*$\"\nlastLineDetectionPattern = \"'.*$\"\n\n[TRIPLESLASH_STYLE]\nfirstLine = '///'\nendLine = \"///\\n\"\nbeforeEachLine = '/// '\nafterEachLine = ''\nallowBlankLines = false\nmultipleLines = false\npadLines = false\nfirstLineDetectionPattern = '///.*$'\nlastLineDetectionPattern = '///.*$'\n\n[PHP]\nfirstLine = '/*'\nendLine = ' */'\nbeforeEachLine = ' * '\nafterEachLine = ''\nallowBlankLines = false\nmultipleLines = true\npadLines = false\nskipLinePattern = '^<\\?php.*$'\nfirstLineDetectionPattern = '(\\s|\\t)*/\\*.*$'\nlastLineDetectionPattern = '.*\\*/(\\s|\\t)*$'\n\n[LUA]\nfirstLine = \"--[[\\n\"\nendLine = \"\\n]]\"\nbeforeEachLine = '    '\nafterEachLine = ''\nallowBlankLines = true\nmultipleLines = true\npadLines = false\nfirstLineDetectionPattern = '--\\[\\[$'\nlastLineDetectionPattern = '\\]\\]$'\n\n[SCALA_STYLE]\nfirstLine = '/**'\nendLine = '  */'\nbeforeEachLine = '  * '\nafterEachLine = ''\nallowBlankLines = false\nmultipleLines = true\npadLines = false\nfirstLineDetectionPattern = '(\\s|\\t)*/\\*.*$'\nlastLineDetectionPattern = '.*\\*/(\\s|\\t)*$'\n\n[BATCH]\nfirstLine = '@REM'\nendLine = \"@REM\\n\"\nbeforeEachLine = '@REM '\nafterEachLine = ''\nallowBlankLines = false\nmultipleLines = false\npadLines = false\nfirstLineDetectionPattern = '@REM.*$'\nlastLineDetectionPattern = '@REM.*$'\n\n[MUSTACHE_STYLE]\nfirstLine = '{{!'\nendLine = '}}'\nbeforeEachLine = '    '\nafterEachLine = ''\nallowBlankLines = false\nmultipleLines = true\npadLines = false\nfirstLineDetectionPattern = '\\{\\{\\!.*$'\nlastLineDetectionPattern = '\\}\\}.*$'\n\n[HAML_STYLE]\nfirstLine = '-#'\nendLine = \"-#\\n\"\nbeforeEachLine = '-# '\nafterEachLine = ''\nallowBlankLines = false\nmultipleLines = false\npadLines = false\nskipLinePattern = '^-#!.*$'\nfirstLineDetectionPattern = '-#.*$'\nlastLineDetectionPattern = '-#.*$'\n\n[PERCENT3_STYLE]\nfirstLine = '%%%'\nendLine = \"%%%\\n\"\nbeforeEachLine = '%%% '\nafterEachLine = ''\nallowBlankLines = false\nmultipleLines = false\npadLines = false\nfirstLineDetectionPattern = '%%%.*$'\nlastLineDetectionPattern = '%%%.*$'\n\n[EXCLAMATION3_STYLE]\nfirstLine = '!!!'\nendLine = \"!!!\\n\"\nbeforeEachLine = '!!! '\nafterEachLine = ''\nallowBlankLines = false\nmultipleLines = false\npadLines = false\nfirstLineDetectionPattern = '!!!.*$'\nlastLineDetectionPattern = '!!!.*$'\n\n[ASP]\nfirstLine = '<%'\nendLine = '%>'\nbeforeEachLine = \"' \"\nafterEachLine = ''\nallowBlankLines = true\nmultipleLines = true\npadLines = false\nfirstLineDetectionPattern = '(\\s|\\t)*<%( .*)?$'\nlastLineDetectionPattern = '.*%>(\\s|\\t)*$'\n\n[SCRIPT_STYLE]\nfirstLine = ''\nendLine = \"\\n\"\nbeforeEachLine = '# '\nafterEachLine = ''\nallowBlankLines = false\nmultipleLines = false\npadLines = false\nskipLinePattern = '^#!.*$'\nfirstLineDetectionPattern = '#.*$'\nlastLineDetectionPattern = '#.*$'\n\n[JAVAPKG_STYLE]\nfirstLine = \"\\n/*-\"\nendLine = ' */'\nbeforeEachLine = ' * '\nafterEachLine = ''\nallowBlankLines = false\nmultipleLines = true\npadLines = false\nskipLinePattern = '^package [a-z_]+(\\.[a-z_][a-z0-9_]*)*;$'\nfirstLineDetectionPattern = '(\\s|\\t)*/\\*.*$'\nlastLineDetectionPattern = '.*\\*/(\\s|\\t)*$'\n\n[DOUBLESLASH_STYLE]\nfirstLine = ''\nendLine = \"\\n\"\nbeforeEachLine = '// '\nafterEachLine = ''\nallowBlankLines = false\nmultipleLines = false\npadLines = false\nfirstLineDetectionPattern = '//.*$'\nlastLineDetectionPattern = '//.*$'\n\n[XML_STYLE]\nfirstLine = \"<!--\\n\"\nendLine = \"\\n-->\"\nbeforeEachLine = '    '\nafterEachLine = ''\nallowBlankLines = true\nmultipleLines = true\npadLines = false\nskipLinePattern = '^<\\?xml.*>$'\nfirstLineDetectionPattern = '(\\s|\\t)*<!--.*$'\nlastLineDetectionPattern = '.*-->(\\s|\\t)*$'\n\n[ASCIIDOC_STYLE]\nfirstLine = '////'\nendLine = \"////\\n\"\nbeforeEachLine = '  // '\nafterEachLine = ''\nallowBlankLines = false\nmultipleLines = true\npadLines = false\nfirstLineDetectionPattern = '^////$'\nlastLineDetectionPattern = '^////$'\n\n[SEMICOLON_STYLE]\nfirstLine = ';'\nendLine = \";\\n\"\nbeforeEachLine = '; '\nafterEachLine = ''\nallowBlankLines = false\nmultipleLines = false\npadLines = false\nfirstLineDetectionPattern = ';.*$'\nlastLineDetectionPattern = ';.*$'\n\n[DOUBLETILDE_STYLE]\nfirstLine = '~~'\nendLine = \"~~\\n\"\nbeforeEachLine = '~~ '\nafterEachLine = ''\nallowBlankLines = false\nmultipleLines = false\npadLines = false\nfirstLineDetectionPattern = '~~.*$'\nlastLineDetectionPattern = '~~.*$'\n\n[SLASHSTAR_STYLE]\nfirstLine = '/*'\nendLine = \" */\\n\"\nbeforeEachLine = ' * '\nafterEachLine = ''\nallowBlankLines = false\nmultipleLines = true\npadLines = false\nfirstLineDetectionPattern = '(\\s|\\t)*/\\*.*$'\nlastLineDetectionPattern = '.*\\*/(\\s|\\t)*$'\n\n[FTL_ALT]\nfirstLine = \"[#--\\n\"\nendLine = \"\\n--]\"\nbeforeEachLine = '    '\nafterEachLine = ''\nallowBlankLines = true\nmultipleLines = true\npadLines = false\nskipLinePattern = '\\[#ftl(\\s.*)?\\]'\nfirstLineDetectionPattern = '(\\s|\\t)*\\[#--.*$'\nlastLineDetectionPattern = '.*--\\](\\s|\\t)*$'\n\n[DOUBLEDASHES_STYLE]\nfirstLine = '--'\nendLine = \"--\\n\"\nbeforeEachLine = '-- '\nafterEachLine = ''\nallowBlankLines = false\nmultipleLines = false\npadLines = false\nfirstLineDetectionPattern = '--.*$'\nlastLineDetectionPattern = '--.*$'\n\n[DYNASCRIPT3_STYLE]\nfirstLine = \"<!---\\n\"\nendLine = \"\\n--->\"\nbeforeEachLine = '    '\nafterEachLine = ''\nallowBlankLines = true\nmultipleLines = true\npadLines = false\nfirstLineDetectionPattern = '(\\s|\\t)*<!---.*$'\nlastLineDetectionPattern = '.*--->(\\s|\\t)*$'\n\n[SINGLE_LINE_DOUBLESLASH_STYLE]\nfirstLine = ''\nendLine = ''\nbeforeEachLine = '// '\nafterEachLine = ''\nallowBlankLines = false\nmultipleLines = false\npadLines = false\nfirstLineDetectionPattern = '//.*$'\nlastLineDetectionPattern = '//.*$'\n\n[XML_PER_LINE]\nfirstLine = \"\\n\"\nendLine = \"\\n\"\nbeforeEachLine = '<!-- '\nafterEachLine = ' -->'\nallowBlankLines = false\nmultipleLines = false\npadLines = true\nskipLinePattern = '^<\\?xml.*>$'\nfirstLineDetectionPattern = '(\\s|\\t)*<!--.*$'\nlastLineDetectionPattern = '.*-->(\\s|\\t)*$'\n\n[UNKNOWN]\nfirstLine = ''\nendLine = ''\nbeforeEachLine = ''\nafterEachLine = ''\nallowBlankLines = false\nmultipleLines = false\npadLines = false\nfirstLineDetectionPattern = ''\nlastLineDetectionPattern = ''\n\n[JAVADOC_STYLE]\nfirstLine = '/**'\nendLine = ' */'\nbeforeEachLine = ' * '\nafterEachLine = ''\nallowBlankLines = false\nmultipleLines = true\npadLines = false\nfirstLineDetectionPattern = '(\\s|\\t)*/\\*.*$'\nlastLineDetectionPattern = '.*\\*/(\\s|\\t)*$'\n\n[DYNASCRIPT_STYLE]\nfirstLine = \"<%--\\n\"\nendLine = \"\\n--%>\"\nbeforeEachLine = '    '\nafterEachLine = ''\nallowBlankLines = true\nmultipleLines = true\npadLines = false\nfirstLineDetectionPattern = '(\\s|\\t)*<%--.*$'\nlastLineDetectionPattern = '.*--%>(\\s|\\t)*$'\n\n[FTL]\nfirstLine = \"<#--\\n\"\nendLine = \"\\n-->\"\nbeforeEachLine = '    '\nafterEachLine = ''\nallowBlankLines = true\nmultipleLines = true\npadLines = false\nfirstLineDetectionPattern = '(\\s|\\t)*<#--.*$'\nlastLineDetectionPattern = '.*-->(\\s|\\t)*$'\n\n[LINE_BLOCK_STYLE]\nfirstLine = '//===----------------------------------------------------------------------===//'\nendLine = \"//===----------------------------------------------------------------------===//\\n\"\nbeforeEachLine = '// '\nafterEachLine = ''\nallowBlankLines = false\nmultipleLines = true\npadLines = false\nfirstLineDetectionPattern = '//\\s?==='\nlastLineDetectionPattern = '//\\s?==='\n"
  },
  {
    "path": "fmt/src/header/matcher.rs",
    "content": "// Copyright 2024 tison <wander4096@gmail.com>\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nuse std::fmt::Display;\nuse std::fmt::Formatter;\n\nuse crate::header::model::HeaderDef;\n\n#[derive(Debug)]\npub struct HeaderMatcher {\n    header_content: String,\n    header_content_one_line: String,\n    header_content_lines: Vec<String>,\n    max_length: usize,\n}\n\nimpl HeaderMatcher {\n    pub fn new(header_content: String) -> Self {\n        let header_content_one_line = header_content.split_whitespace().collect();\n        let header_content_lines = header_content\n            .lines()\n            .map(ToString::to_string)\n            .collect::<Vec<_>>();\n        let max_length = header_content_lines\n            .iter()\n            .map(|l| l.len())\n            .max()\n            .unwrap_or(0);\n        Self {\n            header_content,\n            header_content_one_line,\n            header_content_lines,\n            max_length,\n        }\n    }\n\n    pub fn build_for_definition(&self, def: &HeaderDef) -> String {\n        let eol = \"\\n\";\n        let mut result = String::new();\n\n        if !def.first_line.is_empty() {\n            result.push_str(&def.first_line);\n            if def.first_line != eol {\n                result.push_str(eol);\n            }\n        }\n\n        for line in &self.header_content_lines {\n            let before = &def.before_each_line;\n            let after = &def.after_each_line;\n            let this_line = if def.pad_lines {\n                let max_length = self.max_length;\n                format!(\"{before}{line: <max_length$}{after}\")\n            } else {\n                format!(\"{before}{line}{after}\")\n            };\n            result.push_str(this_line.trim_end());\n            result.push_str(eol);\n        }\n\n        if !def.end_line.is_empty() {\n            result.push_str(&def.end_line);\n            if def.end_line != eol {\n                result.push_str(eol);\n            }\n        }\n\n        result\n    }\n\n    pub fn header_content_lines_count(&self) -> usize {\n        self.header_content_lines.len()\n    }\n\n    pub fn header_content_one_line(&self) -> &str {\n        &self.header_content_one_line\n    }\n}\n\nimpl Display for HeaderMatcher {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        f.write_str(&self.header_content)\n    }\n}\n"
  },
  {
    "path": "fmt/src/header/mod.rs",
    "content": "// Copyright 2024 tison <wander4096@gmail.com>\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npub mod matcher;\npub mod model;\npub mod parser;\n"
  },
  {
    "path": "fmt/src/header/model.rs",
    "content": "// Copyright 2024 tison <wander4096@gmail.com>\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nuse std::collections::HashMap;\n\nuse exn::Result;\nuse exn::ResultExt;\nuse regex::Regex;\nuse serde::Deserialize;\nuse serde::Serialize;\n\nuse crate::default_true;\nuse crate::error::Error;\n\n#[derive(Debug, Clone)]\npub struct HeaderDef {\n    pub name: String,\n    pub first_line: String,\n    pub end_line: String,\n    pub before_each_line: String,\n    pub after_each_line: String,\n\n    pub allow_blank_lines: bool,\n    pub multiple_lines: bool,\n    pub pad_lines: bool,\n\n    pub skip_line_pattern: Option<Regex>,\n    pub first_line_detection_pattern: Option<Regex>,\n    pub last_line_detection_pattern: Option<Regex>,\n}\n\nimpl HeaderDef {\n    /// Tells if the given content line must be skipped according to this header definition. The\n    /// header is outputted after any skipped line if any pattern defined on this point or on the\n    /// first line if not pattern defined.\n    pub fn is_skip_line(&self, line: &str) -> bool {\n        self.skip_line_pattern\n            .as_ref()\n            .is_some_and(|pattern| pattern.is_match(line))\n    }\n\n    /// Tells if the given content line is the first line of a possible header of this definition\n    /// kind.\n    pub fn is_first_header_line(&self, line: &str) -> bool {\n        self.first_line_detection_pattern\n            .as_ref()\n            .is_some_and(|p| p.is_match(line))\n    }\n\n    /// Tells if the given content line is the last line of a possible header of this definition\n    /// kind.\n    pub fn is_last_header_line(&self, line: &str) -> bool {\n        self.last_line_detection_pattern\n            .as_ref()\n            .is_some_and(|p| p.is_match(line))\n    }\n}\n\npub fn default_headers() -> HashMap<String, HeaderDef> {\n    let defaults = include_str!(\"defaults.toml\");\n    deserialize_header_definitions(defaults.to_string()).unwrap()\n}\n\npub fn deserialize_header_definitions(value: String) -> Result<HashMap<String, HeaderDef>, Error> {\n    let header_styles = toml::from_str::<HashMap<String, HeaderStyle>>(&value)\n        .or_raise(|| Error::new(\"failed to parse header definitions\"))?;\n\n    let headers = header_styles\n        .into_iter()\n        .map(|(name, style)| {\n            let name = name.to_lowercase();\n\n            assert!(\n                !style.allow_blank_lines || style.multiple_lines,\n                \"Header style {name} allowing blank lines must be of multi-line header style\"\n            );\n\n            let def = HeaderDef {\n                name: name.clone(),\n                first_line: style.first_line,\n                end_line: style.end_line,\n                before_each_line: style.before_each_line,\n                after_each_line: style.after_each_line,\n                allow_blank_lines: style.allow_blank_lines,\n                multiple_lines: style.multiple_lines,\n                pad_lines: style.pad_lines,\n                skip_line_pattern: style\n                    .skip_line_pattern\n                    .map(|pattern| {\n                        Regex::new(&pattern).or_raise(|| {\n                            Error::new(format!(\"malformed skip_line_pattern: {pattern}\"))\n                        })\n                    })\n                    .transpose()?,\n                first_line_detection_pattern: style\n                    .first_line_detection_pattern\n                    .map(|pattern| {\n                        Regex::new(&pattern).or_raise(|| {\n                            Error::new(format!(\"malformed first_line_detection_pattern: {pattern}\"))\n                        })\n                    })\n                    .transpose()?,\n                last_line_detection_pattern: style\n                    .last_line_detection_pattern\n                    .map(|pattern| {\n                        Regex::new(&pattern).or_raise(|| {\n                            Error::new(format!(\"malformed last_line_detection_pattern: {pattern}\"))\n                        })\n                    })\n                    .transpose()?,\n            };\n\n            Ok((name, def))\n        })\n        .collect::<Result<HashMap<String, HeaderDef>, Error>>()?;\n    Ok(headers)\n}\n\n#[derive(Debug, Clone, Default, Serialize, Deserialize)]\n#[serde(default, rename_all = \"camelCase\")]\npub struct HeaderStyle {\n    /// The first fixed line of this header.\n    pub first_line: String,\n    /// The last fixed line of this header.\n    pub end_line: String,\n    /// The characters to prepend before each license header lines. Default to empty.\n    pub before_each_line: String,\n    /// The characters to append after each license header lines. Default to empty.\n    pub after_each_line: String,\n    /// Only for multi-line comments: specify if blank lines are allowed.\n    /// Default to false because most of the time, a header has some characters on each line.\n    pub allow_blank_lines: bool,\n    /// Specify whether this is a multi-line comment style or not.\n    ///\n    /// A multi-line comment style is equivalent to what we have in Java, where a first line and\n    /// line will delimit a whole multi-line comment section.\n    ///\n    /// A style that is not multi-line is usually repeating in each line the characters before and\n    /// after each line to delimit a one-line comment.\n    #[serde(default = \"default_true\")]\n    pub multiple_lines: bool,\n    /// Only for non multi-line comments: specify if some spaces should be added after the header\n    /// line and before the {@link #afterEachLine} characters so that all the lines are aligned.\n    /// Default to false.\n    pub pad_lines: bool,\n    /// A regex to define a first line in a file that should be skipped and kept untouched, like\n    /// the XML declaration at the top of XML documents. Default to none.\n    pub skip_line_pattern: Option<String>,\n    /// The regex used to detect the start of a header section or line.\n    pub first_line_detection_pattern: Option<String>,\n    /// The regex used to detect the end of a header section or line.\n    pub last_line_detection_pattern: Option<String>,\n}\n"
  },
  {
    "path": "fmt/src/header/parser.rs",
    "content": "// Copyright 2024 tison <wander4096@gmail.com>\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nuse std::fmt::Display;\nuse std::fmt::Formatter;\nuse std::fs::File;\nuse std::io::BufRead;\nuse std::io::BufReader;\nuse std::path::Path;\n\nuse crate::header::model::HeaderDef;\n\n#[derive(Debug)]\npub struct HeaderParser {\n    pub begin_pos: usize,\n    /// Some if header exists; None if header does not exist.\n    pub end_pos: Option<usize>,\n    pub file_content: FileContent,\n}\n\npub fn parse_header(\n    mut file_content: FileContent,\n    header_def: &HeaderDef,\n    keywords: &[String],\n) -> HeaderParser {\n    let mut line = file_content.next_line();\n\n    // 1. find begin position\n    let begin_pos = find_first_position(&mut line, &mut file_content, header_def);\n\n    // 2. has header\n    let existing_header = existing_header(&mut line, &mut file_content, header_def, keywords);\n\n    // 3. find end position\n    let end_pos = if existing_header {\n        // we check if there is a header, if the next line is the blank line of the header\n        let mut end = file_content.pos;\n        line = file_content.next_line();\n        if begin_pos == 0 {\n            while line.as_ref().map(|l| l.trim().is_empty()).unwrap_or(false) {\n                end = file_content.pos;\n                line = file_content.next_line();\n            }\n        }\n        if header_def.end_line.ends_with('\\n')\n            && line.as_ref().map(|l| l.trim().is_empty()).unwrap_or(false)\n        {\n            end = file_content.pos;\n        }\n        Some(end)\n    } else {\n        None\n    };\n\n    HeaderParser {\n        begin_pos,\n        end_pos,\n        file_content,\n    }\n}\n\nfn find_first_position(\n    line: &mut Option<String>,\n    file_content: &mut FileContent,\n    header_def: &HeaderDef,\n) -> usize {\n    const UTF8_BOM: [u8; 3] = [0xEF, 0xBB, 0xBF];\n\n    let mut begin_pos = 0;\n\n    if let Some(l) = line.as_ref() {\n        // skip UTF-8 BOM if exists\n        if l.as_bytes().starts_with(&UTF8_BOM) {\n            log::debug!(\"Detected UTF-8 BOM for {file_content}; skip\");\n            begin_pos = 3;\n            file_content.reset_to(3);\n        }\n    }\n\n    if header_def.skip_line_pattern.is_some() {\n        // the format expect to find lines to be skipped\n        while line\n            .as_ref()\n            .map(|l| !header_def.is_skip_line(l))\n            .unwrap_or(false)\n        {\n            begin_pos = file_content.pos;\n            *line = file_content.next_line();\n        }\n\n        // at least we have found the line to skip, or we are the end of the file\n        // this time we are going to skip next lines if they match the skip pattern\n        while line\n            .as_ref()\n            .map(|l| header_def.is_skip_line(l))\n            .unwrap_or(false)\n        {\n            begin_pos = file_content.pos;\n            *line = file_content.next_line();\n        }\n\n        // After skipping everything we are at the end of the file\n        // Header has to be at the file beginning\n        if line.is_none() {\n            begin_pos = 0;\n            file_content.reset();\n            *line = file_content.next_line();\n\n            // recheck for UTF-8 BOM\n            if let Some(l) = line.as_ref() {\n                if l.as_bytes().starts_with(&UTF8_BOM) {\n                    begin_pos = 3;\n                    file_content.reset_to(3);\n                }\n            }\n        }\n    }\n\n    begin_pos\n}\n\nfn existing_header(\n    line: &mut Option<String>,\n    file_content: &mut FileContent,\n    header_def: &HeaderDef,\n    keywords: &[String],\n) -> bool {\n    // skip blank lines\n    while line.as_ref().map(|l| l.trim().is_empty()).unwrap_or(false) {\n        *line = file_content.next_line();\n    }\n\n    // check if there is already a header\n    let l = match line.as_ref() {\n        Some(l) if header_def.is_first_header_line(l) => l,\n        _ => return false,\n    };\n\n    let mut got_header = false;\n    let mut in_place_header = String::new();\n    in_place_header.push_str(&l.to_lowercase());\n\n    *line = file_content.next_line();\n\n    // skip blank lines before header text\n    if header_def.allow_blank_lines {\n        while line.as_ref().map(|l| l.trim().is_empty()).unwrap_or(false) {\n            *line = file_content.next_line();\n        }\n    }\n\n    // first header detected line & potential blank lines have been detected\n    // following lines should be header lines\n    if let Some(l) = line.as_ref() {\n        let before = {\n            let mut before = header_def.before_each_line.trim_end();\n            if before.is_empty() && !header_def.multiple_lines {\n                before = header_def.before_each_line.as_str();\n            }\n            before\n        };\n\n        let found_end = {\n            let mut found_end = false;\n            if (header_def.multiple_lines && header_def.is_last_header_line(l))\n                || l.trim().is_empty()\n            {\n                in_place_header.push_str(&l.to_lowercase());\n                found_end = true;\n            } else {\n                loop {\n                    match line.as_ref() {\n                        Some(l) if l.starts_with(before) => {\n                            in_place_header.push_str(&l.to_lowercase());\n                            if header_def.multiple_lines && header_def.is_last_header_line(l) {\n                                found_end = true;\n                                break;\n                            }\n                        }\n                        _ => break,\n                    }\n                    *line = file_content.next_line();\n                }\n\n                if line.as_ref().map(|l| l.trim().is_empty()).unwrap_or(true) {\n                    found_end = true;\n                }\n            }\n            found_end\n        };\n\n        // skip blank lines after header text\n        if header_def.multiple_lines && header_def.allow_blank_lines && !found_end {\n            loop {\n                if !line.as_ref().map(|l| l.trim().is_empty()).unwrap_or(false) {\n                    break;\n                }\n                *line = file_content.next_line();\n            }\n            file_content.rewind();\n        } else if !header_def.multiple_lines && !found_end {\n            file_content.rewind();\n        }\n\n        if !header_def.multiple_lines {\n            // keep track of the position for headers where the end line is the same as the\n            // before each line\n            let pos = file_content.pos;\n            // check if the line is the end line\n            while line\n                .as_ref()\n                .map(|l| {\n                    !header_def.is_last_header_line(l)\n                        && (header_def.allow_blank_lines || !l.trim().is_empty())\n                        && l.starts_with(before)\n                })\n                .unwrap_or(false)\n            {\n                *line = file_content.next_line();\n            }\n            if line.is_none() {\n                file_content.reset_to(pos);\n            }\n        } else if line.is_some() {\n            // we could end up there if we still have some lines, but not matching \"before\".\n            // This can be the last line in a multi line header\n            let pos = file_content.pos;\n            *line = file_content.next_line();\n            if line\n                .as_ref()\n                .map(|l| !header_def.is_last_header_line(l))\n                .unwrap_or(true)\n            {\n                file_content.reset_to(pos);\n            }\n        }\n\n        got_header = true;\n        for keyword in keywords {\n            if !in_place_header.contains(keyword) {\n                got_header = false;\n                break;\n            }\n        }\n    }\n    // else - we detected previously a one line comment block that matches the header\n    // detection it is not a header it is a comment\n    got_header\n}\n\n#[derive(Debug)]\npub struct FileContent {\n    pos: usize,\n    old_pos: usize,\n    content: String,\n    filepath: String,\n}\n\nimpl Display for FileContent {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        f.write_str(&self.filepath)\n    }\n}\n\nimpl FileContent {\n    pub fn new(file: &Path) -> std::io::Result<Self> {\n        Ok(Self {\n            pos: 0,\n            old_pos: 0,\n            content: {\n                let mut content = String::new();\n                let mut reader = File::open(file).map(BufReader::new)?;\n                let mut buf = String::new();\n                let mut n = reader.read_line(&mut buf)?;\n                while n > 0 {\n                    if buf.ends_with('\\n') {\n                        buf.pop();\n                        if buf.ends_with('\\r') {\n                            buf.pop();\n                        }\n                        content.push_str(&buf);\n                        content.push('\\n');\n                    } else {\n                        content.push_str(&buf);\n                    }\n                    buf.clear();\n                    n = reader.read_line(&mut buf)?;\n                }\n                content\n            },\n            filepath: file.to_string_lossy().to_string(),\n        })\n    }\n\n    pub fn reset_to(&mut self, pos: usize) {\n        self.old_pos = pos;\n        self.pos = pos;\n    }\n\n    pub fn reset(&mut self) {\n        self.reset_to(0);\n    }\n\n    pub fn rewind(&mut self) {\n        self.pos = self.old_pos;\n    }\n\n    pub fn end_reached(&self) -> bool {\n        self.pos >= self.content.len()\n    }\n\n    pub fn next_line(&mut self) -> Option<String> {\n        if self.end_reached() {\n            return None;\n        }\n\n        let lf = self.content[self.pos..].find('\\n').map(|i| i + self.pos);\n        let eol = lf.unwrap_or(self.content.len());\n        let result = self.content[self.pos..eol].to_string();\n\n        self.old_pos = self.pos;\n        self.pos = if let Some(lf) = lf {\n            lf + 1\n        } else {\n            self.content.len()\n        };\n\n        Some(result)\n    }\n\n    pub fn content(&self) -> String {\n        self.content.clone()\n    }\n\n    pub fn insert(&mut self, index: usize, s: &str) {\n        self.content.insert_str(index, s);\n    }\n\n    pub fn delete(&mut self, start: usize, end: usize) {\n        self.content.drain(start..end);\n    }\n}\n"
  },
  {
    "path": "fmt/src/lib.rs",
    "content": "// Copyright 2024 tison <wander4096@gmail.com>\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npub mod config;\npub mod document;\npub mod error;\npub mod git;\npub mod header;\npub mod license;\npub mod processor;\npub mod selection;\n\nconst fn default_true() -> bool {\n    true\n}\n"
  },
  {
    "path": "fmt/src/license/Apache-2.0-ASF.txt",
    "content": "Licensed to the Apache Software Foundation (ASF) under one\nor more contributor license agreements.  See the NOTICE file\ndistributed with this work for additional information\nregarding copyright ownership.  The ASF licenses this file\nto you under the Apache License, Version 2.0 (the\n\"License\"); you may not use this file except in compliance\nwith the License.  You may obtain a copy of the License at\n\n  http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing,\nsoftware distributed under the License is distributed on an\n\"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\nKIND, either express or implied.  See the License for the\nspecific language governing permissions and limitations\nunder the License."
  },
  {
    "path": "fmt/src/license/Apache-2.0.txt",
    "content": "Copyright {{props[\"inceptionYear\"]}} {{props[\"copyrightOwner\"]}}\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License."
  },
  {
    "path": "fmt/src/license/Elastic-2.0.txt",
    "content": "Copyright {{props[\"inceptionYear\"]}} {{props[\"copyrightOwner\"]}}\n\nLicensed under the Elastic License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.elastic.co/licensing/elastic-license\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License."
  },
  {
    "path": "fmt/src/license/mod.rs",
    "content": "// Copyright 2024 tison <wander4096@gmail.com>\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n#[derive(Debug, Clone)]\npub struct HeaderSource {\n    pub content: String,\n}\n\nmacro_rules! match_bundled_headers {\n    ($name:expr, $($file:expr),*) => {\n        match $name {\n            $(\n                $file => Some(HeaderSource { content: include_str!($file).to_string() }),\n            )*\n            _ => None,\n        }\n    }\n}\n\npub(crate) fn bundled_headers(name: &str) -> Option<HeaderSource> {\n    match_bundled_headers!(\n        name,\n        \"Apache-2.0.txt\",\n        \"Apache-2.0-ASF.txt\",\n        \"Elastic-2.0.txt\"\n    )\n}\n"
  },
  {
    "path": "fmt/src/processor.rs",
    "content": "// Copyright 2024 tison <wander4096@gmail.com>\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nuse std::collections::hash_map::Entry;\nuse std::collections::HashMap;\nuse std::fs;\nuse std::path::Path;\nuse std::path::PathBuf;\n\nuse exn::bail;\nuse exn::ensure;\nuse exn::OptionExt;\nuse exn::Result;\nuse exn::ResultExt;\n\nuse crate::config::Config;\nuse crate::document::factory::DocumentFactory;\nuse crate::document::model::default_mapping;\nuse crate::document::Document;\nuse crate::error::Error;\nuse crate::git;\nuse crate::header::matcher::HeaderMatcher;\nuse crate::header::model::default_headers;\nuse crate::header::model::deserialize_header_definitions;\nuse crate::header::model::HeaderDef;\nuse crate::license::bundled_headers;\nuse crate::license::HeaderSource;\nuse crate::selection::Selection;\n\n/// Callback for processing the result of checking license headers.\npub trait Callback {\n    /// Called when the header is unknown.\n    fn on_unknown(&mut self, path: &Path);\n\n    /// Called when the header is matched.\n    fn on_matched(&mut self, header: &HeaderMatcher, document: Document) -> Result<(), Error>;\n\n    /// Called when the header is not matched.\n    fn on_not_matched(&mut self, header: &HeaderMatcher, document: Document) -> Result<(), Error>;\n}\n\npub fn check_license_header<C: Callback>(\n    run_config: PathBuf,\n    callback: &mut C,\n) -> Result<(), Error> {\n    let config = {\n        let name = run_config.display().to_string();\n        let config = fs::read_to_string(&run_config)\n            .or_raise(|| Error::new(format!(\"cannot load config: {name}\")))?;\n        toml::from_str::<Config>(&config)\n            .or_raise(|| Error::new(format!(\"cannot parse config file: {name}\")))?\n    };\n\n    let config_dir = run_config\n        .parent()\n        .ok_or_raise(|| Error::new(\"cannot get parent directory of config file\"))?;\n\n    let basedir = config.base_dir.clone();\n    ensure!(\n        basedir.is_dir(),\n        Error::new(format!(\n            \"{} does not exist or is not a directory\",\n            basedir.display()\n        ))\n    );\n\n    let git_context = git::discover(&basedir, config.git)?;\n\n    let selected_files = {\n        let selection = Selection::new(\n            basedir,\n            config.header_path.as_ref(),\n            &config.includes,\n            &config.excludes,\n            config.use_default_excludes,\n            git_context.clone(),\n        );\n        selection.select()?\n    };\n\n    let mapping = {\n        let mut mapping = config.mapping.clone();\n        if config.use_default_mapping {\n            let default_mapping = default_mapping();\n            for m in default_mapping {\n                if let Some(o) = mapping.get(&m) {\n                    log::warn!(\"default mapping {m:?} is override by {o:?}\");\n                    continue;\n                }\n                mapping.insert(m);\n            }\n        }\n        mapping\n    };\n\n    let definitions = {\n        let mut defs = HashMap::new();\n        for (k, v) in default_headers() {\n            match defs.entry(k) {\n                Entry::Occupied(mut ent) => {\n                    log::warn!(\"Default header {} is override\", ent.key());\n                    ent.insert(v);\n                }\n                Entry::Vacant(ent) => {\n                    ent.insert(v);\n                }\n            }\n        }\n\n        for additional_header in &config.additional_headers {\n            let additional_defs = load_additional_headers(additional_header, &config, config_dir)?;\n            for (k, v) in additional_defs {\n                match defs.entry(k) {\n                    Entry::Occupied(mut ent) => {\n                        log::warn!(\"Additional header {} is override\", ent.key());\n                        ent.insert(v);\n                    }\n                    Entry::Vacant(ent) => {\n                        ent.insert(v);\n                    }\n                }\n            }\n        }\n\n        defs\n    };\n\n    let header_matcher = {\n        let header_source = load_header_sources(&config, config_dir)?;\n        HeaderMatcher::new(header_source.content)\n    };\n\n    let git_file_attrs = git::resolve_file_attrs(git_context)?;\n\n    let document_factory = DocumentFactory::new(\n        mapping,\n        definitions,\n        config.properties,\n        config.keywords,\n        git_file_attrs,\n    );\n\n    for file in selected_files {\n        let document = match document_factory.create_document(&file)? {\n            Some(document) => document,\n            None => {\n                callback.on_unknown(&file);\n                continue;\n            }\n        };\n\n        if document.is_unsupported() {\n            callback.on_unknown(&file);\n        } else if document.header_matched(&header_matcher, config.strict_check)? {\n            callback.on_matched(&header_matcher, document)?;\n        } else {\n            callback.on_not_matched(&header_matcher, document)?;\n        }\n    }\n\n    Ok(())\n}\n\nfn load_additional_headers(\n    additional_header: impl AsRef<Path>,\n    config: &Config,\n    config_dir: &Path,\n) -> Result<HashMap<String, HeaderDef>, Error> {\n    fn make_error(path: &Path) -> String {\n        format!(\"cannot load additional header {}\", path.display())\n    }\n\n    let additional_header = additional_header.as_ref();\n\n    // 1. Based on config directory.\n    let path = {\n        let mut path = config_dir.to_path_buf();\n        path.push(additional_header);\n        path\n    };\n    if let Ok(content) = fs::read_to_string(&path) {\n        return deserialize_header_definitions(content).or_raise(|| Error::new(make_error(&path)));\n    }\n\n    // 2. Based on the base_dir.\n    let path = {\n        let mut path = config.base_dir.clone();\n        path.push(additional_header);\n        path\n    };\n    if let Ok(content) = fs::read_to_string(&path) {\n        return deserialize_header_definitions(content).or_raise(|| Error::new(make_error(&path)));\n    }\n\n    // 3. Based on current working directory.\n    let path = additional_header;\n    if let Ok(content) = fs::read_to_string(path) {\n        return deserialize_header_definitions(content).or_raise(|| Error::new(make_error(path)));\n    }\n\n    bail!(Error::new(format!(\n        \"cannot find header definitions: {}\",\n        additional_header.display()\n    )))\n}\n\nfn load_header_sources(config: &Config, config_dir: &Path) -> Result<HeaderSource, Error> {\n    // 1. inline_header takes priority.\n    if let Some(content) = config.inline_header.as_ref().cloned() {\n        return Ok(HeaderSource { content });\n    }\n\n    // 2. Then, try to load from header_path.\n    let header_path = config.header_path.as_ref().ok_or_else(|| {\n        Error::new(\"no header source found (both inline_header and header_path are None)\")\n    })?;\n\n    // 2.1 Based on config directory.\n    let path = {\n        let mut path = config_dir.to_path_buf();\n        path.push(header_path);\n        path\n    };\n    if let Ok(content) = fs::read_to_string(path) {\n        return Ok(HeaderSource { content });\n    }\n\n    // 2.2 Based on the base_dir.\n    let path = {\n        let mut path = config.base_dir.clone();\n        path.push(header_path);\n        path\n    };\n    if let Ok(content) = fs::read_to_string(path) {\n        return Ok(HeaderSource { content });\n    }\n\n    // 2.3 Based on current working directory.\n    if let Ok(content) = fs::read_to_string(header_path) {\n        return Ok(HeaderSource { content });\n    }\n\n    // 3. Finally, fallback to try bundled headers.\n    bundled_headers(header_path).ok_or_raise(|| {\n        Error::new(format!(\n            \"no header source found (header_path is invalid: {header_path})\"\n        ))\n    })\n}\n"
  },
  {
    "path": "fmt/src/selection.rs",
    "content": "// Copyright 2024 tison <wander4096@gmail.com>\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nuse std::path::Path;\nuse std::path::PathBuf;\n\nuse exn::ensure;\nuse exn::Result;\nuse exn::ResultExt;\nuse ignore::overrides::OverrideBuilder;\nuse walkdir::WalkDir;\n\nuse crate::error::Error;\nuse crate::git::GitContext;\n\npub struct Selection {\n    basedir: PathBuf,\n    includes: Vec<String>,\n    excludes: Vec<String>,\n    git_context: GitContext,\n}\n\nimpl Selection {\n    pub fn new(\n        basedir: PathBuf,\n        header_path: Option<&String>,\n        includes: &[String],\n        excludes: &[String],\n        use_default_excludes: bool,\n        git_context: GitContext,\n    ) -> Selection {\n        let includes = if includes.is_empty() {\n            INCLUDES.iter().map(|s| s.to_string()).collect()\n        } else {\n            includes.to_vec()\n        };\n\n        let input_excludes = excludes;\n        let mut excludes = vec![];\n        if let Some(path) = header_path.cloned() {\n            excludes.push(path);\n        }\n        if use_default_excludes {\n            excludes.extend(EXCLUDES.iter().map(ToString::to_string));\n        }\n        excludes.extend(input_excludes.to_vec());\n\n        Selection {\n            basedir,\n            includes,\n            excludes,\n            git_context,\n        }\n    }\n\n    pub fn select(self) -> Result<Vec<PathBuf>, Error> {\n        log::debug!(\n            \"selecting files with baseDir: {}, included: {:?}, excluded: {:?}\",\n            self.basedir.display(),\n            self.includes,\n            self.excludes,\n        );\n\n        let (excludes, reverse_excludes) = {\n            let mut excludes = self.excludes;\n            let reverse_excludes = excludes\n                .extract_if(.., |pat| {\n                    if pat.starts_with('!') {\n                        pat.remove(0);\n                        true\n                    } else {\n                        false\n                    }\n                })\n                .collect::<Vec<_>>();\n            (excludes, reverse_excludes)\n        };\n\n        let includes = self.includes;\n        ensure!(\n            includes.iter().all(|pat| !pat.starts_with('!')),\n            Error::new(format!(\n                \"select files failed; reverse pattern is not allowed for includes: {includes:?}\"\n            ))\n        );\n\n        let ignore = self.git_context.config.ignore.is_auto();\n        let result = match self.git_context.repo {\n            None => select_files_with_ignore(\n                &self.basedir,\n                &includes,\n                &excludes,\n                &reverse_excludes,\n                ignore,\n            )?,\n            Some(repo) => {\n                select_files_with_git(&self.basedir, &includes, &excludes, &reverse_excludes, repo)?\n            }\n        };\n\n        log::debug!(\"selected files: {:?} (count: {})\", result, result.len());\n        Ok(result)\n    }\n}\n\nfn select_files_with_ignore(\n    basedir: &PathBuf,\n    includes: &[String],\n    excludes: &[String],\n    reverse_excludes: &[String],\n    turn_on_git_ignore: bool,\n) -> Result<Vec<PathBuf>, Error> {\n    let make_error = || Error::new(\"failed to select files with ignore crate\");\n\n    log::debug!(turn_on_git_ignore; \"Selecting files with ignore crate\");\n    let mut result = vec![];\n\n    let walker = ignore::WalkBuilder::new(basedir)\n        .ignore(false) // do not use .ignore file\n        .hidden(false) // check hidden files\n        .follow_links(true) // proper path name\n        .parents(turn_on_git_ignore)\n        .git_exclude(turn_on_git_ignore)\n        .git_global(turn_on_git_ignore)\n        .git_ignore(turn_on_git_ignore)\n        .overrides({\n            let mut builder = OverrideBuilder::new(basedir);\n            for pat in includes.iter() {\n                builder.add(pat).or_raise(make_error)?;\n            }\n            for pat in excludes.iter() {\n                let pat = format!(\"!{pat}\");\n                builder.add(pat.as_str()).or_raise(make_error)?;\n            }\n            for pat in reverse_excludes.iter() {\n                builder.add(pat).or_raise(make_error)?;\n            }\n            builder.build().or_raise(make_error)?\n        })\n        .build();\n\n    for mat in walker {\n        let mat = mat.or_raise(make_error)?;\n        if mat.file_type().map(|ft| ft.is_file()).unwrap_or(false) {\n            result.push(mat.into_path())\n        }\n    }\n\n    Ok(result)\n}\n\nfn select_files_with_git(\n    basedir: &Path,\n    includes: &[String],\n    excludes: &[String],\n    reverse_excludes: &[String],\n    repo: gix::Repository,\n) -> Result<Vec<PathBuf>, Error> {\n    log::debug!(\"selecting files with git helper\");\n    let mut result = vec![];\n\n    let matcher = {\n        let make_error = || Error::new(\"failed to select files with ignore crate\");\n\n        let mut builder = OverrideBuilder::new(basedir);\n        for pat in includes.iter() {\n            builder.add(pat).or_raise(make_error)?;\n        }\n        for pat in excludes.iter() {\n            let pat = format!(\"!{pat}\");\n            builder.add(pat.as_str()).or_raise(make_error)?;\n        }\n        for pat in reverse_excludes.iter() {\n            builder.add(pat).or_raise(make_error)?;\n        }\n        builder.build().or_raise(make_error)?\n    };\n\n    let basedir = basedir.canonicalize().or_raise(|| {\n        Error::new(format!(\n            \"cannot resolve absolute path: {}\",\n            basedir.display()\n        ))\n    })?;\n    let mut it = WalkDir::new(basedir.clone())\n        .follow_links(false)\n        .into_iter();\n\n    let workdir = repo.workdir().expect(\"workdir cannot be absent\");\n    let workdir = workdir.canonicalize().or_raise(|| {\n        Error::new(format!(\n            \"cannot resolve absolute path: {}\",\n            workdir.display()\n        ))\n    })?;\n    let worktree = repo.worktree().expect(\"worktree cannot be absent\");\n    let mut excludes = worktree\n        .excludes(None)\n        .or_raise(|| Error::new(\"cannot create gix exclude stack\"))?;\n    let index = repo\n        .index_or_empty()\n        .or_raise(|| Error::new(\"cannot open gix index\"))?;\n\n    while let Some(entry) = it.next() {\n        let entry = entry.or_raise(|| Error::new(\"cannot traverse directory\"))?;\n        let path = entry.path();\n        let file_type = entry.file_type();\n        if !file_type.is_file() && !file_type.is_dir() {\n            log::debug!(file_type:?; \"skip file: {path:?}\");\n            continue;\n        }\n\n        let rela_path = path\n            .strip_prefix(&workdir)\n            .expect(\"git repository encloses iteration\");\n        let mode = Some(if file_type.is_dir() {\n            gix::index::entry::Mode::DIR\n        } else {\n            gix::index::entry::Mode::FILE\n        });\n        let platform = excludes\n            .at_path(rela_path, mode)\n            .or_raise(|| Error::new(\"cannot check gix exclude\"))?;\n\n        if file_type.is_dir() {\n            if platform.is_excluded() {\n                let rela = gix::path::try_into_bstr(rela_path)\n                    .or_raise(|| Error::new(\"cannot convert path to git path\"))?;\n\n                if !index.path_is_directory(rela.as_ref()) {\n                    log::debug!(path:?, rela_path:?; \"skip git ignored directory\");\n                    it.skip_current_dir();\n                    continue;\n                }\n            }\n            if matcher.matched(rela_path, file_type.is_dir()).is_ignore() {\n                log::debug!(path:?, rela_path:?; \"skip glob ignored directory\");\n                it.skip_current_dir();\n                continue;\n            }\n        } else if file_type.is_file() {\n            if platform.is_excluded() {\n                let rela = gix::path::try_into_bstr(rela_path)\n                    .or_raise(|| Error::new(\"cannot convert path to git path\"))?;\n\n                if index.entry_by_path(rela.as_ref()).is_none() {\n                    log::debug!(path:?, rela_path:?; \"skip git ignored file\");\n                    continue;\n                }\n            }\n            if !matcher\n                .matched(rela_path, file_type.is_dir())\n                .is_whitelist()\n            {\n                log::debug!(path:?, rela_path:?; \"skip glob ignored file\");\n                continue;\n            }\n            result.push(path.to_path_buf());\n        }\n    }\n\n    Ok(result)\n}\n\npub const INCLUDES: [&str; 1] = [\"**\"];\npub const EXCLUDES: [&str; 140] = [\n    // Miscellaneous typical temporary files\n    \"**/*~\",\n    \"**/#*#\",\n    \"**/.#*\",\n    \"**/%*%\",\n    \"**/._*\",\n    \"**/.repository/**\",\n    \"**/*.lck\",\n    // CVS\n    \"**/CVS\",\n    \"**/CVS/**\",\n    \"**/.cvsignore\",\n    // RCS\n    \"**/RCS\",\n    \"**/RCS/**\",\n    // SCCS\n    \"**/SCCS\",\n    \"**/SCCS/**\",\n    // Visual SourceSafe\n    \"**/vssver.scc\",\n    // Subversion\n    \"**/.svn\",\n    \"**/.svn/**\",\n    // Arch\n    \"**/.arch-ids\",\n    \"**/.arch-ids/**\",\n    // Bazaar\n    \"**/.bzr\",\n    \"**/.bzr/**\",\n    // SurroundSCM\n    \"**/.MySCMServerInfo\",\n    // Mac\n    \"**/.DS_Store\",\n    // Docker\n    \".dockerignore\",\n    // Serena Dimensions Version 10\n    \"**/.metadata\",\n    \"**/.metadata/**\",\n    // Mercurial\n    \"**/.hg\",\n    \"**/.hg/**\",\n    \"**/.hgignore\",\n    // git\n    \"**/.git\",\n    \"**/.git/**\",\n    \"**/.gitattributes\",\n    \"**/.gitignore\",\n    \"**/.gitkeep\",\n    \"**/.gitmodules\",\n    // BitKeeper\n    \"**/BitKeeper\",\n    \"**/BitKeeper/**\",\n    \"**/ChangeSet\",\n    \"**/ChangeSet/**\",\n    // darcs\n    \"**/_darcs\",\n    \"**/_darcs/**\",\n    \"**/.darcsrepo\",\n    \"**/.darcsrepo/**\",\n    \"**/-darcs-backup*\",\n    \"**/.darcs-temp-mail\",\n    // maven project's temporary files\n    \"**/target/**\",\n    \"**/test-output/**\",\n    \"**/release.properties\",\n    \"**/dependency-reduced-pom.xml\",\n    \"**/release-pom.xml\",\n    \"**/pom.xml.releaseBackup\",\n    \"**/pom.xml.versionsBackup\",\n    // Node\n    \"**/node/**\",\n    \"**/node_modules/**\",\n    // Yarn\n    \"**/.yarn/**\",\n    \"**/yarn.lock\",\n    // pnpm\n    \"pnpm-lock.yaml\",\n    // Golang\n    \"**/go.sum\",\n    // Cargo\n    \"**/Cargo.lock\",\n    // code coverage tools\n    \"**/cobertura.ser\",\n    \"**/.clover/**\",\n    \"**/jacoco.exec\",\n    // eclipse project files\n    \"**/.classpath\",\n    \"**/.project\",\n    \"**/.settings/**\",\n    // IDEA project files\n    \"**/*.iml\",\n    \"**/*.ipr\",\n    \"**/*.iws\",\n    \"**/.idea/**\",\n    // Netbeans\n    \"**/nb-configuration.xml\",\n    // Hibernate Validator Annotation Processor\n    \"**/.factorypath\",\n    // descriptors\n    \"**/MANIFEST.MF\",\n    // License files\n    \"**/LICENSE\",\n    \"**/LICENSE_HEADER\",\n    // binary files - images\n    \"**/*.jpg\",\n    \"**/*.png\",\n    \"**/*.gif\",\n    \"**/*.ico\",\n    \"**/*.bmp\",\n    \"**/*.tiff\",\n    \"**/*.tif\",\n    \"**/*.cr2\",\n    \"**/*.xcf\",\n    // binary files - programs\n    \"**/*.class\",\n    \"**/*.exe\",\n    \"**/*.dll\",\n    \"**/*.so\",\n    // checksum files\n    \"**/*.md5\",\n    \"**/*.sha1\",\n    \"**/*.sha256\",\n    \"**/*.sha512\",\n    // Security files\n    \"**/*.asc\",\n    \"**/*.jks\",\n    \"**/*.keytab\",\n    \"**/*.lic\",\n    \"**/*.p12\",\n    \"**/*.pub\",\n    // binary files - archives\n    \"**/*.jar\",\n    \"**/*.zip\",\n    \"**/*.rar\",\n    \"**/*.tar\",\n    \"**/*.tar.gz\",\n    \"**/*.tar.bz2\",\n    \"**/*.gz\",\n    \"**/*.7z\",\n    // ServiceLoader files\n    \"**/META-INF/services/**\",\n    // Markdown files\n    \"**/*.md\",\n    // Office documents\n    \"**/*.xls\",\n    \"**/*.doc\",\n    \"**/*.odt\",\n    \"**/*.ods\",\n    \"**/*.pdf\",\n    // Travis\n    \"**/.travis.yml\",\n    // AppVeyor\n    \"**/.appveyor.yml\",\n    \"**/appveyor.yml\",\n    // CircleCI\n    \"**/.circleci\",\n    \"**/.circleci/**\",\n    // SourceHut\n    \"**/.build.yml\",\n    // Maven 3.3+ configs\n    \"**/jvm.config\",\n    \"**/maven.config\",\n    // Wrappers\n    \"**/gradlew\",\n    \"**/gradlew.bat\",\n    \"**/gradle-wrapper.properties\",\n    \"**/mvnw\",\n    \"**/mvnw.cmd\",\n    \"**/maven-wrapper.properties\",\n    \"**/MavenWrapperDownloader.java\",\n    // flash\n    \"**/*.swf\",\n    // json files\n    \"**/*.json\",\n    // fonts\n    \"**/*.svg\",\n    \"**/*.eot\",\n    \"**/*.otf\",\n    \"**/*.ttf\",\n    \"**/*.woff\",\n    \"**/*.woff2\",\n    // logs\n    \"**/*.log\",\n    // office documents\n    \"**/*.xlsx\",\n    \"**/*.docx\",\n    \"**/*.ppt\",\n    \"**/*.pptx\",\n];\n"
  },
  {
    "path": "fmt/tests/content/empty.py",
    "content": "# Copyright 2024 tison <wander4096@gmail.com>\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n"
  },
  {
    "path": "fmt/tests/content/two_headers.rs",
    "content": "// Copyright 2023 Greptime Team\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// This file also contains some code from prometheus project.\n// Copyright 2015 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n//! Implementations of `rate`, `increase` and `delta` functions in PromQL.\n\nuse std::fmt::Display;\nuse std::sync::Arc;\n"
  },
  {
    "path": "fmt/tests/tests.rs",
    "content": "// Copyright 2024 tison <wander4096@gmail.com>\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nuse std::path::Path;\n\nuse hawkeye_fmt::header::model::default_headers;\nuse hawkeye_fmt::header::parser::parse_header;\nuse hawkeye_fmt::header::parser::FileContent;\n\n#[test]\nfn test_remove_file_only_header() {\n    let file = Path::new(\"tests/content/empty.py\");\n    let defs = default_headers();\n    let def = defs.get(\"script_style\").unwrap().clone();\n    let keywords = vec![\"copyright\".to_string()];\n\n    let file_content = FileContent::new(file).unwrap();\n    let document = parse_header(file_content, &def, &keywords);\n    let end_pos = document.end_pos.unwrap();\n    let content = document.file_content.content();\n    assert!(content[end_pos..].trim().is_empty());\n}\n\n#[test]\nfn test_two_headers_should_only_remove_the_first() {\n    let file = Path::new(\"tests/content/two_headers.rs\");\n    let defs = default_headers();\n    let def = defs.get(\"doubleslash_style\").unwrap().clone();\n    let keywords = vec![\"copyright\".to_string()];\n\n    let file_content = FileContent::new(file).unwrap();\n    let document = parse_header(file_content, &def, &keywords);\n    let end_pos = document.end_pos.unwrap();\n    let content = document.file_content.content();\n    assert!(content[end_pos..].contains(\"Copyright 2015 The Prometheus Authors\"));\n}\n"
  },
  {
    "path": "licenserc.toml",
    "content": "# Copyright 2024 tison <wander4096@gmail.com>\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nheaderPath = \"Apache-2.0.txt\"\n\nexcludes = [\n  # Plain text files AS IS\n  \"*.txt\",\n\n  # Test files\n  \"fmt/tests/content/**\",\n  \"tests/**\",\n  \"!tests/it.py\",\n\n  # Generated files\n  \".github/workflows/release.yml\",\n]\n\n[git]\nattrs = 'auto'\nignore = 'auto'\n\n[properties]\ninceptionYear = 2024\ncopyrightOwner = \"tison <wander4096@gmail.com>\"\nprojectName = \"HawkEye\"\n"
  },
  {
    "path": "pyproject.toml",
    "content": "# Copyright 2024 tison <wander4096@gmail.com>\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n[build-system]\nrequires = [\"maturin>=1.0,<2.0\"]\nbuild-backend = \"maturin\"\n\n[tool.maturin]\nbindings = \"bin\"\nmanifest-path = \"cli/Cargo.toml\"\n"
  },
  {
    "path": "rust-toolchain.toml",
    "content": "# Copyright 2024 tison <wander4096@gmail.com>\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n[toolchain]\nchannel = \"stable\"\ncomponents = [\"cargo\", \"rustfmt\", \"clippy\", \"rust-analyzer\"]\n"
  },
  {
    "path": "rustfmt.toml",
    "content": "# Copyright 2024 tison <wander4096@gmail.com>\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimports_granularity = \"Item\"\ngroup_imports = \"StdExternalCrate\"\ncomment_width = 120\nwrap_comments = true\nformat_code_in_doc_comments = true\n"
  },
  {
    "path": "tests/.gitignore",
    "content": "*.formatted\n"
  },
  {
    "path": "tests/attrs_and_props/license.txt",
    "content": "props[\"inceptionYear\"]={{props[\"inceptionYear\"]}}\nprops[\"copyrightOwner\"]={{props[\"copyrightOwner\"]}}\n\nattrs.filename={{attrs.filename}}\nattrs.git_file_created_year={{attrs.git_file_created_year}}\nattrs.git_file_modified_year={{attrs.git_file_modified_year}}\n"
  },
  {
    "path": "tests/attrs_and_props/licenserc.toml",
    "content": "baseDir = \".\"\nheaderPath = \"license.txt\"\n\nexcludes = [\"licenserc.toml\", \"*.expected\"]\n\n[git]\nignore = 'auto'\nattrs = 'enable'\n\n[properties]\ninceptionYear = 2024\ncopyrightOwner = \"Mike Delaney <dev@mldelaney.io>\"\n"
  },
  {
    "path": "tests/attrs_and_props/main.rs",
    "content": "fn main() {\n    println!(\"Hello, world!\");\n}\n"
  },
  {
    "path": "tests/attrs_and_props/main.rs.expected",
    "content": "// props[\"inceptionYear\"]=2024\n// props[\"copyrightOwner\"]=Mike Delaney <dev@mldelaney.io>\n//\n// attrs.filename=main.rs\n// attrs.git_file_created_year=2025\n// attrs.git_file_modified_year=2025\n\nfn main() {\n    println!(\"Hello, world!\");\n}\n"
  },
  {
    "path": "tests/bom_issue/headless_bom.cs",
    "content": "﻿using System;\n\nnamespace Some.Fake.Namespace;\n\npublic class SomeFakeClass\n{\n    public void SomeFakeMethod()\n    {\n        Console.WriteLine(\"Hello, World!\");\n    }\n}\n"
  },
  {
    "path": "tests/bom_issue/headless_bom.cs.expected",
    "content": "﻿// -------------------------------------------------------\n// Static Copyright\n// -------------------------------------------------------\n\nusing System;\n\nnamespace Some.Fake.Namespace;\n\npublic class SomeFakeClass\n{\n    public void SomeFakeMethod()\n    {\n        Console.WriteLine(\"Hello, World!\");\n    }\n}\n"
  },
  {
    "path": "tests/bom_issue/license.txt",
    "content": "Static Copyright "
  },
  {
    "path": "tests/bom_issue/licenserc.toml",
    "content": "baseDir = \".\"\nheaderPath = \"license.txt\"\n\nexcludes = [\"*.toml\", \"*.expected\"]\n\nadditionalHeaders = [\"style.toml\"]\n\n[mapping.CS_TEST_STYLE]\nextensions = [\"cs\"]\n"
  },
  {
    "path": "tests/bom_issue/style.toml",
    "content": "[CS_TEST_STYLE]\nfirstLine = \"// -------------------------------------------------------\"\nendLine = \"// -------------------------------------------------------\\n\"\nskipLinePattern = \"^#!.*$\"\nallowBlankLines = false\nmultipleLines = false\nbeforeEachLine = \"// \"\nfirstLineDetectionPattern = \"//\\\\s+\\\\-+\"\nlastLineDetectionPattern = \"//\\\\s+\\\\-+\"\n"
  },
  {
    "path": "tests/disk_file_created_year/license.txt",
    "content": "Copyright {{attrs.git_file_created_year if attrs.git_file_created_year else attrs.disk_file_created_year }}"
  },
  {
    "path": "tests/disk_file_created_year/licenserc.toml",
    "content": "baseDir = \".\"\nheaderPath = \"license.txt\"\n\nexcludes = [\"licenserc.toml\", \"*.expected\"]\n"
  },
  {
    "path": "tests/disk_file_created_year/main.rs",
    "content": "fn main() {\n    println!(\"Hello, world!\");\n}\n"
  },
  {
    "path": "tests/disk_file_created_year/main.rs.expected",
    "content": "// Copyright <CURRENT_YEAR>\n\nfn main() {\n    println!(\"Hello, world!\");\n}\n"
  },
  {
    "path": "tests/it.py",
    "content": "#!/usr/bin/env python3\n\n# Copyright 2024 tison <wander4096@gmail.com>\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nfrom pathlib import Path\nimport difflib\nimport subprocess\nimport os\nimport shutil\nimport datetime\n\ndef diff_files(file1, file2):\n    with file1.open(\"r\", encoding=\"utf8\") as f1, file2.open(\"r\", encoding=\"utf8\") as f2:\n        diff = difflib.unified_diff(f1.readlines(), f2.readlines(), str(file1), str(file2))\n        diff = list(diff)\n        if diff:\n            for line in diff:\n                print(line, end=\"\")\n            exit(1)\n\n\nbasedir = Path(__file__).parent.absolute()\nrootdir = basedir.parent\n\nsubprocess.run([\"cargo\", \"build\", \"--bin\", \"hawkeye\"], cwd=rootdir, check=True)\nhawkeye = rootdir / \"target\" / \"debug\" / \"hawkeye\"\n\ndef drive(name, files, create_temp_copy=False):\n    temp_paths = []\n    expected_files = []\n    case_dir = basedir / name\n    try:\n        if create_temp_copy:\n            current_year = str(datetime.datetime.now().year)\n            for filepath in files:\n                base, ext = os.path.splitext(filepath)\n                temp_path = f\"{base}_temp{ext}\"\n                shutil.copy2(case_dir / filepath, case_dir / temp_path)\n                temp_paths.append(temp_path)\n                expected_temp_path = f\"{base}_temp{ext}.expected\"\n                shutil.copy2(case_dir / f\"{filepath}.expected\", case_dir / expected_temp_path)\n                expected_files.append(expected_temp_path)\n                with (case_dir / expected_temp_path).open(\"r\", encoding=\"utf8\") as f:\n                    content = f.read()\n                content = content.replace(\"<CURRENT_YEAR>\", current_year)\n                with (case_dir / expected_temp_path).open(\"w\", encoding=\"utf8\") as f:\n                    f.write(content)\n        else:\n            temp_paths = files\n            expected_files = [f\"{file}.expected\" for file in files]\n        subprocess.run([hawkeye, \"format\", \"--fail-if-unknown\", \"--fail-if-updated=false\", \"--dry-run\"], cwd=case_dir, check=True)\n\n        for file in temp_paths:\n            diff_files(case_dir / f\"{file}.expected\", case_dir / f\"{file}.formatted\")\n    finally:\n        # Remove all temp files at the end\n        if create_temp_copy:\n            for temp_path in temp_paths:\n                if os.path.exists(case_dir / temp_path):\n                    os.remove(case_dir / temp_path)\n            for expected_file in expected_files:\n                if os.path.exists(case_dir / expected_file):\n                    os.remove(case_dir / expected_file)\n\ndrive(\"attrs_and_props\", [\"main.rs\"])\ndrive(\"load_header_path\", [\"main.rs\"])\ndrive(\"bom_issue\", [\"headless_bom.cs\"])\ndrive(\"regression_blank_line\", [\"main.rs\"])\ndrive(\"regression_no_blank_lines\", [\"repro.py\"])\ndrive(\"disk_file_created_year\", [\"main.rs\"], True)\n"
  },
  {
    "path": "tests/load_header_path/license.txt",
    "content": "Copyright {{props[\"inceptionYear\"]}} {{props[\"copyrightOwner\"]}}\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "tests/load_header_path/licenserc.toml",
    "content": "baseDir = \".\"\nheaderPath = \"license.txt\"\n\nexcludes = [\"licenserc.toml\", \"*.expected\"]\n\n[properties]\ninceptionYear = 2024\ncopyrightOwner = \"Mike Delaney <dev@mldelaney.io>\"\n"
  },
  {
    "path": "tests/load_header_path/main.rs",
    "content": "fn main() {\n    println!(\"Hello, world!\");\n}\n"
  },
  {
    "path": "tests/load_header_path/main.rs.expected",
    "content": "// Copyright 2024 Mike Delaney <dev@mldelaney.io>\n//\n// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n//\n// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n//\n// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\n//\n// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.\n//\n// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nfn main() {\n    println!(\"Hello, world!\");\n}\n"
  },
  {
    "path": "tests/regression_blank_line/licenserc.toml",
    "content": "baseDir = \".\"\nheaderPath = \"Apache-2.0.txt\"\n\nincludes = [\"*.rs\"]\nexcludes = [\"*.expected\"]\n\n[properties]\ninceptionYear = 2023\ncopyrightOwner = \"The CopyrightOwner\"\n"
  },
  {
    "path": "tests/regression_blank_line/main.rs",
    "content": "// Copyright 2022-2023 The Authors. Licensed under Apache-2.0.\n\n//! Load balancer\n\nuse macros::define_result;\n"
  },
  {
    "path": "tests/regression_blank_line/main.rs.expected",
    "content": "// Copyright 2023 The CopyrightOwner\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n//! Load balancer\n\nuse macros::define_result;\n"
  },
  {
    "path": "tests/regression_no_blank_lines/licenserc.toml",
    "content": "# Copyright (c) 2022-2025, Example, All Rights Reserved.\n\ninlineHeader = \"Copyright (c) 2022-2025, Example, All Rights Reserved.\"\n\nbaseDir = \".\"\nincludes = [\"*.py\"]\nexcludes = [\"*.expected\"]\n"
  },
  {
    "path": "tests/regression_no_blank_lines/repro.py",
    "content": "# Copyright (c) 2022-2025, Example, All Rights Reserved.\nfrom __future__ import annotations\n\nif __name__ == \"__main__\":\n    print()\n"
  },
  {
    "path": "tests/regression_no_blank_lines/repro.py.expected",
    "content": "# Copyright (c) 2022-2025, Example, All Rights Reserved.\n\nfrom __future__ import annotations\n\nif __name__ == \"__main__\":\n    print()\n"
  }
]