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