""" 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()