149 lines
4.3 KiB
Python
Executable File
149 lines
4.3 KiB
Python
Executable File
"""
|
|
Detect files where all changes are whitespace-only.
|
|
|
|
This helps avoid unnecessary commit history noise from whitespace-only changes.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
import time
|
|
import urllib.error
|
|
import urllib.request
|
|
from typing import cast
|
|
|
|
BYPASS_LABEL = "allow-whitespace-only"
|
|
|
|
_MAX_ATTEMPTS = 3
|
|
|
|
|
|
def _is_retryable(exc: Exception) -> bool:
|
|
match exc:
|
|
case urllib.error.HTTPError(code=code):
|
|
return code >= 500
|
|
case urllib.error.URLError():
|
|
return True
|
|
case _:
|
|
return False
|
|
|
|
|
|
def _retry_urlopen(request: urllib.request.Request, timeout: int = 30) -> str:
|
|
for i in range(_MAX_ATTEMPTS):
|
|
try:
|
|
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
return cast(str, response.read().decode("utf-8"))
|
|
except Exception as exc:
|
|
if not _is_retryable(exc) or i == _MAX_ATTEMPTS - 1:
|
|
raise
|
|
time.sleep(2**i)
|
|
raise RuntimeError("unreachable")
|
|
|
|
|
|
def github_api_request(url: str, accept: str) -> str:
|
|
headers = {
|
|
"Accept": accept,
|
|
"X-GitHub-Api-Version": "2022-11-28",
|
|
}
|
|
|
|
if github_token := os.environ.get("GH_TOKEN"):
|
|
headers["Authorization"] = f"Bearer {github_token}"
|
|
|
|
request = urllib.request.Request(url, headers=headers)
|
|
return _retry_urlopen(request)
|
|
|
|
|
|
def get_pr_diff(owner: str, repo: str, pull_number: int) -> str:
|
|
url = f"https://github.com/{owner}/{repo}/pull/{pull_number}.diff"
|
|
request = urllib.request.Request(url)
|
|
return _retry_urlopen(request)
|
|
|
|
|
|
def get_pr_labels(owner: str, repo: str, pull_number: int) -> list[str]:
|
|
url = f"https://api.github.com/repos/{owner}/{repo}/pulls/{pull_number}"
|
|
data = json.loads(github_api_request(url, "application/vnd.github.v3+json"))
|
|
return [label_obj["name"] for label_obj in data.get("labels", [])]
|
|
|
|
|
|
def parse_diff(diff_text: str | None) -> list[str]:
|
|
if not diff_text:
|
|
return []
|
|
|
|
files: list[str] = []
|
|
current_file: str | None = None
|
|
changes: list[str] = []
|
|
|
|
for line in diff_text.split("\n"):
|
|
if line.startswith("diff --git"):
|
|
if current_file and changes and all(c.strip() == "" for c in changes):
|
|
files.append(current_file)
|
|
|
|
current_file = None
|
|
changes = []
|
|
|
|
elif line.startswith("--- a/"):
|
|
current_file = None if line == "--- /dev/null" else line[6:]
|
|
|
|
elif line.startswith("+++ b/"):
|
|
current_file = None if line == "+++ /dev/null" else line[6:]
|
|
|
|
elif line.startswith("+") or line.startswith("-"):
|
|
content = line[1:]
|
|
changes.append(content)
|
|
|
|
if current_file and changes and all(c.strip() == "" for c in changes):
|
|
files.append(current_file)
|
|
|
|
return files
|
|
|
|
|
|
def parse_args() -> tuple[str, str, int]:
|
|
parser = argparse.ArgumentParser(
|
|
description="Check for unnecessary whitespace-only changes in the diff"
|
|
)
|
|
parser.add_argument(
|
|
"--repo",
|
|
required=True,
|
|
help='Repository in the format "owner/repo" (e.g., "mlflow/mlflow")',
|
|
)
|
|
parser.add_argument(
|
|
"--pr",
|
|
type=int,
|
|
required=True,
|
|
help="Pull request number",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
owner, repo = args.repo.split("/")
|
|
return owner, repo, args.pr
|
|
|
|
|
|
def main() -> None:
|
|
owner, repo, pull_number = parse_args()
|
|
diff_text = get_pr_diff(owner, repo, pull_number)
|
|
if files := parse_diff(diff_text):
|
|
pr_labels = get_pr_labels(owner, repo, pull_number)
|
|
has_bypass_label = BYPASS_LABEL in pr_labels
|
|
|
|
level = "warning" if has_bypass_label else "error"
|
|
message = (
|
|
f"This file only has whitespace changes (bypassed with '{BYPASS_LABEL}' label)."
|
|
if has_bypass_label
|
|
else (
|
|
f"This file only has whitespace changes. "
|
|
f"Please revert them or apply the '{BYPASS_LABEL}' label to bypass this check "
|
|
f"if they are necessary."
|
|
)
|
|
)
|
|
|
|
for file_path in files:
|
|
# https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions
|
|
print(f"::{level} file={file_path},line=1,col=1::{message}")
|
|
|
|
if not has_bypass_label:
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|