Skip to content

Commit 9da33b1

Browse files
committed
pre-commit: validate hooks and auto create docs with Python
1 parent 867c93d commit 9da33b1

6 files changed

Lines changed: 322 additions & 0 deletions

File tree

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
#!/usr/bin/env python3
2+
3+
import yaml
4+
import sys # Import the sys module to access command-line arguments
5+
6+
7+
def generate_pre_commit_table(yaml_path):
8+
"""
9+
Generates a Markdown table from a pre-commit-config.yaml file.
10+
11+
Args:
12+
yaml_path (str): The path to the pre-commit-config.yaml file.
13+
14+
Returns:
15+
str: A Markdown formatted table string or an error message.
16+
"""
17+
try:
18+
with open(yaml_path, 'r', encoding='utf-8') as f: # Added encoding for better compatibility
19+
config = yaml.safe_load(f)
20+
except FileNotFoundError:
21+
return f"Error: The file '{yaml_path}' was not found."
22+
except yaml.YAMLError as e:
23+
return f"Error parsing YAML file '{yaml_path}': {e}"
24+
except Exception as e:
25+
return f"An unexpected error occurred while reading '{yaml_path}': {e}"
26+
27+
table_header = "| Hook ID | Language | Name | Description | Version |\n"
28+
table_separator = "|---|---|---|---|---|\n"
29+
table_rows = []
30+
31+
# Ensure config is a dictionary and has a 'repos' key that is a list
32+
if not isinstance(config, dict) or 'repos' not in config or not isinstance(config['repos'], list):
33+
return f"Error: Invalid pre-commit config structure in '{yaml_path}'. 'repos' section is missing or not a list."
34+
35+
for repo_index, repo in enumerate(config.get("repos", [])):
36+
if not isinstance(repo, dict):
37+
print(f"Warning: Repository at index {repo_index} is not a valid dictionary. Skipping.")
38+
continue
39+
40+
version = repo.get("rev", "N/A")
41+
url = repo.get("repo", "N/A")
42+
43+
if 'hooks' not in repo or not isinstance(repo['hooks'], list):
44+
print(
45+
f"Warning: 'hooks' section not found or is not a list for repo '{url}'. Skipping hooks processing for this repo.")
46+
continue
47+
48+
for hook_index, hook in enumerate(repo.get("hooks", [])):
49+
if not isinstance(hook, dict):
50+
print(f"Warning: Hook at index {hook_index} in repo '{url}' is not a valid dictionary. Skipping.")
51+
continue
52+
53+
hook_id = hook.get("id", "N/A")
54+
name = hook.get("name", "N/A")
55+
description = hook.get("description", "N/A")
56+
language = hook.get("language", "N/A")
57+
58+
# Construct the entry for the Hook ID column
59+
if url and url not in ["local", "meta"]:
60+
entry = f"[{hook_id}]({url})"
61+
else:
62+
entry = f"{hook_id}"
63+
64+
table_rows.append(f"| {entry} | {language} | {name} | {description} | {version} |\n")
65+
66+
if not table_rows:
67+
return f"No hooks found in '{yaml_path}' to generate a table."
68+
69+
return table_header + table_separator + "".join(table_rows)
70+
71+
72+
def create_markdown_file(target_file_path, content_to_append):
73+
"""
74+
Creates or overwrites a Markdown file with the provided content.
75+
76+
Args:
77+
target_file_path (str): The path to the output Markdown file.
78+
content_to_append (str): The Markdown content to write to the file.
79+
80+
Returns:
81+
str: A success message or an error message.
82+
"""
83+
try:
84+
# Ensure the directory exists before writing the file
85+
import os
86+
os.makedirs(os.path.dirname(target_file_path), exist_ok=True)
87+
88+
with open(target_file_path, "w", encoding='utf-8') as f: # Changed to 'w' to overwrite, added encoding
89+
f.write("# pre-commit hook documentation\n\n")
90+
f.write(content_to_append)
91+
return f"File content successfully created at '{target_file_path}'."
92+
except OSError as e:
93+
return f"Error creating file '{target_file_path}': {e}"
94+
except Exception as e:
95+
return f"An unexpected error occurred while writing to '{target_file_path}': {e}"
96+
97+
98+
if __name__ == "__main__":
99+
# Check if a command-line argument for the config file path is provided
100+
if len(sys.argv) > 1:
101+
pre_commit_yaml_path = sys.argv[1]
102+
else:
103+
# Default file path if no argument is provided
104+
pre_commit_yaml_path = ".pre-commit-config.yaml"
105+
print(f"No pre-commit config file path provided. Attempting to use default: '{pre_commit_yaml_path}'")
106+
107+
output_markdown_path = "doc/guides/pre-commit-hooks.md"
108+
109+
# Generate the Markdown table
110+
markdown_table = generate_pre_commit_table(pre_commit_yaml_path)
111+
112+
# Add the table to the target Markdown file if no error occurred during table generation
113+
if markdown_table.startswith("Error:"):
114+
print(markdown_table) # Print the error message from generate_pre_commit_table
115+
sys.exit(1) # Exit with a non-zero code to indicate failure
116+
elif markdown_table.startswith("No hooks found:"):
117+
print(markdown_table) # Print the message if no hooks are found
118+
sys.exit(0) # Exit with success if no hooks but no other error
119+
else:
120+
result = create_markdown_file(output_markdown_path, markdown_table)
121+
print(result)
122+
if "Error" in result:
123+
sys.exit(1) # Exit with failure if create_markdown_file had an error
124+
else:
125+
sys.exit(0) # Exit with success
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#!/usr/bin/env python3
2+
3+
import yaml
4+
import sys
5+
6+
# Define a constant for the default pre-commit config filename
7+
DEFAULT_PRE_COMMIT_CONFIG_FILE = "pre-commit-config.yaml"
8+
9+
def validate_pre_commit_config(file_path):
10+
"""
11+
Validates a pre-commit-config.yaml file to ensure all hooks have
12+
'name' and 'description' keys.
13+
14+
Args:
15+
file_path (str): The path to the pre-commit-config.yaml file.
16+
17+
Returns:
18+
bool: True if all hooks are valid, False otherwise.
19+
"""
20+
try:
21+
with open(file_path, 'r', encoding='utf-8') as f:
22+
config = yaml.safe_load(f)
23+
except FileNotFoundError:
24+
print(f"Error: The file '{file_path}' was not found.")
25+
return False
26+
except yaml.YAMLError as e:
27+
print(f"Error: Could not parse YAML file '{file_path}'. Please check its syntax.")
28+
print(f"Details: {e}")
29+
return False
30+
except Exception as e:
31+
print(f"An unexpected error occurred while reading the file: {e}")
32+
return False
33+
34+
if not isinstance(config, dict):
35+
print(f"Error: The content of '{file_path}' is not a valid YAML dictionary.")
36+
return False
37+
38+
if 'repos' not in config or not isinstance(config['repos'], list):
39+
print(f"Error: 'repos' section not found or is not a list in '{file_path}'.")
40+
return False
41+
42+
all_hooks_valid = True
43+
for repo_index, repo in enumerate(config['repos']):
44+
if not isinstance(repo, dict):
45+
print(f"Warning: Repository at index {repo_index} is not a valid dictionary. Skipping.")
46+
all_hooks_valid = False
47+
continue
48+
49+
repo_url = repo.get('repo', 'Unknown Repo URL')
50+
51+
if 'hooks' not in repo or not isinstance(repo['hooks'], list):
52+
print(
53+
f"Warning: 'hooks' section not found or is not a list for repo '{repo_url}'. Skipping hooks validation for this repo.")
54+
all_hooks_valid = False
55+
continue
56+
57+
for hook_index, hook in enumerate(repo['hooks']):
58+
if not isinstance(hook, dict):
59+
print(f"Warning: Hook at index {hook_index} in repo '{repo_url}' is not a valid dictionary. Skipping.")
60+
all_hooks_valid = False
61+
continue
62+
63+
hook_id = hook.get('id', 'Unknown ID')
64+
65+
# Check for 'name' key
66+
if 'name' not in hook:
67+
print(
68+
f"Validation Error: Hook '{hook_id}' (index {hook_index}) in repo '{repo_url}' is missing the 'name' key.")
69+
all_hooks_valid = False
70+
elif not isinstance(hook['name'], str) or not hook['name'].strip():
71+
print(
72+
f"Validation Error: Hook '{hook_id}' (index {hook_index}) in repo '{repo_url}' has an empty or invalid 'name' key.")
73+
all_hooks_valid = False
74+
75+
# Check for 'description' key
76+
if 'description' not in hook:
77+
print(
78+
f"Validation Error: Hook '{hook_id}' (index {hook_index}) in repo '{repo_url}' is missing the 'description' key.")
79+
all_hooks_valid = False
80+
elif not isinstance(hook['description'], str) or not hook['description'].strip():
81+
print(
82+
f"Validation Error: Hook '{hook_id}' (index {hook_index}) in repo '{repo_url}' has an empty or invalid 'description' key.")
83+
all_hooks_valid = False
84+
85+
return all_hooks_valid
86+
87+
88+
if __name__ == "__main__":
89+
# Check if a command-line argument for the config file path is provided
90+
if len(sys.argv) > 1:
91+
config_file = sys.argv[1]
92+
else:
93+
# Use the defined default file path if no argument is provided
94+
config_file = DEFAULT_PRE_COMMIT_CONFIG_FILE
95+
print(f"No file path provided. Attempting to validate default file: '{config_file}'")
96+
97+
if validate_pre_commit_config(config_file):
98+
print(f"\nValidation successful! All hooks in '{config_file}' have 'name' and 'description' keys.")
99+
else:
100+
print(f"\nValidation failed. Please check the errors listed above for '{config_file}'.")
101+
sys.exit(1) # Exit with a non-zero code to indicate failure

