# headerkit

headerkit keeps license and copyright headers correct across polyglot repositories. It adds missing headers, repairs outdated copyright years from git history, detects files that declare the wrong license, and fails CI when a header drifts. It understands 43 languages, including shebangs, encoding lines, XML prologs, PHP open tags, doctypes, Go build tags and Python docstrings, and it never touches generated, vendored or binary files.

## Features

- **Language registry (43)** with line, block, HTML and OCaml comment styles and per-language rules for what must stay above the header (see [docs/languages.md](docs/languages.md)).
- **Templates**: SPDX short form or standard short notices (MIT, BSD-2/3-Clause, ISC, Apache-2.0, GPL-3.0-or-later, MPL-2.0), custom templates with `{year}`, `{years}`, `{owner}`, `{project}`, `{license}`, and per-path overrides by glob.
- **Year logic**: first-last commit year per file from one `git log` pass (rename-aware), or fixed years. Existing owner lines are preserved; multiple holders are supported.
- **`check`**: missing, outdated-year, wrong-license, malformed and missing-owner findings with fix hints; text, JSON, SARIF 2.1.0 and GitHub annotation output; CI-friendly exit codes.
- **`fix`**: in-place insert/update, idempotent, preserves line endings and BOM. Wrong licenses are only rewritten with `--relicense`.
- **`report`**: license inventory by SPDX id, files lacking an SPDX tag, conflicting licenses.
- **`strip`**: remove headers.
- Skips generated files (`Code generated ... DO NOT EDIT`), vendored directories, binaries, empty files and anything git ignores. Checking 4000 files takes about a fifth of a second.

## Install

```bash
uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'
source .venv/bin/activate    # or call .venv/bin/headerkit directly
```

Requires Python 3.12+ and git (for year logic). The only dependency is `click`.

## Quickstart

```bash
./examples/make_sample_repo.sh /tmp/sample   # a polyglot repo with planted problems and dated history
cd /tmp/sample
headerkit check          # exit 1, lists every problem
headerkit fix            # repair everything that is safe to repair
headerkit check          # exit 1: only src/Main.java (wrong license) remains; see `fix --relicense` below
```

In your own repository run `headerkit init --license MIT --owner "Your Name"`, edit `headerkit.toml`, then `headerkit check`.

## Usage

Global options (before the command): `-C/--root DIR` (project root, default `.`) and `-c/--config FILE`. Every command except `init` accepts optional `PATHS` to restrict the scan to files or directories. The output below was captured on the sample repo built by `examples/make_sample_repo.sh`.

### check

```
$ headerkit check
Makefile:1: missing: no license header
    hint: run `headerkit fix` to add a MIT header
Tool.cs:1: missing: no license header
    hint: run `headerkit fix` to add a MIT header
config/pom.xml:1: missing: no license header
    hint: run `headerkit fix` to add a MIT header
public/index.php:1: missing: no license header
    hint: run `headerkit fix` to add a MIT header
scripts/build.rb:1: missing: no license header
    hint: run `headerkit fix` to add a MIT header
scripts/deploy.sh:2: malformed: license notice detected as MIT but there is no SPDX-License-Identifier line
    hint: run `headerkit fix` to rewrite the header in SPDX form
scripts/deploy.sh:5: outdated-year: copyright has 2020, git history says 2023
    hint: run `headerkit fix` to update the years
sql/schema.sql:1: missing: no license header
    hint: run `headerkit fix` to add a MIT header
src/Main.java:1: wrong-license: header declares GPL-3.0-or-later, expected MIT
    hint: run `headerkit fix --relicense` if the file really is relicensed, or add a [[override]] for its path
src/app.py:1: missing: no license header
    hint: run `headerkit fix` to add a MIT header
src/db.go:1: missing: no license header
    hint: run `headerkit fix` to add a MIT header
src/lib.rs:1: malformed: header has no SPDX tag or recognised license notice
    hint: run `headerkit fix` to add `MIT`
src/server.js:1: missing: no license header
    hint: run `headerkit fix` to add a MIT header
src/shared.py:1: missing-owner: no copyright line for Acme Robotics Ltd
    hint: run `headerkit fix` to add it
src/util.py:1: outdated-year: copyright has 2019, git history says 2019-2024
    hint: run `headerkit fix` to update the years
web/index.html:1: missing: no license header
    hint: run `headerkit fix` to add a MIT header
web/style.css:1: missing: no license header
    hint: run `headerkit fix` to add a MIT header
18 files checked, 16 with issues (17 problems)
```

Formats: `--format text|json|github|sarif`. GitHub annotations look like:

```
::error file=Makefile,line=1,title=headerkit HK001::no license header. run `headerkit fix` to add a MIT header
::error file=Tool.cs,line=1,title=headerkit HK001::no license header. run `headerkit fix` to add a MIT header
::error file=config/pom.xml,line=1,title=headerkit HK001::no license header. run `headerkit fix` to add a MIT header
```

Exit codes: 0 clean, 1 problems found, 2 usage or configuration error.

### fix

```
$ headerkit fix
fixed Makefile (missing)
fixed Tool.cs (missing)
fixed config/pom.xml (missing)
fixed public/index.php (missing)
fixed scripts/build.rb (missing)
fixed scripts/deploy.sh (malformed, outdated-year)
fixed sql/schema.sql (missing)
skipped src/Main.java: header declares GPL-3.0-or-later, expected MIT (use --relicense or an [[override]])
fixed src/app.py (missing)
fixed src/db.go (missing)
fixed src/lib.rs (malformed)
fixed src/server.js (missing)
fixed src/shared.py (missing-owner)
fixed src/util.py (outdated-year)
fixed web/index.html (missing)
fixed web/style.css (missing)
15 files changed, 1 skipped
```

