reuseify 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- reuseify-0.1.0/PKG-INFO +140 -0
- reuseify-0.1.0/README.md +128 -0
- reuseify-0.1.0/pyproject.toml +43 -0
- reuseify-0.1.0/setup.cfg +4 -0
- reuseify-0.1.0/src/reuseify/__init__.py +6 -0
- reuseify-0.1.0/src/reuseify/annotate.py +161 -0
- reuseify-0.1.0/src/reuseify/cli.py +25 -0
- reuseify-0.1.0/src/reuseify/get_authors.py +189 -0
- reuseify-0.1.0/src/reuseify.egg-info/PKG-INFO +140 -0
- reuseify-0.1.0/src/reuseify.egg-info/SOURCES.txt +12 -0
- reuseify-0.1.0/src/reuseify.egg-info/dependency_links.txt +1 -0
- reuseify-0.1.0/src/reuseify.egg-info/entry_points.txt +2 -0
- reuseify-0.1.0/src/reuseify.egg-info/requires.txt +3 -0
- reuseify-0.1.0/src/reuseify.egg-info/top_level.txt +1 -0
reuseify-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: reuseify
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Automate REUSE license annotation from git history.
|
|
5
|
+
Project-URL: Homepage, https://github.com/sahiljhawar/reuseify
|
|
6
|
+
Project-URL: Tracker, https://github.com/sahiljhawar/reuseify/issues
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: typer>=0.12
|
|
10
|
+
Requires-Dist: rich>=13.0
|
|
11
|
+
Requires-Dist: reuse>=6.2.0
|
|
12
|
+
|
|
13
|
+
<!--
|
|
14
|
+
SPDX-FileCopyrightText: 2026 Sahil Jhawar
|
|
15
|
+
SPDX-FileContributor: Sahil Jhawar
|
|
16
|
+
|
|
17
|
+
SPDX-License-Identifier: GPL-3.0-or-later
|
|
18
|
+
-->
|
|
19
|
+
|
|
20
|
+
<!--
|
|
21
|
+
-->
|
|
22
|
+
|
|
23
|
+
# reuseify
|
|
24
|
+
[](https://api.reuse.software/info/github.com/sahiljhawar/reuseify)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
Automate [REUSE](https://reuse.software/) license annotation from git history.
|
|
28
|
+
|
|
29
|
+
`reuseify` inspects which files are missing license headers (via `reuse lint`),
|
|
30
|
+
looks up their git commit authors, and applies `reuse annotate`, all from a single CLI.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
uv pip install .
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
The workflow is two steps: collect authors → annotate files.
|
|
41
|
+
|
|
42
|
+
### Step 1: collect authors
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
reuseify get-authors [OPTIONS]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Runs `reuse lint`, finds every file missing a license header, looks up its git
|
|
49
|
+
commit authors, and writes a JSON file:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"src/foo.py": ["Alice", "Bob"],
|
|
54
|
+
"src/bar.c": ["Alice"],
|
|
55
|
+
"src/new.py": [] #NOT_IN_GIT
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
| Option | Short | Default | Description |
|
|
60
|
+
| ---------------------- | ----- | ----------------------------- | ---------------------------------------------------------------------- |
|
|
61
|
+
| `--output` | `-o` | `reuse_annotate_authors.json` | Output JSON file |
|
|
62
|
+
| `--include-not-in-git` | `-i` | off | Include files with no git history (empty author list) |
|
|
63
|
+
| `--exclude PATTERN` | `-e` | | Extra glob pattern to exclude (matched per path component, repeatable) |
|
|
64
|
+
|
|
65
|
+
Files matching built-in patterns are always excluded:
|
|
66
|
+
`__pycache__`, `.venv`, `venv`, `.env`, `env`, `.git`, `.vscode`, `.idea`,
|
|
67
|
+
`*.egg-info`, `*.pyc`, `dist`, `build`, `node_modules`, `.tox`,
|
|
68
|
+
`.mypy_cache`, `.pytest_cache`, `.ruff_cache`.
|
|
69
|
+
|
|
70
|
+
Files ignored by `.gitignore` are also excluded
|
|
71
|
+
automatically.
|
|
72
|
+
|
|
73
|
+
**Examples**
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# defaults
|
|
77
|
+
reuseify get-authors
|
|
78
|
+
|
|
79
|
+
# custom output path + include untracked files
|
|
80
|
+
reuseify get-authors --output authors.json --include-not-in-git
|
|
81
|
+
|
|
82
|
+
# add an extra exclusion pattern
|
|
83
|
+
reuseify get-authors --exclude reports --exclude "*.tmp"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
### Step 2: annotate files
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
reuseify annotate [OPTIONS] [REUSE ANNOTATE FLAGS...]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Reads the JSON file from [Step 1](#step-1-collect-authors) and calls `reuse annotate` for every file.
|
|
95
|
+
`--contributor` flags are injected automatically from the JSON data.
|
|
96
|
+
All unrecognised flags are forwarded verbatim to `reuse annotate`, giving you
|
|
97
|
+
full control over `--copyright`, `--license`, `--year`, `--style`,
|
|
98
|
+
`--fallback-dot-license`, `--force-dot-license`, `--skip-unrecognised`, etc.
|
|
99
|
+
|
|
100
|
+
| Option | Short | Default | Description |
|
|
101
|
+
| ---------------------------- | ----- | ----------------------------- | -------------------------------------------------------- |
|
|
102
|
+
| `--input` | `-i` | `reuse_annotate_authors.json` | JSON file from `get-authors` |
|
|
103
|
+
| `--default-contributor NAME` | `-d` | — | Fallback contributor for `NOT_IN_GIT` files (repeatable) |
|
|
104
|
+
|
|
105
|
+
Output is grouped: all successes first, then skips, then failures, then finally a summary.
|
|
106
|
+
|
|
107
|
+
### Examples
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
# basic
|
|
111
|
+
reuseify annotate \
|
|
112
|
+
--copyright "2025 X-Men" \
|
|
113
|
+
--license Apache-2.0 \
|
|
114
|
+
--fallback-dot-license
|
|
115
|
+
|
|
116
|
+
# custom input + fallback contributor for untracked files
|
|
117
|
+
reuseify annotate \
|
|
118
|
+
--input authors.json \
|
|
119
|
+
--default-contributor "Charles Xavier" \
|
|
120
|
+
--copyright "2025 X-Men" \
|
|
121
|
+
--license Apache-2.0
|
|
122
|
+
|
|
123
|
+
# multiple default contributors
|
|
124
|
+
reuseify annotate \
|
|
125
|
+
--default-contributor "Professor X" \
|
|
126
|
+
--default-contributor "Cyclops" \
|
|
127
|
+
--copyright "2025 X-Men" \
|
|
128
|
+
--license MIT
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Disclaimer
|
|
132
|
+
|
|
133
|
+
> [!CAUTION]
|
|
134
|
+
> Use at your own risk. `reuse annotate` modifies files in place.
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
reuse annotate --help
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
This project is not affiliated with the REUSE project or its maintainers in any way.
|
reuseify-0.1.0/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Sahil Jhawar
|
|
3
|
+
SPDX-FileContributor: Sahil Jhawar
|
|
4
|
+
|
|
5
|
+
SPDX-License-Identifier: GPL-3.0-or-later
|
|
6
|
+
-->
|
|
7
|
+
|
|
8
|
+
<!--
|
|
9
|
+
-->
|
|
10
|
+
|
|
11
|
+
# reuseify
|
|
12
|
+
[](https://api.reuse.software/info/github.com/sahiljhawar/reuseify)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
Automate [REUSE](https://reuse.software/) license annotation from git history.
|
|
16
|
+
|
|
17
|
+
`reuseify` inspects which files are missing license headers (via `reuse lint`),
|
|
18
|
+
looks up their git commit authors, and applies `reuse annotate`, all from a single CLI.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
uv pip install .
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
The workflow is two steps: collect authors → annotate files.
|
|
29
|
+
|
|
30
|
+
### Step 1: collect authors
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
reuseify get-authors [OPTIONS]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Runs `reuse lint`, finds every file missing a license header, looks up its git
|
|
37
|
+
commit authors, and writes a JSON file:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"src/foo.py": ["Alice", "Bob"],
|
|
42
|
+
"src/bar.c": ["Alice"],
|
|
43
|
+
"src/new.py": [] #NOT_IN_GIT
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
| Option | Short | Default | Description |
|
|
48
|
+
| ---------------------- | ----- | ----------------------------- | ---------------------------------------------------------------------- |
|
|
49
|
+
| `--output` | `-o` | `reuse_annotate_authors.json` | Output JSON file |
|
|
50
|
+
| `--include-not-in-git` | `-i` | off | Include files with no git history (empty author list) |
|
|
51
|
+
| `--exclude PATTERN` | `-e` | | Extra glob pattern to exclude (matched per path component, repeatable) |
|
|
52
|
+
|
|
53
|
+
Files matching built-in patterns are always excluded:
|
|
54
|
+
`__pycache__`, `.venv`, `venv`, `.env`, `env`, `.git`, `.vscode`, `.idea`,
|
|
55
|
+
`*.egg-info`, `*.pyc`, `dist`, `build`, `node_modules`, `.tox`,
|
|
56
|
+
`.mypy_cache`, `.pytest_cache`, `.ruff_cache`.
|
|
57
|
+
|
|
58
|
+
Files ignored by `.gitignore` are also excluded
|
|
59
|
+
automatically.
|
|
60
|
+
|
|
61
|
+
**Examples**
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# defaults
|
|
65
|
+
reuseify get-authors
|
|
66
|
+
|
|
67
|
+
# custom output path + include untracked files
|
|
68
|
+
reuseify get-authors --output authors.json --include-not-in-git
|
|
69
|
+
|
|
70
|
+
# add an extra exclusion pattern
|
|
71
|
+
reuseify get-authors --exclude reports --exclude "*.tmp"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
### Step 2: annotate files
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
reuseify annotate [OPTIONS] [REUSE ANNOTATE FLAGS...]
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Reads the JSON file from [Step 1](#step-1-collect-authors) and calls `reuse annotate` for every file.
|
|
83
|
+
`--contributor` flags are injected automatically from the JSON data.
|
|
84
|
+
All unrecognised flags are forwarded verbatim to `reuse annotate`, giving you
|
|
85
|
+
full control over `--copyright`, `--license`, `--year`, `--style`,
|
|
86
|
+
`--fallback-dot-license`, `--force-dot-license`, `--skip-unrecognised`, etc.
|
|
87
|
+
|
|
88
|
+
| Option | Short | Default | Description |
|
|
89
|
+
| ---------------------------- | ----- | ----------------------------- | -------------------------------------------------------- |
|
|
90
|
+
| `--input` | `-i` | `reuse_annotate_authors.json` | JSON file from `get-authors` |
|
|
91
|
+
| `--default-contributor NAME` | `-d` | — | Fallback contributor for `NOT_IN_GIT` files (repeatable) |
|
|
92
|
+
|
|
93
|
+
Output is grouped: all successes first, then skips, then failures, then finally a summary.
|
|
94
|
+
|
|
95
|
+
### Examples
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# basic
|
|
99
|
+
reuseify annotate \
|
|
100
|
+
--copyright "2025 X-Men" \
|
|
101
|
+
--license Apache-2.0 \
|
|
102
|
+
--fallback-dot-license
|
|
103
|
+
|
|
104
|
+
# custom input + fallback contributor for untracked files
|
|
105
|
+
reuseify annotate \
|
|
106
|
+
--input authors.json \
|
|
107
|
+
--default-contributor "Charles Xavier" \
|
|
108
|
+
--copyright "2025 X-Men" \
|
|
109
|
+
--license Apache-2.0
|
|
110
|
+
|
|
111
|
+
# multiple default contributors
|
|
112
|
+
reuseify annotate \
|
|
113
|
+
--default-contributor "Professor X" \
|
|
114
|
+
--default-contributor "Cyclops" \
|
|
115
|
+
--copyright "2025 X-Men" \
|
|
116
|
+
--license MIT
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Disclaimer
|
|
120
|
+
|
|
121
|
+
> [!CAUTION]
|
|
122
|
+
> Use at your own risk. `reuse annotate` modifies files in place.
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
reuse annotate --help
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
This project is not affiliated with the REUSE project or its maintainers in any way.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Sahil Jhawar
|
|
2
|
+
# SPDX-FileContributor: Sahil Jhawar
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
[build-system]
|
|
7
|
+
requires = ["setuptools>=68", "wheel"]
|
|
8
|
+
build-backend = "setuptools.build_meta"
|
|
9
|
+
|
|
10
|
+
[project]
|
|
11
|
+
name = "reuseify"
|
|
12
|
+
version = "0.1.0"
|
|
13
|
+
description = "Automate REUSE license annotation from git history."
|
|
14
|
+
readme = "README.md"
|
|
15
|
+
requires-python = ">=3.11"
|
|
16
|
+
dependencies = [
|
|
17
|
+
"typer>=0.12",
|
|
18
|
+
"rich>=13.0",
|
|
19
|
+
"reuse>=6.2.0",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
reuseify = "reuseify.cli:app"
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
where = ["src"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/sahiljhawar/reuseify"
|
|
31
|
+
Tracker = "https://github.com/sahiljhawar/reuseify/issues"
|
|
32
|
+
|
|
33
|
+
[tool.bumpver]
|
|
34
|
+
current_version = "0.1.0"
|
|
35
|
+
version_pattern = "MAJOR.MINOR.PATCH[PYTAGNUM]"
|
|
36
|
+
commit_message = "bump version {old_version} -> {new_version}"
|
|
37
|
+
commit = true
|
|
38
|
+
tag = true
|
|
39
|
+
push = true
|
|
40
|
+
|
|
41
|
+
[tool.bumpver.file_patterns]
|
|
42
|
+
"pyproject.toml" = ['version = "{version}"']
|
|
43
|
+
"src/reuseify/__init__.py" = ['__version__ = "{version}"']
|
reuseify-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Sahil Jhawar
|
|
2
|
+
# SPDX-FileContributor: Sahil Jhawar
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
"""Apply REUSE license headers to files using authors from a JSON file.
|
|
7
|
+
|
|
8
|
+
All flags not consumed by this script are forwarded verbatim to
|
|
9
|
+
`reuse annotate` (e.g. --copyright, --license, --year, --style,
|
|
10
|
+
--fallback-dot-license, --force-dot-license, --skip-unrecognised, ...).
|
|
11
|
+
The --contributor flags are populated automatically from the JSON file
|
|
12
|
+
produced by get_authors.py.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import shutil
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
from typing import Annotated
|
|
21
|
+
|
|
22
|
+
import typer
|
|
23
|
+
from rich.console import Console
|
|
24
|
+
|
|
25
|
+
console = Console()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def check_reuse() -> None:
|
|
29
|
+
if not shutil.which("reuse"):
|
|
30
|
+
console.print(
|
|
31
|
+
"[bold red]Error:[/] 'reuse' command not found. Please install it:"
|
|
32
|
+
)
|
|
33
|
+
console.print(" pip install reuse")
|
|
34
|
+
sys.exit(1)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
app = typer.Typer()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def main(
|
|
41
|
+
ctx: typer.Context,
|
|
42
|
+
input_file: Annotated[
|
|
43
|
+
str,
|
|
44
|
+
typer.Option(
|
|
45
|
+
"--input",
|
|
46
|
+
"-i",
|
|
47
|
+
help="JSON file produced by get-authors.",
|
|
48
|
+
show_default=True,
|
|
49
|
+
),
|
|
50
|
+
] = "reuse_annotate_authors.json",
|
|
51
|
+
default_contributor: Annotated[
|
|
52
|
+
list[str] | None,
|
|
53
|
+
typer.Option(
|
|
54
|
+
"--default-contributor",
|
|
55
|
+
"-d",
|
|
56
|
+
help=(
|
|
57
|
+
"Fallback contributor name(s) for files with no git history (NOT_IN_GIT). "
|
|
58
|
+
"Can be repeated for multiple names. Without this flag those files are skipped."
|
|
59
|
+
),
|
|
60
|
+
),
|
|
61
|
+
] = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Apply REUSE license headers using authors from a JSON file.
|
|
65
|
+
|
|
66
|
+
Any additional flags (not part of reuseify) are forwarded directly to `reuse annotate`.
|
|
67
|
+
|
|
68
|
+
Example:
|
|
69
|
+
reuseify annotate -i file.json --copyright-holder "John Doe"
|
|
70
|
+
"""
|
|
71
|
+
reuse_args: list[str] = ctx.args
|
|
72
|
+
_default_contributors: list[str] = default_contributor or []
|
|
73
|
+
check_reuse()
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
with open(input_file) as f:
|
|
77
|
+
authors_map: dict[str, list[str]] = json.load(f)
|
|
78
|
+
except FileNotFoundError:
|
|
79
|
+
console.print(f"[bold red]Error:[/] Input file '{input_file}' not found.")
|
|
80
|
+
console.print("Run [bold]reuseify get-authors[/] first to generate it.")
|
|
81
|
+
sys.exit(1)
|
|
82
|
+
except json.JSONDecodeError as exc:
|
|
83
|
+
console.print(f"[bold red]Error:[/] Failed to parse '{input_file}': {exc}")
|
|
84
|
+
sys.exit(1)
|
|
85
|
+
|
|
86
|
+
console.print(f"Reading authors from: [bold]{input_file}[/]")
|
|
87
|
+
|
|
88
|
+
to_annotate: list[tuple[str, list[str]]] = []
|
|
89
|
+
skipped: list[tuple[str, str]] = []
|
|
90
|
+
|
|
91
|
+
for filepath, authors in authors_map.items():
|
|
92
|
+
if not authors:
|
|
93
|
+
if _default_contributors and os.path.isfile(filepath):
|
|
94
|
+
to_annotate.append((filepath, _default_contributors))
|
|
95
|
+
else:
|
|
96
|
+
reason = "NOT_IN_GIT" + (
|
|
97
|
+
"" if not _default_contributors else " (file not found)"
|
|
98
|
+
)
|
|
99
|
+
skipped.append((filepath, reason))
|
|
100
|
+
elif not os.path.isfile(filepath):
|
|
101
|
+
skipped.append((filepath, "file not found"))
|
|
102
|
+
else:
|
|
103
|
+
to_annotate.append((filepath, authors))
|
|
104
|
+
|
|
105
|
+
console.print(
|
|
106
|
+
f"Found [bold]{len(to_annotate)}[/] file(s) to annotate, "
|
|
107
|
+
f"[bold]{len(skipped)}[/] to skip.\n"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
passed: list[str] = []
|
|
111
|
+
failed: list[tuple[str, str]] = [] # (filepath, stderr)
|
|
112
|
+
|
|
113
|
+
for filepath, authors in to_annotate:
|
|
114
|
+
contributor_flags: list[str] = []
|
|
115
|
+
for author in authors:
|
|
116
|
+
contributor_flags.extend(["--contributor", author])
|
|
117
|
+
|
|
118
|
+
cmd = ["reuse", "annotate"] + list(reuse_args) + contributor_flags + [filepath]
|
|
119
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
120
|
+
|
|
121
|
+
if result.returncode == 0:
|
|
122
|
+
passed.append(filepath)
|
|
123
|
+
else:
|
|
124
|
+
failed.append((filepath, result.stderr.strip()))
|
|
125
|
+
|
|
126
|
+
if passed:
|
|
127
|
+
console.print("[bold]Annotated:[/]")
|
|
128
|
+
for filepath in passed:
|
|
129
|
+
authors = authors_map.get(filepath, _default_contributors)
|
|
130
|
+
console.print(
|
|
131
|
+
f" [bold green]PASS[/] {filepath} [dim]({', '.join(authors)})[/]"
|
|
132
|
+
)
|
|
133
|
+
console.print()
|
|
134
|
+
|
|
135
|
+
if skipped:
|
|
136
|
+
console.print("[bold]Skipped:[/]")
|
|
137
|
+
for filepath, reason in skipped:
|
|
138
|
+
console.print(f" [yellow]SKIP[/] {filepath} [dim]({reason})[/]")
|
|
139
|
+
console.print()
|
|
140
|
+
|
|
141
|
+
if failed:
|
|
142
|
+
console.print("[bold]Failed:[/]")
|
|
143
|
+
for filepath, stderr in failed:
|
|
144
|
+
console.print(f" [bold red]FAIL[/] {filepath}")
|
|
145
|
+
if stderr:
|
|
146
|
+
console.print(f" [red]{stderr}[/]")
|
|
147
|
+
console.print()
|
|
148
|
+
|
|
149
|
+
total = len(passed) + len(skipped) + len(failed)
|
|
150
|
+
console.rule()
|
|
151
|
+
console.print(f"Total: {total}")
|
|
152
|
+
console.print(f"[green]Success: {len(passed)}[/]")
|
|
153
|
+
console.print(f"[yellow]Skipped: {len(skipped)}[/]")
|
|
154
|
+
if failed:
|
|
155
|
+
console.print(f"[red]Failed: {len(failed)}[/]")
|
|
156
|
+
else:
|
|
157
|
+
console.print(f"Failed: {len(failed)}")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if __name__ == "__main__":
|
|
161
|
+
app()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Sahil Jhawar
|
|
2
|
+
# SPDX-FileContributor: Sahil Jhawar
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
"""reuseify — top-level CLI entry point."""
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from reuseify.get_authors import main as _get_authors_cmd
|
|
11
|
+
from reuseify.annotate import main as _annotate_cmd
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(
|
|
14
|
+
name="reuseify",
|
|
15
|
+
help="Automate REUSE license annotation from git history.",
|
|
16
|
+
no_args_is_help=True,
|
|
17
|
+
rich_markup_mode="rich",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
app.command("get-authors")(_get_authors_cmd)
|
|
21
|
+
app.command("annotate", context_settings={"allow_extra_args": True, "ignore_unknown_options": True})(_annotate_cmd)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if __name__ == "__main__":
|
|
25
|
+
app()
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Sahil Jhawar
|
|
2
|
+
# SPDX-FileContributor: Sahil Jhawar
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
"""Get authors for files with missing REUSE licenses and save to a JSON file."""
|
|
7
|
+
|
|
8
|
+
import fnmatch
|
|
9
|
+
import json
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Annotated
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
DEFAULT_EXCLUDE_PATTERNS: tuple[str, ...] = (
|
|
21
|
+
"__pycache__",
|
|
22
|
+
".venv",
|
|
23
|
+
"venv",
|
|
24
|
+
".env",
|
|
25
|
+
"env",
|
|
26
|
+
".git",
|
|
27
|
+
".vscode",
|
|
28
|
+
".idea",
|
|
29
|
+
"*.egg-info",
|
|
30
|
+
"*.pyc",
|
|
31
|
+
"dist",
|
|
32
|
+
"build",
|
|
33
|
+
"node_modules",
|
|
34
|
+
".tox",
|
|
35
|
+
".mypy_cache",
|
|
36
|
+
".pytest_cache",
|
|
37
|
+
".ruff_cache",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def is_path_excluded(filepath: str, patterns: tuple[str, ...]) -> bool:
|
|
42
|
+
"""Return True if any component of *filepath* matches any glob *pattern*."""
|
|
43
|
+
return any(
|
|
44
|
+
fnmatch.fnmatch(part, pattern)
|
|
45
|
+
for part in Path(filepath).parts
|
|
46
|
+
for pattern in patterns
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def filter_git_ignored(files: list[str]) -> list[str]:
|
|
51
|
+
"""Remove files that are ignored by git (.gitignore et al.)."""
|
|
52
|
+
if not files:
|
|
53
|
+
return []
|
|
54
|
+
result = subprocess.run(
|
|
55
|
+
["git", "check-ignore", "--stdin"],
|
|
56
|
+
input="\n".join(files),
|
|
57
|
+
capture_output=True,
|
|
58
|
+
text=True,
|
|
59
|
+
)
|
|
60
|
+
ignored = set(result.stdout.splitlines())
|
|
61
|
+
return [f for f in files if f not in ignored]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def check_git_repo() -> None:
|
|
65
|
+
result = subprocess.run(
|
|
66
|
+
["git", "rev-parse", "--git-dir"],
|
|
67
|
+
capture_output=True,
|
|
68
|
+
text=True,
|
|
69
|
+
)
|
|
70
|
+
if result.returncode != 0:
|
|
71
|
+
console.print("[bold red]Error:[/] Not in a git repository.")
|
|
72
|
+
sys.exit(1)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_missing_license_files() -> list[str]:
|
|
76
|
+
result = subprocess.run(
|
|
77
|
+
["reuse", "lint"],
|
|
78
|
+
capture_output=True,
|
|
79
|
+
text=True,
|
|
80
|
+
)
|
|
81
|
+
files: list[str] = []
|
|
82
|
+
for line in (result.stdout + result.stderr).splitlines():
|
|
83
|
+
if line.strip().startswith("# SUMMARY"):
|
|
84
|
+
break
|
|
85
|
+
stripped = line.strip()
|
|
86
|
+
if stripped.startswith("* "):
|
|
87
|
+
files.append(stripped[2:])
|
|
88
|
+
return files
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_git_authors(filepath: str) -> list[str]:
|
|
92
|
+
result = subprocess.run(
|
|
93
|
+
["git", "log", "--format=%an", "--", filepath],
|
|
94
|
+
capture_output=True,
|
|
95
|
+
text=True,
|
|
96
|
+
)
|
|
97
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
98
|
+
return []
|
|
99
|
+
return sorted(set(result.stdout.strip().splitlines()))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
app = typer.Typer()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@app.command()
|
|
106
|
+
def main(
|
|
107
|
+
output: Annotated[
|
|
108
|
+
str,
|
|
109
|
+
typer.Option("--output", "-o", help="Output JSON file.", show_default=True),
|
|
110
|
+
] = "reuse_annotate_authors.json",
|
|
111
|
+
include_not_in_git: Annotated[
|
|
112
|
+
bool,
|
|
113
|
+
typer.Option(
|
|
114
|
+
"--include-not-in-git",
|
|
115
|
+
"-i",
|
|
116
|
+
help="Include files with no git history in the JSON output (empty author list).",
|
|
117
|
+
),
|
|
118
|
+
] = False,
|
|
119
|
+
exclude: Annotated[
|
|
120
|
+
list[str] | None,
|
|
121
|
+
typer.Option(
|
|
122
|
+
"--exclude",
|
|
123
|
+
"-e",
|
|
124
|
+
help=(
|
|
125
|
+
"Glob pattern to exclude (matched against each path component). "
|
|
126
|
+
"Can be repeated. Default patterns always apply: "
|
|
127
|
+
+ ", ".join(DEFAULT_EXCLUDE_PATTERNS)
|
|
128
|
+
),
|
|
129
|
+
),
|
|
130
|
+
] = None,
|
|
131
|
+
) -> None:
|
|
132
|
+
"""Get git authors for files missing REUSE license headers."""
|
|
133
|
+
_exclude: tuple[str, ...] = tuple(exclude or [])
|
|
134
|
+
check_git_repo()
|
|
135
|
+
|
|
136
|
+
console.print("Running [bold]reuse lint[/]...")
|
|
137
|
+
files = get_missing_license_files()
|
|
138
|
+
|
|
139
|
+
if not files:
|
|
140
|
+
console.print("[green]No files with licensing issues found by reuse lint.[/]")
|
|
141
|
+
sys.exit(0)
|
|
142
|
+
|
|
143
|
+
console.print(f"Found [bold]{len(files)}[/] file(s) with licensing issues.")
|
|
144
|
+
|
|
145
|
+
all_patterns = DEFAULT_EXCLUDE_PATTERNS + _exclude
|
|
146
|
+
before = len(files)
|
|
147
|
+
files = [f for f in files if not is_path_excluded(f, all_patterns)]
|
|
148
|
+
files = filter_git_ignored(files)
|
|
149
|
+
excluded_count = before - len(files)
|
|
150
|
+
if excluded_count:
|
|
151
|
+
console.print(
|
|
152
|
+
f"[dim]Excluded {excluded_count} file(s) via path patterns / .gitignore.[/]"
|
|
153
|
+
)
|
|
154
|
+
if not files:
|
|
155
|
+
console.print("[green]All remaining files were excluded.[/]")
|
|
156
|
+
sys.exit(0)
|
|
157
|
+
|
|
158
|
+
console.print("Fetching git authors...\n")
|
|
159
|
+
|
|
160
|
+
authors_map: dict[str, list[str]] = {}
|
|
161
|
+
not_in_git: list[str] = []
|
|
162
|
+
for filepath in files:
|
|
163
|
+
authors = get_git_authors(filepath)
|
|
164
|
+
if not authors:
|
|
165
|
+
not_in_git.append(filepath)
|
|
166
|
+
if include_not_in_git:
|
|
167
|
+
authors_map[filepath] = []
|
|
168
|
+
console.print(f" [yellow]{filepath}[/]: NOT_IN_GIT (included)")
|
|
169
|
+
else:
|
|
170
|
+
console.print(f" [dim]{filepath}[/]: NOT_IN_GIT (omitted)")
|
|
171
|
+
else:
|
|
172
|
+
authors_map[filepath] = authors
|
|
173
|
+
console.print(f" [cyan]{filepath}[/]: {', '.join(authors)}")
|
|
174
|
+
|
|
175
|
+
if not_in_git and not include_not_in_git:
|
|
176
|
+
console.print(
|
|
177
|
+
f"\n[yellow]Note:[/] {len(not_in_git)} file(s) with no git history were omitted. "
|
|
178
|
+
"Use [bold]--include-not-in-git[/] / [bold]-i[/] to include them."
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
with open(output, "w") as f:
|
|
182
|
+
json.dump(authors_map, f, indent=2)
|
|
183
|
+
|
|
184
|
+
console.print(f"\n[green]JSON written to:[/] {output}")
|
|
185
|
+
console.print(f"Total entries: {len(authors_map)}")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
if __name__ == "__main__":
|
|
189
|
+
app()
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: reuseify
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Automate REUSE license annotation from git history.
|
|
5
|
+
Project-URL: Homepage, https://github.com/sahiljhawar/reuseify
|
|
6
|
+
Project-URL: Tracker, https://github.com/sahiljhawar/reuseify/issues
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: typer>=0.12
|
|
10
|
+
Requires-Dist: rich>=13.0
|
|
11
|
+
Requires-Dist: reuse>=6.2.0
|
|
12
|
+
|
|
13
|
+
<!--
|
|
14
|
+
SPDX-FileCopyrightText: 2026 Sahil Jhawar
|
|
15
|
+
SPDX-FileContributor: Sahil Jhawar
|
|
16
|
+
|
|
17
|
+
SPDX-License-Identifier: GPL-3.0-or-later
|
|
18
|
+
-->
|
|
19
|
+
|
|
20
|
+
<!--
|
|
21
|
+
-->
|
|
22
|
+
|
|
23
|
+
# reuseify
|
|
24
|
+
[](https://api.reuse.software/info/github.com/sahiljhawar/reuseify)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
Automate [REUSE](https://reuse.software/) license annotation from git history.
|
|
28
|
+
|
|
29
|
+
`reuseify` inspects which files are missing license headers (via `reuse lint`),
|
|
30
|
+
looks up their git commit authors, and applies `reuse annotate`, all from a single CLI.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
uv pip install .
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
The workflow is two steps: collect authors → annotate files.
|
|
41
|
+
|
|
42
|
+
### Step 1: collect authors
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
reuseify get-authors [OPTIONS]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Runs `reuse lint`, finds every file missing a license header, looks up its git
|
|
49
|
+
commit authors, and writes a JSON file:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"src/foo.py": ["Alice", "Bob"],
|
|
54
|
+
"src/bar.c": ["Alice"],
|
|
55
|
+
"src/new.py": [] #NOT_IN_GIT
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
| Option | Short | Default | Description |
|
|
60
|
+
| ---------------------- | ----- | ----------------------------- | ---------------------------------------------------------------------- |
|
|
61
|
+
| `--output` | `-o` | `reuse_annotate_authors.json` | Output JSON file |
|
|
62
|
+
| `--include-not-in-git` | `-i` | off | Include files with no git history (empty author list) |
|
|
63
|
+
| `--exclude PATTERN` | `-e` | | Extra glob pattern to exclude (matched per path component, repeatable) |
|
|
64
|
+
|
|
65
|
+
Files matching built-in patterns are always excluded:
|
|
66
|
+
`__pycache__`, `.venv`, `venv`, `.env`, `env`, `.git`, `.vscode`, `.idea`,
|
|
67
|
+
`*.egg-info`, `*.pyc`, `dist`, `build`, `node_modules`, `.tox`,
|
|
68
|
+
`.mypy_cache`, `.pytest_cache`, `.ruff_cache`.
|
|
69
|
+
|
|
70
|
+
Files ignored by `.gitignore` are also excluded
|
|
71
|
+
automatically.
|
|
72
|
+
|
|
73
|
+
**Examples**
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# defaults
|
|
77
|
+
reuseify get-authors
|
|
78
|
+
|
|
79
|
+
# custom output path + include untracked files
|
|
80
|
+
reuseify get-authors --output authors.json --include-not-in-git
|
|
81
|
+
|
|
82
|
+
# add an extra exclusion pattern
|
|
83
|
+
reuseify get-authors --exclude reports --exclude "*.tmp"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
### Step 2: annotate files
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
reuseify annotate [OPTIONS] [REUSE ANNOTATE FLAGS...]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Reads the JSON file from [Step 1](#step-1-collect-authors) and calls `reuse annotate` for every file.
|
|
95
|
+
`--contributor` flags are injected automatically from the JSON data.
|
|
96
|
+
All unrecognised flags are forwarded verbatim to `reuse annotate`, giving you
|
|
97
|
+
full control over `--copyright`, `--license`, `--year`, `--style`,
|
|
98
|
+
`--fallback-dot-license`, `--force-dot-license`, `--skip-unrecognised`, etc.
|
|
99
|
+
|
|
100
|
+
| Option | Short | Default | Description |
|
|
101
|
+
| ---------------------------- | ----- | ----------------------------- | -------------------------------------------------------- |
|
|
102
|
+
| `--input` | `-i` | `reuse_annotate_authors.json` | JSON file from `get-authors` |
|
|
103
|
+
| `--default-contributor NAME` | `-d` | — | Fallback contributor for `NOT_IN_GIT` files (repeatable) |
|
|
104
|
+
|
|
105
|
+
Output is grouped: all successes first, then skips, then failures, then finally a summary.
|
|
106
|
+
|
|
107
|
+
### Examples
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
# basic
|
|
111
|
+
reuseify annotate \
|
|
112
|
+
--copyright "2025 X-Men" \
|
|
113
|
+
--license Apache-2.0 \
|
|
114
|
+
--fallback-dot-license
|
|
115
|
+
|
|
116
|
+
# custom input + fallback contributor for untracked files
|
|
117
|
+
reuseify annotate \
|
|
118
|
+
--input authors.json \
|
|
119
|
+
--default-contributor "Charles Xavier" \
|
|
120
|
+
--copyright "2025 X-Men" \
|
|
121
|
+
--license Apache-2.0
|
|
122
|
+
|
|
123
|
+
# multiple default contributors
|
|
124
|
+
reuseify annotate \
|
|
125
|
+
--default-contributor "Professor X" \
|
|
126
|
+
--default-contributor "Cyclops" \
|
|
127
|
+
--copyright "2025 X-Men" \
|
|
128
|
+
--license MIT
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Disclaimer
|
|
132
|
+
|
|
133
|
+
> [!CAUTION]
|
|
134
|
+
> Use at your own risk. `reuse annotate` modifies files in place.
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
reuse annotate --help
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
This project is not affiliated with the REUSE project or its maintainers in any way.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/reuseify/__init__.py
|
|
4
|
+
src/reuseify/annotate.py
|
|
5
|
+
src/reuseify/cli.py
|
|
6
|
+
src/reuseify/get_authors.py
|
|
7
|
+
src/reuseify.egg-info/PKG-INFO
|
|
8
|
+
src/reuseify.egg-info/SOURCES.txt
|
|
9
|
+
src/reuseify.egg-info/dependency_links.txt
|
|
10
|
+
src/reuseify.egg-info/entry_points.txt
|
|
11
|
+
src/reuseify.egg-info/requires.txt
|
|
12
|
+
src/reuseify.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
reuseify
|