summaryrefslogtreecommitdiffstats
path: root/release/check-metadata.py
diff options
context:
space:
mode:
authorMike Frysinger <vapier@google.com>2025-08-21 10:40:51 -0400
committerLUCI <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com>2025-08-21 11:16:35 -0700
commit80d1a5ad3ec862c64a3bbe9919d4547340950183 (patch)
treece00fff8d509cb9292dcd0e42922b8235fde224c /release/check-metadata.py
parentc615c964fb0c40f1ff2b70681336d0d5d89ddcd7 (diff)
downloadgit-repo-80d1a5ad3ec862c64a3bbe9919d4547340950183.tar.gz
run_tests: add file header checker for licensing blocks
Change-Id: Ic0bfa3b03e2ba46d565a5bc2c1b7a7463b7dca2c Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/500103 Commit-Queue: Mike Frysinger <vapier@google.com> Tested-by: Mike Frysinger <vapier@google.com> Reviewed-by: Scott Lee <ddoman@google.com>
Diffstat (limited to 'release/check-metadata.py')
-rwxr-xr-xrelease/check-metadata.py152
1 files changed, 152 insertions, 0 deletions
diff --git a/release/check-metadata.py b/release/check-metadata.py
new file mode 100755
index 00000000..e17932da
--- /dev/null
+++ b/release/check-metadata.py
@@ -0,0 +1,152 @@
1#!/usr/bin/env python3
2# Copyright (C) 2025 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16"""Helper tool to check various metadata (e.g. licensing) in source files."""
17
18import argparse
19from pathlib import Path
20import re
21import sys
22
23import util
24
25
26_FILE_HEADER_RE = re.compile(
27 r"""# Copyright \(C\) 20[0-9]{2} The Android Open Source Project
28#
29# Licensed under the Apache License, Version 2\.0 \(the "License"\);
30# you may not use this file except in compliance with the License\.
31# You may obtain a copy of the License at
32#
33# http://www\.apache\.org/licenses/LICENSE-2\.0
34#
35# Unless required by applicable law or agreed to in writing, software
36# distributed under the License is distributed on an "AS IS" BASIS,
37# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\.
38# See the License for the specific language governing permissions and
39# limitations under the License\.
40"""
41)
42
43
44def check_license(path: Path, lines: list[str]) -> bool:
45 """Check license header."""
46 # Enforce licensing on configs & scripts.
47 if not (
48 path.suffix in (".bash", ".cfg", ".ini", ".py", ".toml")
49 or lines[0] in ("#!/bin/bash", "#!/bin/sh", "#!/usr/bin/env python3")
50 ):
51 return True
52
53 # Extract the file header.
54 header_lines = []
55 for line in lines:
56 if line.startswith("#"):
57 header_lines.append(line)
58 else:
59 break
60 if not header_lines:
61 print(
62 f"error: {path.relative_to(util.TOPDIR)}: "
63 "missing file header (copyright+licensing)",
64 file=sys.stderr,
65 )
66 return False
67
68 # Skip the shebang.
69 if header_lines[0].startswith("#!"):
70 header_lines.pop(0)
71
72 # If this file is imported into the tree, then leave it be.
73 if header_lines[0] == "# DO NOT EDIT THIS FILE":
74 return True
75
76 header = "".join(f"{x}\n" for x in header_lines)
77 if not _FILE_HEADER_RE.match(header):
78 print(
79 f"error: {path.relative_to(util.TOPDIR)}: "
80 "file header incorrectly formatted",
81 file=sys.stderr,
82 )
83 print(
84 "".join(f"> {x}\n" for x in header_lines), end="", file=sys.stderr
85 )
86 return False
87
88 return True
89
90
91def check_path(opts: argparse.Namespace, path: Path) -> bool:
92 """Check a single path."""
93 data = path.read_text(encoding="utf-8")
94 lines = data.splitlines()
95 # NB: Use list comprehension and not a generator so we run all the checks.
96 return all(
97 [
98 check_license(path, lines),
99 ]
100 )
101
102
103def check_paths(opts: argparse.Namespace, paths: list[Path]) -> bool:
104 """Check all the paths."""
105 # NB: Use list comprehension and not a generator so we check all paths.
106 return all([check_path(opts, x) for x in paths])
107
108
109def find_files(opts: argparse.Namespace) -> list[Path]:
110 """Find all the files in the source tree."""
111 result = util.run(
112 opts,
113 ["git", "ls-tree", "-r", "-z", "--name-only", "HEAD"],
114 cwd=util.TOPDIR,
115 capture_output=True,
116 encoding="utf-8",
117 )
118 return [util.TOPDIR / x for x in result.stdout.split("\0")[:-1]]
119
120
121def get_parser() -> argparse.ArgumentParser:
122 """Get a CLI parser."""
123 parser = argparse.ArgumentParser(description=__doc__)
124 parser.add_argument(
125 "-n",
126 "--dry-run",
127 dest="dryrun",
128 action="store_true",
129 help="show everything that would be done",
130 )
131 parser.add_argument(
132 "paths",
133 nargs="*",
134 help="the paths to scan",
135 )
136 return parser
137
138
139def main(argv: list[str]) -> int:
140 """The main func!"""
141 parser = get_parser()
142 opts = parser.parse_args(argv)
143
144 paths = opts.paths
145 if not opts.paths:
146 paths = find_files(opts)
147
148 return 0 if check_paths(opts, paths) else 1
149
150
151if __name__ == "__main__":
152 sys.exit(main(sys.argv[1:]))