Files
amethyst/.claude/hooks/lib/git_push_gate.py
T
davotoula 42c96bf8fc feature: document the orphaned-translation trap and gate it pre-push
The [ExtraTranslation] failure that took main red in 1ce583ec92 was not
documented anywhere. amethyst/src/main/res/CLAUDE.md and the
find-missing-translations skill both mention the lint rule, but only inside
one narrow case (converting a <string> to <plurals>). Neither stated the
general rule: removing or renaming a key in the default values/strings.xml
orphans every locale entry that still declares it.

Nor would running lint have caught it in practice. The only pre-push gate is
pre-push-spotless.sh, which runs spotlessApply and nothing else, and
:amethyst:lintFdroidBenchmark takes ~19 minutes on a warm daemon, so it is
not a per-commit check.

Add both halves:

- amethyst/src/main/res/CLAUDE.md gains a "Renaming or removing a string key"
  section stating the same-commit rule and why Crowdin is not a cleanup step
  CI waits for. The trap is that Crowdin is *partly* reliable — it cleaned 32
  of 47 locales — so the tree looks correct in whichever files you open.
  Retitled the file (it is no longer plural-only) and marked the existing
  sections as the plural-specific ones they always were.
- orphan_strings_check.py scans every locale's resource names against its
  tree's default values/, across both Crowdin-managed resource systems: the
  Android res trees and the commons Compose-Multiplatform catalog. 0.17s
  against the full repo. pre-push-orphan-strings.sh wraps it as a PreToolUse
  gate on git push / create_pull_request, reusing the shell-tokenizing
  push-detection from pre-push-spotless.sh so "push" inside a commit message
  is not mistaken for the subcommand.
- find-missing-translations gains a Common Mistakes entry pointing at both.

Verified: clean on the current tree; exit 2 listing the orphans when
route_video is reintroduced in two locales and a retired key is seeded in the
Compose catalog; gate fires on git push and create_pull_request, stays quiet
on a commit whose message contains "push" and on non-Bash tools.
2026-09-01 11:12:43 +02:00

62 lines
1.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""Decide whether a PreToolUse payload on stdin is a push/PR boundary.
Shared by every pre-push hook in this directory (pre-push-spotless.sh,
pre-push-orphan-strings.sh) so the gate condition is defined once. Each hook is
a separate process with its own stdin, so this is exec'd per hook rather than
run once and shared.
Exit 0 = this call publishes code (gate it). Exit 1 = let it through.
"""
import json
import shlex
import sys
# Reaching the push subcommand means stepping over git's global options first.
GLOBAL_WITH_ARG = {"-c", "-C", "--namespace", "--git-dir", "--work-tree", "--exec-path"}
def is_boundary(data):
tool = data.get("tool_name", "")
if tool.endswith("create_pull_request"):
return True
if tool != "Bash":
return False
cmd = (data.get("tool_input") or {}).get("command", "")
# Tokenize like a shell so `push` inside a quoted commit message or heredoc
# stays one token and is NOT mistaken for the push subcommand.
try:
tokens = shlex.split(cmd, comments=True)
except ValueError:
tokens = cmd.split()
for i, token in enumerate(tokens):
if token != "git" and not token.endswith("/git"):
continue
j = i + 1
while j < len(tokens):
tok = tokens[j]
if tok in GLOBAL_WITH_ARG:
j += 2
elif tok.startswith("-"):
j += 1
else:
break
if j < len(tokens) and tokens[j] == "push":
return True
return False
def main():
try:
data = json.load(sys.stdin)
except Exception:
return 1
return 0 if is_boundary(data) else 1
if __name__ == "__main__":
sys.exit(main())