.pre-commit-config.yaml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,22 @@ repos:
1919
description: check hooks apply to the repository
2020
- repo: local
2121
hooks:
22+
- id: pre-commit-hook-validator
23+
name: run pre-commit hook validator
24+
description: Validates a pre-commit-config.yaml file to ensure all hooks have 'name' and 'description' keys
25+
entry: .github/workflows/scripts/pre_commit_hooks_validator.py .pre-commit-config.yaml
26+
language: python
27+
additional_dependencies:
28+
- pyyaml
29+
require_serial: true
30+
- id: create-pre-commit-docs
31+
name: create pre-commit docs
32+
description: creates a Markdown file with information on the pre-commit hooks
33+
entry: .github/workflows/scripts/create_pre_commit_docs.py .pre-commit-config.yaml
34+
language: python
35+
additional_dependencies:
36+
- pyyaml
37+
require_serial: true
2238
- id: prettier
2339
name: run prettier
2440
description: format files with prettier
@@ -43,29 +59,67 @@ repos:
4359
rev: v5.0.0
4460
hooks:
4561
- id: check-added-large-files
62+
name: check added large files
63+
description: Prevent giant files from being committed
4664
- id: check-case-conflict
65+
name: check case conflict
66+
description: Check for files with names that would conflict on a case-insensitive filesystem like MacOS HFS+ or Windows FAT
4767
- id: check-executables-have-shebangs
68+
name: check executables have shebangs
69+
description: Checks that non-binary executables have a proper shebang
4870
exclude: ^test/t/lang\.rb$
4971
- id: check-illegal-windows-names
72+
name: check illegal windows names
73+
description: Check for files that cannot be created on Windows
5074
- id: pretty-format-json
75+
name: pretty format json
76+
description: Checks that all your JSON files are pretty
5177
args: [--autofix, --no-sort-keys]
5278
- id: check-json
79+
name: check json
80+
description: Attempts to load all json files to verify syntax
5381
- id: check-merge-conflict
82+
name: check merge conflict
83+
description: Check for files that contain merge conflict strings
5484
- id: check-shebang-scripts-are-executable
85+
name: check shebang scripts are executable
86+
description: Checks that scripts with shebangs are executable
5587
- id: check-vcs-permalinks
88+
name: check vcs permalinks
89+
description: Ensures that links to vcs websites are permalinks
5690
- id: check-yaml
91+
name: check yaml
92+
description: Attempts to load all yaml files to verify syntax
5793
- id: destroyed-symlinks
94+
name: destroyed symlinks
95+
description: Detects symlinks which are changed to regular files with a content of a path which that symlink was pointing to
5896
- id: detect-aws-credentials
97+
name: detect aws credentials
98+
description: Checks for the existence of AWS secrets that you have set up with the AWS CLI
5999
args: [--allow-missing-credentials]
60100
- id: detect-private-key
101+
name: detect private key
102+
description: Checks for the existence of private keys
61103
- id: end-of-file-fixer
104+
name: end of file fixer
105+
description: Makes sure files end in a newline and only a newline
62106
- id: file-contents-sorter
107+
name: file contents sorter
108+
description: Sort the lines in specified files (defaults to alphabetical)
63109
args: [--unique]
64110
files: ^\.github/linters/codespell\.txt$
65111
- id: fix-byte-order-marker
112+
name: fix byte order marker
113+
description: Removes UTF-8 byte order marker
66114
- id: forbid-submodules
115+
name: forbid submodules
116+
description: Prevent addition of new git submodules
67117
- id: mixed-line-ending
118+
name: mixed line ending
119+
description: Replaces or checks mixed line ending
68120
- id: trailing-whitespace
121+
name: trailing whitespace
122+
description: Trims trailing whitespace
69123
- repo: https://github.com/Lucas-C/pre-commit-hooks
70124
rev: v1.5.5
71125
hooks:

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Ignore artifacts:
22
build
33
coverage
4+
doc/guides/pre-commit-hooks.md
45
doc/internal/opcode.md
56
tools/lrama

