Skip to content

Commit ce68be2

Browse files
sdarwincursoragent
andcommitted
Restructure policy checks into a single tool [GH #164]
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent c3a40b8 commit ce68be2

7 files changed

Lines changed: 702 additions & 679 deletions

File tree

‎.git-hooks/pre-push.py‎

Lines changed: 19 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,15 @@
3232
# pushing anything. Information about why the push is rejected may be sent to
3333
# the user by writing to standard error.
3434
#
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.
3838
#
3939

4040
import platform
41+
import shutil
4142
import subprocess
4243
import sys
43-
import tempfile
44-
45-
import edgpolicy
46-
import edgutil
47-
48-
from pathlib import Path
4944

5045
if platform.system() == 'Windows':
5146
# On Windows we need to use the kernel API to detect if this script is
@@ -68,172 +63,24 @@
6863
creationflags = subprocess.CREATE_NEW_CONSOLE
6964
)
7065
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
15266

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')
15868

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)
23277

233-
return self.errors == 0
78+
completed_process = subprocess.run([
79+
TOOL_PATH_CHECK_POLICY,
80+
'--unpushed',
81+
'--interactive'
82+
])
23483

235-
def main():
236-
checker = PrePushChecker()
237-
sys.exit(0 if checker.run() else 1)
84+
sys.exit(completed_process.returncode)
23885

23986
main()

‎CONTRIBUTING.md‎

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -81,33 +81,22 @@ existing issues and discussion (it's possible someone is already working on
8181
the item or that there has been significant discussion about the desired
8282
implementation characeristics).
8383

84-
### What CI Enforces
84+
### Automated Checks
8585

86-
Some of the items below are checked automatically on every pull request by
87-
the `Policy Check` workflow, which runs the same rules as the `pre-push`
88-
hook installed by `dev-init.py`.
89-
90-
These **fail** the check:
91-
92-
- Changed files must stay within 79 columns, must comment their `#endif`
93-
and `#else` directives, and must not contain repeated-word typos.
94-
95-
These are **reported but do not fail** the check:
96-
97-
- Commit subjects should carry a bracketed tag naming at least one GitHub
98-
issue or legacy PR, such as `[GH #213]` or `[EDGcpfe/29032]`. Whether
99-
commits that belong to no issue must still carry an empty `[]` is an
100-
open question, so this is advisory for now.
101-
- Spelling, because there is no project dictionary and a compiler code
102-
base trips `aspell` constantly. Please still read the spelling
103-
annotations; they catch real mistakes.
104-
105-
You can run the same check locally before opening a pull request:
86+
Some of the checklist below is checked automatically when you open a pull
87+
request. `edg-check-policy` is the tool that does it, and you can run it
88+
yourself at any point to see what a pull request would report:
10689

10790
```sh
10891
edg-check-policy --base-rev origin/main
10992
```
11093

94+
Commit subjects must name the work they belong to, and changed files must
95+
follow the project's coding style; both will fail the check. Spelling is
96+
reported but does not fail, because there is no project dictionary and a
97+
compiler code base trips `aspell` constantly. Please still read what it
98+
says; it catches real mistakes.
99+
111100
### Checklist for Success
112101

113102
> [!NOTE]

‎HACKING.md‎

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,26 +12,6 @@ As a result it is *highly recommended* to use the Docker based development
1212
process as this will typically provide the best experience (even if directly
1313
running a Linux-based operation system).
1414

15-
## Project Policy Checks
16-
17-
`dev-init.py` installs a `pre-push` hook that checks new commits against the
18-
project's commit message and coding style policies. The hook exists to tell
19-
you about problems before you push; it is not what enforces them. It is only
20-
present if you ran `dev-init.py`, it is skipped by `git push --no-verify`, and
21-
it never runs for work done through the GitHub web interface.
22-
23-
Enforcement happens in the `Policy Check` workflow, which applies the same
24-
rules from the same `edgpolicy` library when a pull request is opened. To
25-
run those checks yourself at any point:
26-
27-
```sh
28-
edg-check-policy --base-rev origin/main
29-
```
30-
31-
Use `--strict` to also fail on the spelling findings that the workflow only
32-
reports as warnings. See `.github/rulesets/README.md` for how the checks are
33-
tied to branch protection.
34-
3515
## Prerequisites
3616

3717
The first step (regardless of Docker use) is to make EDG development tools

0 commit comments

Comments
 (0)