239 lines
7.5 KiB
Python
239 lines
7.5 KiB
Python
#!/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) |