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.
@@ -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
+ [![REUSE status](https://api.reuse.software/badge/github.com/sahiljhawar/reuseify)](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,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
+ [![REUSE status](https://api.reuse.software/badge/github.com/sahiljhawar/reuseify)](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}"']
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ # SPDX-FileCopyrightText: 2026 Sahil Jhawar
2
+ # SPDX-FileContributor: Sahil Jhawar
3
+ #
4
+ # SPDX-License-Identifier: GPL-3.0-or-later
5
+
6
+ __version__ = "0.1.0"
@@ -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
+ [![REUSE status](https://api.reuse.software/badge/github.com/sahiljhawar/reuseify)](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,2 @@
1
+ [console_scripts]
2
+ reuseify = reuseify.cli:app
@@ -0,0 +1,3 @@
1
+ typer>=0.12
2
+ rich>=13.0
3
+ reuse>=6.2.0
@@ -0,0 +1 @@
1
+ reuseify