`src/Main.java` declares GPL-3.0-or-later in an MIT project, so it is reported and left alone. After reviewing it, `--relicense` replaces it:

```
$ headerkit fix --relicense
fixed src/Main.java (wrong-license)
1 files changed, 0 skipped
```

Changes are minimal. A stale year is edited in place, and a free-form permission notice is replaced by the SPDX form:

```diff
diff --git a/scripts/deploy.sh b/scripts/deploy.sh
--- a/scripts/deploy.sh
+++ b/scripts/deploy.sh
@@ -1,8 +1,6 @@
 #!/bin/sh
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software, to deal in the Software without restriction.
-#
-# Copyright 2020 Acme Robotics Ltd
+# Copyright 2020-2023 Acme Robotics Ltd
+# SPDX-License-Identifier: MIT
 
 set -eu
 echo "deploying"
diff --git a/src/util.py b/src/util.py
--- a/src/util.py
+++ b/src/util.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2019 Acme Robotics Ltd
+# Copyright (c) 2019-2024 Acme Robotics Ltd
 # SPDX-License-Identifier: MIT
 
 def greet(name: str) -> str:
```

`--dry-run` lists what would change without writing. Afterwards:

```
$ headerkit check
src/Main.java:1: wrong-license: header declares GPL-3.0-or-later, expected MIT
    hint: run `headerkit fix --relicense` if the file really is relicensed, or add a [[override]] for its path
18 files checked, 1 with issues (1 problems)
```

### report

```
$ headerkit report
Scanned 18 files (skipped: excluded 2, generated 1, unsupported 1)

Licenses by SPDX identifier
  MIT                                   3
  GPL-3.0-or-later                      1
  Apache-2.0 (notice, no SPDX tag)      1
  MIT (notice, no SPDX tag)             1
  (copyright only, no license text)     1
  (no header)                          11

Languages
  python 3, makefile 1, csharp 1, xml 1, c 1, php 1, ruby 1, shell 1, sql 1, java 1, go 1, typescript 1, rust 1, javascript 1, html 1, css 1

Files lacking an SPDX tag (14)
  Makefile
  Tool.cs
  config/pom.xml
  legacy/old.c
  public/index.php
  scripts/build.rb
  scripts/deploy.sh
  sql/schema.sql
  src/app.py
  src/db.go
  src/lib.rs
  src/server.js
  web/index.html
  web/style.css

Conflicts (1)
  copyleft license GPL-3.0-or-later inside a project licensed MIT
```

`report --format json` gives the same data as JSON. `--fail-on-conflict` exits 1 if conflicts exist (copyleft inside a permissive project, GPL-2.0-only mixed with Apache-2.0 or GPL-3.0).

### strip

`headerkit strip [--dry-run] [PATHS]` removes detected license headers and the blank line that followed them.

## Configuration

```toml
[headerkit]
license = "MIT"
style = "spdx"                 # or "notice"
owner = ["Acme Robotics Ltd"]  # one or several holders
project = "Acme Toolkit"
year_mode = "git"              # or "fixed" with year = "2020-2024"
exclude = ["docs/**"]

[[override]]
glob = "legacy/**"
license = "Apache-2.0"
style = "notice"
```

All keys, glob rules, templates and output formats are described in [docs/configuration.md](docs/configuration.md).

## Architecture

| Module | Responsibility |
|---|---|
| `src/headerkit/languages.py` | Language registry and detection by name, extension and shebang. |
| `src/headerkit/header.py` | Preamble handling, locating and parsing the existing header, copyright-line parsing, comment rendering. |
| `src/headerkit/licenses.py` | Short notice templates, license signatures, SPDX expression helpers. |
| `src/headerkit/config.py` | TOML config, per-path rule resolution, glob matching. |
| `src/headerkit/years.py` | Git history year ranges (rename-aware) and the year provider. |
| `src/headerkit/engine.py` | `analyze`, `fix` and `strip`. |
| `src/headerkit/scan.py` | File discovery and skip rules. |
| `src/headerkit/report.py` | License inventory and conflict detection. |
| `src/headerkit/output.py` | Text, JSON, GitHub and SARIF formatters. |
| `src/headerkit/cli.py` | Click commands. |

Algorithms and worked examples: [docs/design.md](docs/design.md).

## Tests

```bash
.venv/bin/pytest -q
```

132 tests: golden insertion files per language and licence style (`tests/golden/`), git-history year ranges against temporary repositories (renames, subdirectory roots), idempotency, license detection, config validation and CLI behaviour end to end.

## Limitations

- Only the first comment of a file is treated as the header.
- Rebuilding a malformed header keeps copyright lines but drops other lines inside the old header.
- Years reflect committed history; uncommitted edits are ignored on purpose so that `fix` stays idempotent. In CI use a full clone (`fetch-depth: 0`).
- Notices are short standard boilerplate and pointers to the LICENSE file, not full license texts.
- Non-UTF-8 files are skipped. JSON, Markdown and other comment-less formats are not handled.
- Conflict detection is a small heuristic table, not legal advice.

## License

MIT, see [LICENSE](LICENSE).
