|
32 | 32 | # pushing anything. Information about why the push is rejected may be sent to |
33 | 33 | # the user by writing to standard error. |
34 | 34 | # |
35 | | -# The policy this hook applies is defined in the edgpolicy library, which the |
36 | | -# policy CI workflow also uses. This hook exists to give fast local feedback; |
37 | | -# CI is what actually enforces the policy. |
| 35 | +# All of the policy itself lives in edg-check-policy, which CI and users at a |
| 36 | +# shell run too. This hook only arranges for a usable terminal and then asks |
| 37 | +# that tool to review everything that has not been pushed yet. |
38 | 38 | # |
39 | 39 |
|
40 | 40 | import platform |
| 41 | +import shutil |
41 | 42 | import subprocess |
42 | 43 | import sys |
43 | | -import tempfile |
44 | | - |
45 | | -import edgpolicy |
46 | | -import edgutil |
47 | | - |
48 | | -from pathlib import Path |
49 | 44 |
|
50 | 45 | if platform.system() == 'Windows': |
51 | 46 | # On Windows we need to use the kernel API to detect if this script is |
|
68 | 63 | creationflags = subprocess.CREATE_NEW_CONSOLE |
69 | 64 | ) |
70 | 65 | sys.exit(delegate_proc.returncode) |
71 | | -else: |
72 | | - # On Unix-like systems we can regain access to terminal input using /dev/tty. |
73 | | - sys.stdin = open('/dev/tty') |
74 | | - |
75 | | -# |
76 | | -# How each kind of finding reported by edgpolicy is introduced to the user. |
77 | | -# |
78 | | -FINDING_KIND_HEADINGS = { |
79 | | - 'misspelling': 'New misspelled words:', |
80 | | - 'typo': 'New typos:', |
81 | | - 'coding-error': 'Coding errors:', |
82 | | - 'overlong-line': 'Lines too long:' |
83 | | -} |
84 | | - |
85 | | -def print_indented(text, prefix): |
86 | | - '''Print each line of text with the given prefix.''' |
87 | | - if not text: |
88 | | - return |
89 | | - |
90 | | - for line in text.splitlines(): |
91 | | - print(f"{prefix}{line}") |
92 | | - |
93 | | -class PrePushChecker: |
94 | | - '''Runs EDG pre-push policy checks against new commits and changed files.''' |
95 | | - |
96 | | - def __init__(self): |
97 | | - self.errors = 0 |
98 | | - self.repository = edgpolicy.GitRepository(edgutil.find_mono_repo_or_exit()) |
99 | | - |
100 | | - def confirm_okay(self): |
101 | | - '''Ask the user whether a policy warning is acceptable. |
102 | | -
|
103 | | - When stdout is a terminal, prompt on /dev/tty for y/n/q. Otherwise record |
104 | | - an error without prompting (e.g. non-interactive pushes). |
105 | | - ''' |
106 | | - if not sys.stdout.isatty(): |
107 | | - self.errors = 1 |
108 | | - return |
109 | | - |
110 | | - tries = 0 |
111 | | - while True: |
112 | | - response = input(' Okay? [ynq] ').lower() |
113 | | - |
114 | | - if response == 'y': |
115 | | - return |
116 | | - if response == 'n': |
117 | | - self.errors = 1 |
118 | | - return |
119 | | - if response == 'q': |
120 | | - self.errors = 1 |
121 | | - sys.exit(1) |
122 | | - |
123 | | - print( |
124 | | - "Please answer 'y' (to accept), 'n' (to continue check) or " |
125 | | - "'q' (to quit)." |
126 | | - ) |
127 | | - tries += 1 |
128 | | - if tries >= 10: |
129 | | - print('Too many attempts.') |
130 | | - sys.exit(1) |
131 | | - |
132 | | - def check_commits(self): |
133 | | - '''Walk commits until one already on a remote branch is found. |
134 | | -
|
135 | | - As commits are walked, check to make sure they match formatting guidance. |
136 | | -
|
137 | | - Returns the oldest new commit hash, or None if there are no new commits. |
138 | | - A commit subject that fails the policy aborts the push outright when |
139 | | - edgpolicy says the rule is binding, and is otherwise offered for |
140 | | - confirmation like any other finding. |
141 | | - ''' |
142 | | - first_new_commit = None |
143 | | - |
144 | | - commits = self.repository.lines(['log', '--pretty=%H']) |
145 | | - for commit in commits: |
146 | | - # Break if the remote already has this commit. |
147 | | - contains = self.repository.run( |
148 | | - ['branch', '-r', '--contains', commit], check = False |
149 | | - ).stdout.strip() |
150 | | - if contains: |
151 | | - break |
152 | 66 |
|
153 | | - print('New commit:') |
154 | | - print('') |
155 | | - show_output = self.repository.output(['show', '--no-patch', commit]) |
156 | | - print_indented(show_output, ' ') |
157 | | - print('') |
| 67 | +TOOL_PATH_CHECK_POLICY = shutil.which('edg-check-policy') |
158 | 68 |
|
159 | | - subject_error = edgpolicy.commit_subject_error( |
160 | | - self.repository.subject_of(commit) |
161 | | - ) |
162 | | - if subject_error is not None: |
163 | | - print(f" [POLICY] {subject_error}") |
164 | | - print_indented(edgpolicy.SUBJECT_POLICY_DESCRIPTION, ' ') |
165 | | - if edgpolicy.ENFORCE_COMMIT_SUBJECT_POLICY: |
166 | | - sys.exit(1) |
167 | | - self.confirm_okay() |
168 | | - |
169 | | - first_new_commit = commit |
170 | | - |
171 | | - return first_new_commit |
172 | | - |
173 | | - def report_findings(self, findings): |
174 | | - '''Print findings grouped by kind, confirming once per group.''' |
175 | | - for kind, heading in FINDING_KIND_HEADINGS.items(): |
176 | | - messages = [ |
177 | | - message for finding_kind, message in findings |
178 | | - if finding_kind == kind |
179 | | - ] |
180 | | - if not messages: |
181 | | - continue |
182 | | - |
183 | | - print(f" {heading}") |
184 | | - for message in messages: |
185 | | - print(f" - {message}") |
186 | | - self.confirm_okay() |
187 | | - |
188 | | - def check_changed_files(self, first_new_commit): |
189 | | - '''Run per-file policy checks for files changed since |
190 | | - first_new_commit~1.''' |
191 | | - base_rev = f"{first_new_commit}~1" |
192 | | - print('Checking changed files:') |
193 | | - |
194 | | - containing_tmp_dir = self.repository.repo_dir / '.tmp' |
195 | | - containing_tmp_dir.mkdir(exist_ok = True) |
196 | | - |
197 | | - with tempfile.TemporaryDirectory( |
198 | | - dir = str(containing_tmp_dir) |
199 | | - ) as tmp_dir_str: |
200 | | - tmp_dir = Path(tmp_dir_str) |
201 | | - |
202 | | - change_spec = edgpolicy.collect_change_spec( |
203 | | - self.repository, base_rev, 'HEAD', tmp_dir |
204 | | - ) |
205 | | - |
206 | | - use_docker = edgpolicy.should_use_docker(self.repository.repo_dir) |
207 | | - results = edgpolicy.run_file_checks( |
208 | | - self.repository, change_spec, tmp_dir, use_docker = use_docker |
209 | | - ) |
210 | | - if results is None: |
211 | | - sys.exit(1) |
212 | | - |
213 | | - for file_path_str in sorted(results): |
214 | | - print(f" Checking file: {file_path_str}") |
215 | | - |
216 | | - findings = edgpolicy.summarize_file_result(results[file_path_str]) |
217 | | - if any(kind == 'spelling-unavailable' for kind, _ in findings): |
218 | | - print(' Spell check failed:') |
219 | | - if not use_docker: |
220 | | - print(' docker not used.') |
221 | | - print(' aspell not found or missing dictionary.') |
222 | | - |
223 | | - self.report_findings(findings) |
224 | | - |
225 | | - def run(self) -> bool: |
226 | | - '''Execute the full pre-push check sequence.''' |
227 | | - first_new_commit = self.check_commits() |
228 | | - |
229 | | - # Skip per file checks if we didn't actually detect a new commit. |
230 | | - if first_new_commit is not None: |
231 | | - self.check_changed_files(first_new_commit) |
| 69 | +def main(): |
| 70 | + if TOOL_PATH_CHECK_POLICY is None: |
| 71 | + print( |
| 72 | + 'edg-check-policy tool is required, but was not found.\n' |
| 73 | + 'Has dev-init.py been run, and is dev_tools/bin on PATH?', |
| 74 | + file = sys.stderr |
| 75 | + ) |
| 76 | + sys.exit(1) |
232 | 77 |
|
233 | | - return self.errors == 0 |
| 78 | + completed_process = subprocess.run([ |
| 79 | + TOOL_PATH_CHECK_POLICY, |
| 80 | + '--unpushed', |
| 81 | + '--interactive' |
| 82 | + ]) |
234 | 83 |
|
235 | | -def main(): |
236 | | - checker = PrePushChecker() |
237 | | - sys.exit(0 if checker.run() else 1) |
| 84 | + sys.exit(completed_process.returncode) |
238 | 85 |
|
239 | 86 | main() |
0 commit comments