|
16 | 16 |
|
17 | 17 | import re |
18 | 18 | import sys |
| 19 | +from collections.abc import Iterator |
19 | 20 | from pathlib import Path |
| 21 | +from typing import TypedDict |
20 | 22 |
|
21 | | -REPO_ROOT = Path(__file__).resolve().parent.parent |
22 | | -WRAPPER_GO_MOD = REPO_ROOT / "src" / "tailscale" / "wrapper" / "go.mod" |
23 | | -TOOLS_GO_MOD = REPO_ROOT / "tools" / "go.mod" |
24 | | -PRE_COMMIT_CONFIG = REPO_ROOT / ".pre-commit-config.yaml" |
| 23 | +from ruamel.yaml import YAML |
| 24 | + |
| 25 | +PATH_REPO_ROOT = Path(__file__).resolve().parent.parent |
| 26 | +PATH_WRAPPER_GO_MOD = PATH_REPO_ROOT / "src" / "tailscale" / "wrapper" / "go.mod" |
| 27 | +PATH_TOOLS_GO_MOD = PATH_REPO_ROOT / "tools" / "go.mod" |
| 28 | +PATH_PRE_COMMIT_CONFIG = PATH_REPO_ROOT / ".pre-commit-config.yaml" |
25 | 29 | REQUIRE_RE = re.compile(r"^\s+([\w./@\-]+)\s+(v\S+)", re.MULTILINE) |
26 | 30 |
|
27 | 31 |
|
| 32 | +class Hook(TypedDict, total=False): |
| 33 | + id: str |
| 34 | + name: str |
| 35 | + description: str |
| 36 | + language: str |
| 37 | + language_version: str |
| 38 | + entry: str |
| 39 | + args: list[str] |
| 40 | + types: list[str] |
| 41 | + files: str |
| 42 | + pass_filenames: bool |
| 43 | + additional_dependencies: list[str] |
| 44 | + |
| 45 | + |
| 46 | +class Repo(TypedDict, total=False): |
| 47 | + repo: str |
| 48 | + rev: str |
| 49 | + hooks: list[Hook] |
| 50 | + |
| 51 | + |
| 52 | +class CiConfig(TypedDict, total=False): |
| 53 | + skip: list[str] |
| 54 | + |
| 55 | + |
| 56 | +class PreCommitConfig(TypedDict, total=False): |
| 57 | + ci: CiConfig |
| 58 | + repos: list[Repo] |
| 59 | + |
| 60 | + |
28 | 61 | def parse_go_directive(path: Path) -> str: |
29 | 62 | for line in path.read_text().splitlines(): |
30 | 63 | line = line.strip() |
@@ -53,62 +86,77 @@ def sync_go_directive(text: str, go_version: str) -> tuple[str, bool]: |
53 | 86 | return new, new != text |
54 | 87 |
|
55 | 88 |
|
56 | | -def sync_language_version(text: str, go_version: str) -> tuple[str, bool]: |
57 | | - new = re.sub( |
58 | | - r"(language_version:\s*)[\d.]+", |
59 | | - lambda m: m.group(1) + go_version, |
60 | | - text, |
61 | | - ) |
62 | | - return new, new != text |
| 89 | +def golang_hooks(pre_commit_config: PreCommitConfig) -> Iterator[Hook]: |
| 90 | + """Yield every hook dict whose language is 'golang'.""" |
| 91 | + for repo in pre_commit_config.get("repos", []): |
| 92 | + for hook in repo.get("hooks", []): |
| 93 | + if hook.get("language") == "golang": |
| 94 | + yield hook |
63 | 95 |
|
64 | 96 |
|
65 | | -def sync_additional_deps(text: str, modules: dict[str, str]) -> tuple[str, bool]: |
| 97 | +def sync_language_version(pre_commit_config: PreCommitConfig, go_version: str) -> bool: |
| 98 | + """Update language_version on all golang hooks. Returns True if anything changed.""" |
66 | 99 | changed = False |
| 100 | + for hook in golang_hooks(pre_commit_config): |
| 101 | + if hook.get("language_version") != go_version: |
| 102 | + hook["language_version"] = go_version |
| 103 | + changed = True |
| 104 | + return changed |
67 | 105 |
|
68 | | - def replace(match: re.Match[str]) -> str: |
69 | | - nonlocal changed |
70 | | - dep_path, old_ver = match.group(1), match.group(2) |
71 | | - new_ver = find_module_version(dep_path, modules) |
72 | | - if new_ver is None or new_ver == old_ver: |
73 | | - return match.group(0) |
74 | | - changed = True |
75 | | - return f'"{dep_path}@{new_ver}"' |
76 | 106 |
|
77 | | - new = re.sub(r'"([\w./@\-]+)@(v[^\s"]+)"', replace, text) |
78 | | - return new, changed |
| 107 | +def sync_additional_deps( |
| 108 | + pre_commit_config: PreCommitConfig, modules: dict[str, str] |
| 109 | +) -> bool: |
| 110 | + """Update additional_dependencies versions on all golang hooks. Returns True if anything changed.""" |
| 111 | + changed = False |
| 112 | + for hook in golang_hooks(pre_commit_config): |
| 113 | + additional_deps: list[str] = hook.get("additional_dependencies", []) |
| 114 | + for i, dep in enumerate(additional_deps): |
| 115 | + dep_path, old_ver = dep.split("@", 1) |
| 116 | + new_ver = find_module_version(dep_path, modules) |
| 117 | + if new_ver is None or new_ver == old_ver: |
| 118 | + continue |
| 119 | + additional_deps[i] = f"{dep_path}@{new_ver}" |
| 120 | + changed = True |
| 121 | + return changed |
79 | 122 |
|
80 | 123 |
|
81 | 124 | def main() -> int: |
82 | | - go_version = parse_go_directive(WRAPPER_GO_MOD) |
83 | | - tool_modules = parse_requires(TOOLS_GO_MOD) |
| 125 | + go_version = parse_go_directive(PATH_WRAPPER_GO_MOD) |
| 126 | + tool_modules = parse_requires(PATH_TOOLS_GO_MOD) |
84 | 127 |
|
85 | 128 | any_changed = False |
86 | 129 |
|
87 | 130 | # Sync go directive in tools/go.mod |
88 | | - tools_text = TOOLS_GO_MOD.read_text() |
| 131 | + tools_text = PATH_TOOLS_GO_MOD.read_text() |
89 | 132 | new_tools_text, changed = sync_go_directive(tools_text, go_version) |
90 | 133 | if changed: |
91 | | - _ = TOOLS_GO_MOD.write_text(new_tools_text) |
| 134 | + _ = PATH_TOOLS_GO_MOD.write_text(new_tools_text) |
92 | 135 | print(f"tools/go.mod: updated go directive to {go_version}") |
93 | 136 | any_changed = True |
94 | 137 |
|
95 | 138 | # Sync .pre-commit-config.yaml |
96 | | - pc_text = PRE_COMMIT_CONFIG.read_text() |
97 | | - pc_text, lv_changed = sync_language_version(pc_text, go_version) |
98 | | - pc_text, dep_changed = sync_additional_deps(pc_text, tool_modules) |
| 139 | + yaml = YAML() # round-trip: preserves comments and formatting |
| 140 | + yaml.preserve_quotes = True |
| 141 | + # ruamel.yaml's load() is untyped; cast at the boundary and suppress the warnings. |
| 142 | + pre_commit_config: PreCommitConfig = yaml.load(PATH_PRE_COMMIT_CONFIG) # pyright: ignore[reportUnknownMemberType, reportAny] |
| 143 | + |
| 144 | + language_version_changed = sync_language_version(pre_commit_config, go_version) |
| 145 | + dependencies_changed = sync_additional_deps(pre_commit_config, tool_modules) |
99 | 146 |
|
100 | | - if lv_changed: |
| 147 | + if language_version_changed: |
101 | 148 | print(f".pre-commit-config.yaml: updated language_version to {go_version}") |
102 | 149 | any_changed = True |
103 | | - if dep_changed: |
| 150 | + if dependencies_changed: |
104 | 151 | print(".pre-commit-config.yaml: updated additional_dependencies versions") |
105 | 152 | any_changed = True |
106 | 153 |
|
107 | | - if lv_changed or dep_changed: |
108 | | - _ = PRE_COMMIT_CONFIG.write_text(pc_text) |
| 154 | + if language_version_changed or dependencies_changed: |
| 155 | + with PATH_PRE_COMMIT_CONFIG.open("w") as f: |
| 156 | + yaml.dump(pre_commit_config, f) # pyright: ignore[reportUnknownMemberType] |
109 | 157 |
|
110 | 158 | if any_changed: |
111 | | - print("Files were modified — re-stage and re-commit.") |
| 159 | + print("Files were modified - re-stage and re-commit.") |
112 | 160 | return 1 |
113 | 161 | return 0 |
114 | 162 |
|
|
0 commit comments