Skip to content

Commit 150c4e0

Browse files
committed
[ci] Add copyright check
1 parent a9ccdc7 commit 150c4e0

File tree

2 files changed

+192
-0
lines changed

2 files changed

+192
-0
lines changed
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2025 Irreducible Inc.
3+
"""Check and fix copyright headers in Rust files."""
4+
5+
import argparse
6+
import re
7+
import subprocess
8+
import sys
9+
from collections import defaultdict
10+
from datetime import datetime
11+
from pathlib import Path
12+
13+
YEAR = datetime.now().year
14+
COMPANY = "Irreducible Inc."
15+
VALID_PATTERN = re.compile(rf"^// Copyright (\d{{4}}-)?{YEAR} {re.escape(COMPANY)}$")
16+
17+
18+
def get_rust_files(dirname):
19+
"""Get all Rust files in crates directory using git ls-files."""
20+
try:
21+
result = subprocess.run(
22+
["git", "ls-files", f"{dirname}/"],
23+
capture_output=True,
24+
text=True,
25+
check=True,
26+
)
27+
rust_files = [f for f in result.stdout.strip().split("\n") if f.endswith(".rs")]
28+
return sorted(rust_files)
29+
except subprocess.CalledProcessError:
30+
return sorted(str(p) for p in Path(dirname).rglob("*.rs"))
31+
32+
33+
def check_file(filepath):
34+
"""Get first line of file, or full lines if return_lines=True."""
35+
try:
36+
with open(filepath, "r", encoding="utf-8") as f:
37+
return f.readline().strip()
38+
except Exception:
39+
return None
40+
41+
42+
def fix_file(filepath, old_header=None):
43+
"""Fix copyright header in file."""
44+
try:
45+
with open(filepath, "r", encoding="utf-8") as f:
46+
lines = f.readlines()
47+
48+
# Determine new header
49+
header = f"// Copyright {YEAR} {COMPANY}\n"
50+
if old_header and "Irreducible" in old_header:
51+
# Preserve year range if exists
52+
if match := re.search(r"(\d{4})", old_header):
53+
if (year := match.group(1)) != str(YEAR):
54+
header = f"// Copyright {year}-{YEAR} {COMPANY}\n"
55+
56+
# Update or insert header
57+
if lines and lines[0].startswith("// Copyright") and "Irreducible" in lines[0]:
58+
lines[0] = header
59+
else:
60+
lines.insert(0, header)
61+
62+
with open(filepath, "w", encoding="utf-8") as f:
63+
f.writelines(lines)
64+
return True
65+
except Exception as e:
66+
print(f" Error: {filepath}: {e}", file=sys.stderr)
67+
return False
68+
69+
70+
def main():
71+
parser = argparse.ArgumentParser(
72+
description="Check and fix copyright headers in Rust files"
73+
)
74+
parser.add_argument(
75+
"--fix",
76+
action="store_true",
77+
help="Fix missing and wrong format copyright headers",
78+
)
79+
args = parser.parse_args()
80+
81+
files = get_rust_files("crates")
82+
if not files:
83+
print("No Rust files found in crates/")
84+
return 1
85+
86+
# Group files by their first line
87+
headers = defaultdict(list)
88+
for filepath in files:
89+
first_line = check_file(filepath)
90+
key = (
91+
first_line
92+
if first_line and first_line.startswith("// Copyright")
93+
else "[NO COPYRIGHT]"
94+
)
95+
headers[key].append(filepath)
96+
97+
# Categorize headers
98+
categories = {"correct": {}, "other": {}, "wrong": {}, "missing": {}}
99+
100+
for header, file_list in headers.items():
101+
if header == "[NO COPYRIGHT]":
102+
categories["missing"][header] = file_list
103+
elif VALID_PATTERN.match(header):
104+
categories["correct"][header] = file_list
105+
elif "Irreducible" in header:
106+
categories["wrong"][header] = file_list
107+
else:
108+
categories["other"][header] = file_list
109+
110+
# Print results
111+
print(f"Expected: // Copyright {YEAR} {COMPANY}")
112+
print(f" or: // Copyright YYYY-{YEAR} {COMPANY}")
113+
print("=" * 70)
114+
115+
sections = [
116+
("✅ CORRECT COPYRIGHT", categories["correct"], False),
117+
("🔸 OTHER LICENSE (not Irreducible)", categories["other"], False),
118+
("⚠️ WRONG COPYRIGHT FORMAT (has Irreducible)", categories["wrong"], True),
119+
("❌ NO COPYRIGHT", categories["missing"], True),
120+
]
121+
122+
for title, group, show_files in sections:
123+
if group:
124+
print(f"\n{title}\n{'-' * 70}")
125+
for header, file_list in sorted(group.items(), key=lambda x: -len(x[1])):
126+
print(f"{len(file_list):6} {header}")
127+
if show_files:
128+
for filepath in file_list:
129+
print(f" {filepath}")
130+
131+
# Summary
132+
totals = {k: sum(len(f) for f in v.values()) for k, v in categories.items()}
133+
print("\n" + "=" * 70)
134+
print(f"✅ Correct: {totals['correct']:4}")
135+
print(f"🔸 Other: {totals['other']:4}")
136+
print(f"⚠️ Wrong format: {totals['wrong']:4}")
137+
print(f"❌ No copyright: {totals['missing']:4}")
138+
print("-" * 20)
139+
print(f"📊 TOTAL: {len(files):4}")
140+
141+
# Fix if requested
142+
to_fix = totals["wrong"] + totals["missing"]
143+
if args.fix and to_fix > 0:
144+
print(f"\n{'=' * 70}\n🔧 FIXING FILES...\n{'=' * 70}")
145+
146+
fixed = 0
147+
for cat_name, needs_old in [("wrong", True), ("missing", False)]:
148+
for header, file_list in categories[cat_name].items():
149+
for filepath in file_list:
150+
if fix_file(filepath, header if needs_old else None):
151+
fixed += 1
152+
action = "Fixed" if needs_old else "Added"
153+
print(f" ✅ {action}: {filepath}")
154+
155+
print(f"\n{'-' * 70}\n🔧 Fixed: {fixed}/{to_fix} files")
156+
return 0 if fixed == to_fix else 1
157+
158+
if to_fix > 0:
159+
if not args.fix:
160+
print(f"\n💡 Run with --fix to automatically fix {to_fix} files")
161+
return 1
162+
return 0
163+
164+
165+
if __name__ == "__main__":
166+
exit(main())

.github/workflows/ci.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
types: [opened, synchronize, reopened]
8+
workflow_dispatch:
9+
10+
concurrency:
11+
group: ${{ github.event_name }}-${{ github.ref }}
12+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
13+
14+
jobs:
15+
copyright-check:
16+
name: copyright-check
17+
runs-on: ubuntu-latest
18+
steps:
19+
- name: Checkout Repository
20+
uses: actions/checkout@v4
21+
- name: Setup Python
22+
uses: actions/setup-python@v5
23+
with:
24+
python-version: "3.13"
25+
- name: Check copyright notices
26+
run: python3 .github/scripts/check_copyright_notice.py

0 commit comments

Comments
 (0)