-
Notifications
You must be signed in to change notification settings - Fork 813
Expand file tree
/
Copy pathcopyright.py
More file actions
executable file
·239 lines (192 loc) · 7.5 KB
/
copyright.py
File metadata and controls
executable file
·239 lines (192 loc) · 7.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
#!/usr/bin/env python
# Copyright 2021-2023, Arm Limited and Contributors
#
# SPDX-License-Identifier: Apache-2.0
#
# 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.
import argparse
import os
import re
import sys
import datetime
import io
from shutil import which
from subprocess import check_output
# Patterns to search for in the files - Order is important!
COPYRIGHT_PATTERNS = [
re.compile(r"\bCopyright \(c\)[^a-zA-Z0-9]*\b\d{4}-\d{4}\b", re.IGNORECASE),
re.compile(r"\bCopyright[^a-zA-Z0-9]*\b\d{4}-\d{4}\b", re.IGNORECASE),
re.compile(r"\bCopyright \(c\)[^a-zA-Z0-9]*\b\d{4}\b", re.IGNORECASE),
re.compile(r"\bCopyright[^a-zA-Z0-9]*\b\d{4}\b", re.IGNORECASE),
]
YEAR_PATTERN = re.compile(r"\b\d{4}\b")
YEAR_RANGE_PATTERN = re.compile(r"\b\d{4}-\d{4}\b")
EXCEPTION_FILE = ".copyrightignore"
class terminal_colors:
SUCCESS = "\033[92m"
INFO = "\033[94m"
WARNING = "\033[33m"
ERROR = "\033[91m"
END = "\033[0m"
# Get the file extension
def get_ext(file_path):
file_name = os.path.basename(file_path)
file_name, file_ext = os.path.splitext(file_name)
return file_ext
# Query files for the presence of a valid year in the header
def query_files(files):
failed_queries = {}
queries = {}
for filename in files:
with io.open(filename, "r+", encoding="utf-8") as f:
queries[filename] = None
file_contents = f.read()
for query in COPYRIGHT_PATTERNS:
try:
matches = re.findall(query, file_contents)
if len(matches) > 0:
queries[filename] = matches
break # Stop searching after the first match
except re.error as error:
failed_queries[filename] = error
return queries, failed_queries
# Check the files for the presence of a valid year in the header
def check_files(check_files):
queries, failures = query_files(check_files)
current_year = datetime.datetime.now().year
missing = []
outdated = {}
for filename, tokens in queries.items():
if not tokens:
missing.append(filename)
continue
outdated[filename] = None
for token in tokens:
copyright_years = re.findall(YEAR_PATTERN, token)
most_recent_year = int(copyright_years[-1]) if copyright_years else 0
if most_recent_year != current_year:
if not outdated[filename]:
outdated[filename] = []
outdated[filename].append(token)
outdated = {k: v for k, v in outdated.items() if v is not None}
return missing, outdated, failures
def fix(file):
queries, failures = query_files([file])
current_year = datetime.datetime.now().year
for filename, tokens in queries.items():
if not tokens:
continue
for token in tokens:
year_range = YEAR_RANGE_PATTERN.search(token)
if year_range:
year_range = year_range.group(0)
start_year, end_year = year_range.split("-")
if int(end_year) != current_year:
fixed_token = token.replace(year_range, start_year + "-" + str(current_year))
with io.open(filename, "r+", encoding="utf-8") as f:
file_contents = f.read()
file_contents = file_contents.replace(token, fixed_token)
f.seek(0)
f.write(file_contents)
f.truncate(
f.tell()
)
continue
year = YEAR_PATTERN.search(token)
if year:
year = year.group(0)
if int(year) != current_year:
fixed_token = token + "-" + str(current_year)
with io.open(filename, "r+", encoding="utf-8") as f:
file_contents = f.read()
file_contents = file_contents.replace(token, fixed_token)
f.seek(0)
f.write(file_contents)
f.truncate(
f.tell()
) # Truncate the file to the current position of the file pointer
if __name__ == "__main__":
argument_parser = argparse.ArgumentParser(
description="Check that modified files include copyright headers with current year."
)
argument_parser.add_argument(
"branch",
type=str,
default="main",
nargs="?",
help="Branch from which to compute the diff",
)
argument_parser.add_argument(
"--fix",
action="store_true",
help="Fix the files that are missing the header",
default=False,
)
args = argument_parser.parse_args()
if len(sys.argv) == 1:
argument_parser.print_help(sys.stderr)
sys.exit(1)
files = None
if not which("git"):
print(terminal_colors.ERROR + "Missing git" + terminal_colors.END)
sys.exit(1)
file_exceptions = [EXCEPTION_FILE]
try:
ignored = open(EXCEPTION_FILE).readlines()
for file in ignored:
file_exceptions.append(file.strip())
except FileNotFoundError:
pass
out = check_output(["git", "diff", args.branch, "--name-only"])
files = out.decode("utf-8").split("\n")
files_to_check = list(
filter(
lambda x: os.path.isfile(x)
and os.path.basename(x) not in file_exceptions
and get_ext(x) not in file_exceptions
and len(x) > 0,
files,
)
)
if files_to_check and len(files_to_check) > 0:
missing, outdated, failures = check_files(files_to_check)
if len(failures) > 0:
print(terminal_colors.ERROR + "Failed to search:" + terminal_colors.END)
for filename, error in failures.items():
print(filename)
print()
if len(missing) > 0:
print(terminal_colors.ERROR + "Missing copyright:" + terminal_colors.END)
for filename in missing:
print(filename)
print()
if len(outdated) > 0:
print(terminal_colors.ERROR + "Outdated copyright:" + terminal_colors.END)
for filename, tokens in outdated.items():
if args.fix:
fix(filename)
print(terminal_colors.SUCCESS + "Fixed " + filename + terminal_colors.END)
else:
print(filename)
for token in tokens:
print("\t", terminal_colors.WARNING + token + terminal_colors.END)
print()
print("\n=== Files Checked ===")
for filename in files_to_check:
print(terminal_colors.INFO + filename + terminal_colors.END)
if len(outdated) > 0 or len(missing) > 0:
sys.exit(-1)
else:
sys.exit(0)
else:
print(terminal_colors.INFO + "No files found" + terminal_colors.END)