doc/guides/pre-commit-hooks.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# pre-commit hook documentation
2+
3+
| Hook ID | Language | Name | Description | Version |
4+
|---|---|---|---|---|
5+
| identity | N/A | run identity | check your identity | N/A |
6+
| check-hooks-apply | N/A | run check-hooks-apply | check hooks apply to the repository | N/A |
7+
| pre-commit-hook-validator | python | run pre-commit hook validator | Validates a pre-commit-config.yaml file to ensure all hooks have 'name' and 'description' keys | N/A |
8+
| create-pre-commit-docs | python | create pre-commit docs | creates a Markdown file with information on the pre-commit hooks | N/A |
9+
| prettier | node | run prettier | format files with prettier | N/A |
10+
| [gitleaks](https://github.com/gitleaks/gitleaks) | N/A | run gitleaks | detect hardcoded secrets with gitleaks | v8.27.2 |
11+
| [oxipng](https://github.com/shssoichiro/oxipng) | N/A | run oxipng | use lossless compression to optimize PNG files | v9.1.5 |
12+
| [check-added-large-files](https://github.com/pre-commit/pre-commit-hooks) | N/A | check added large files | Prevent giant files from being committed | v5.0.0 |
13+
| [check-case-conflict](https://github.com/pre-commit/pre-commit-hooks) | N/A | check case conflict | Check for files with names that would conflict on a case-insensitive filesystem like MacOS HFS+ or Windows FAT | v5.0.0 |
14+
| [check-executables-have-shebangs](https://github.com/pre-commit/pre-commit-hooks) | N/A | check executables have shebangs | Checks that non-binary executables have a proper shebang | v5.0.0 |
15+
| [check-illegal-windows-names](https://github.com/pre-commit/pre-commit-hooks) | N/A | check illegal windows names | Check for files that cannot be created on Windows | v5.0.0 |
16+
| [pretty-format-json](https://github.com/pre-commit/pre-commit-hooks) | N/A | pretty format json | Checks that all your JSON files are pretty | v5.0.0 |
17+
| [check-json](https://github.com/pre-commit/pre-commit-hooks) | N/A | check json | Attempts to load all json files to verify syntax | v5.0.0 |
18+
| [check-merge-conflict](https://github.com/pre-commit/pre-commit-hooks) | N/A | check merge conflict | Check for files that contain merge conflict strings | v5.0.0 |
19+
| [check-shebang-scripts-are-executable](https://github.com/pre-commit/pre-commit-hooks) | N/A | check shebang scripts are executable | Checks that scripts with shebangs are executable | v5.0.0 |
20+
| [check-vcs-permalinks](https://github.com/pre-commit/pre-commit-hooks) | N/A | check vcs permalinks | Ensures that links to vcs websites are permalinks | v5.0.0 |
21+
| [check-yaml](https://github.com/pre-commit/pre-commit-hooks) | N/A | check yaml | Attempts to load all yaml files to verify syntax | v5.0.0 |
22+
| [destroyed-symlinks](https://github.com/pre-commit/pre-commit-hooks) | N/A | destroyed symlinks | Detects symlinks which are changed to regular files with a content of a path which that symlink was pointing to | v5.0.0 |
23+
| [detect-aws-credentials](https://github.com/pre-commit/pre-commit-hooks) | N/A | detect aws credentials | Checks for the existence of AWS secrets that you have set up with the AWS CLI | v5.0.0 |
24+
| [detect-private-key](https://github.com/pre-commit/pre-commit-hooks) | N/A | detect private key | Checks for the existence of private keys | v5.0.0 |
25+
| [end-of-file-fixer](https://github.com/pre-commit/pre-commit-hooks) | N/A | end of file fixer | Makes sure files end in a newline and only a newline | v5.0.0 |
26+
| [file-contents-sorter](https://github.com/pre-commit/pre-commit-hooks) | N/A | file contents sorter | Sort the lines in specified files (defaults to alphabetical) | v5.0.0 |
27+
| [fix-byte-order-marker](https://github.com/pre-commit/pre-commit-hooks) | N/A | fix byte order marker | Removes UTF-8 byte order marker | v5.0.0 |
28+
| [forbid-submodules](https://github.com/pre-commit/pre-commit-hooks) | N/A | forbid submodules | Prevent addition of new git submodules | v5.0.0 |
29+
| [mixed-line-ending](https://github.com/pre-commit/pre-commit-hooks) | N/A | mixed line ending | Replaces or checks mixed line ending | v5.0.0 |
30+
| [trailing-whitespace](https://github.com/pre-commit/pre-commit-hooks) | N/A | trailing whitespace | Trims trailing whitespace | v5.0.0 |
31+
| [forbid-tabs](https://github.com/Lucas-C/pre-commit-hooks) | N/A | run no-tabs checker | check the codebase for tabs | v1.5.5 |
32+
| [remove-tabs](https://github.com/Lucas-C/pre-commit-hooks) | N/A | run tabs remover | find and convert tabs to spaces | v1.5.5 |
33+
| [actionlint](https://github.com/rhysd/actionlint) | N/A | run actionlint | lint GitHub Actions workflow files | v1.7.7 |
34+
| [codespell](https://github.com/codespell-project/codespell) | N/A | run codespell | check spelling with codespell | v2.4.1 |
35+
| [markdownlint](https://github.com/igorshubovych/markdownlint-cli) | N/A | run markdownlint | checks the style of Markdown files | v0.45.0 |
36+
| [markdown-link-check](https://github.com/tcort/markdown-link-check) | N/A | run markdown-link-check | checks hyperlinks in Markdown files | v3.13.7 |
37+
| [rubocop](https://github.com/rubocop/rubocop) | N/A | run rubocop | RuboCop is a Ruby code style checker (linter) and formatter based on the community-driven Ruby Style Guide | v1.78.0 |
38+
| [shellcheck](https://github.com/shellcheck-py/shellcheck-py) | N/A | run shellcheck | check shell scripts with a static analysis tool | v0.10.0.1 |
39+
| [yamllint](https://github.com/adrienverge/yamllint) | N/A | run yamllint | check YAML files with yamllint | v1.37.1 |

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pre-commit
2+
pyyaml

0 commit comments

Comments
 (0)