fix: вердикт broken-repo-paths не зависит от личных настроек git; пробелы в TODO() #30

Merged
claude-secretary merged 1 commit from fix/deterministic-verdict-todo-spaces into main 2026-08-27 14:33:50 +07:00

Закрывает #27 — два независимых пункта, оба пришли из тела #11.

1. Гейт, который у двух людей отвечает по-разному

no-broken-repo-paths спрашивает git check-ignore, а тот учитывает глобальный core.excludesFile. Значит путь, добавленный в личный игнор-файл, переводил хук с FAIL на pass — не оставляя следа ни в диффе, ни в конфиге репозитория. У разработчика зелено, в CI красно, и разница не видна нигде.

Замерил на временном репозитории: с подсунутым глобальным excludesFile битый путь переставал быть нарушением. Теперь git вызывается с GIT_CONFIG_GLOBAL и GIT_CONFIG_SYSTEM в /dev/null — и в check-ignore, и в rev-parse (глобальный core.worktree сдвинул бы корень резолва, а от него зависит, какой путь считается существующим).

.git/info/exclude так не отключается — git читает его всегда, опции для этого нет. Записал в README как оставшееся ограничение, с честной оговоркой: файл локален для клона и, в отличие от глобального конфига, не приезжает вместе с настройками пользователя на другую машину.

2. TODO (#456) принимался, TODO( #456 ) — нет

Асимметрия, которая нигде не описана: пробел перед скобкой и вокруг запятой regex допускал, а сразу после открывающей скобки и перед закрывающей — нет. Разработчик, поставивший пробел внутри скобок, получал FAIL на формате, который выглядит легальным.

Выбрал «разрешить везде»: правило про формат ссылки, а не про пунктуацию вокруг неё. Паттерн синхронизирован в обоих местах — pygrep-хук todo-needs-issue и исключение TODO_RE в no-issue-refs-in-comments. Два хука обязаны говорить об одном формате одно и то же, иначе TODO( #456 ) проходит у одного и падает у другого.

Что осталось запрещённым — без изменений: TODO без ссылки и префиксная форма TODO(ns/proj#N).

Проверка

  • broken_paths_ignores_global_excludes поднимает git с подсунутым excludesFile, который «прощает» битый путь: вердикт обязан не измениться. Мутация «убрать отключение конфига» роняет кейс.
  • todo_needs_issue_inner_spaces_clean и issue_refs_todo_spaces_clean — обе формы у обоих хуков. Мутация «вернуть запрет пробелов внутри скобок» роняет первый.

Версия

0.21.00.22.0. На тот же номер претендует #29 — тот, что уйдёт вторым, станет 0.23.0. Забавно, что именно #29 и заводит CI-гейт, который такие коллизии впредь ловит автоматически.

Закрывает [#27](https://git.homedevlab.ru/senokosov/pre-commit-hooks/issues/27) — два независимых пункта, оба пришли из тела [#11](https://git.homedevlab.ru/senokosov/pre-commit-hooks/issues/11). ## 1. Гейт, который у двух людей отвечает по-разному `no-broken-repo-paths` спрашивает `git check-ignore`, а тот учитывает глобальный `core.excludesFile`. Значит путь, добавленный в личный игнор-файл, переводил хук с FAIL на pass — **не оставляя следа ни в диффе, ни в конфиге репозитория**. У разработчика зелено, в CI красно, и разница не видна нигде. Замерил на временном репозитории: с подсунутым глобальным `excludesFile` битый путь переставал быть нарушением. Теперь git вызывается с `GIT_CONFIG_GLOBAL` и `GIT_CONFIG_SYSTEM` в `/dev/null` — и в `check-ignore`, и в `rev-parse` (глобальный `core.worktree` сдвинул бы корень резолва, а от него зависит, какой путь считается существующим). `.git/info/exclude` так **не** отключается — git читает его всегда, опции для этого нет. Записал в README как оставшееся ограничение, с честной оговоркой: файл локален для клона и, в отличие от глобального конфига, не приезжает вместе с настройками пользователя на другую машину. ## 2. `TODO (#456)` принимался, `TODO( #456 )` — нет Асимметрия, которая нигде не описана: пробел перед скобкой и вокруг запятой regex допускал, а сразу после открывающей скобки и перед закрывающей — нет. Разработчик, поставивший пробел внутри скобок, получал FAIL на формате, который выглядит легальным. Выбрал «разрешить везде»: правило про формат ссылки, а не про пунктуацию вокруг неё. Паттерн синхронизирован в обоих местах — pygrep-хук `todo-needs-issue` и исключение `TODO_RE` в `no-issue-refs-in-comments`. Два хука обязаны говорить об одном формате одно и то же, иначе `TODO( #456 )` проходит у одного и падает у другого. Что осталось запрещённым — без изменений: `TODO` без ссылки и префиксная форма `TODO(ns/proj#N)`. ## Проверка - `broken_paths_ignores_global_excludes` поднимает git с подсунутым `excludesFile`, который «прощает» битый путь: вердикт обязан не измениться. Мутация «убрать отключение конфига» роняет кейс. - `todo_needs_issue_inner_spaces_clean` и `issue_refs_todo_spaces_clean` — обе формы у обоих хуков. Мутация «вернуть запрет пробелов внутри скобок» роняет первый. ## Версия `0.21.0` → **`0.22.0`**. На тот же номер претендует [#29](https://git.homedevlab.ru/senokosov/pre-commit-hooks/pulls/29) — тот, что уйдёт вторым, станет `0.23.0`. Забавно, что именно #29 и заводит CI-гейт, который такие коллизии впредь ловит автоматически.
fix: вердикт broken-repo-paths не зависит от личных настроек git; пробелы в TODO()
All checks were successful
ci / smoke (push) Successful in 18s
ci / smoke (pull_request) Successful in 18s
543f1114d9
Закрывает #27

1. `git check-ignore` учитывает глобальный `core.excludesFile`, поэтому
   путь, добавленный в личный игнор-файл, переводил хук с FAIL на pass —
   не оставляя следа ни в диффе, ни в конфиге репозитория. У двух людей
   один и тот же коммит получал разные вердикты. Git теперь вызывается с
   GIT_CONFIG_GLOBAL и GIT_CONFIG_SYSTEM в /dev/null — и в check-ignore,
   и в rev-parse (глобальный core.worktree сдвинул бы корень резолва).

   `.git/info/exclude` так не отключается — git читает его всегда. Это
   записано в README как оставшееся ограничение: файл локален для клона
   и, в отличие от глобального конфига, не приезжает с настройками
   пользователя.

2. `TODO (#456)` принимался, а `TODO( #456 )` — нет, и нигде это описано
   не было. Пробелы теперь допускаются везде одинаково: правило про
   формат ссылки, а не про пунктуацию вокруг неё. Паттерн синхронизирован
   в обоих местах — pygrep-хук и исключение в no-issue-refs-in-comments:
   два хука обязаны говорить об одном формате одно и то же.

Мутации, каждая роняет smoke: убрать отключение глобального конфига
(кейс поднимает git с подсунутым excludesFile, который «прощает» битый
путь, — вердикт обязан не измениться), вернуть запрет пробелов внутри
скобок.

Версия 0.21.0 → 0.22.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
claude-secretary force-pushed fix/deterministic-verdict-todo-spaces from 543f1114d9
All checks were successful
ci / smoke (push) Successful in 18s
ci / smoke (pull_request) Successful in 18s
to 9b7e3f39dd
All checks were successful
ci / smoke (push) Successful in 19s
ci / smoke (pull_request) Successful in 19s
2026-08-27 14:22:53 +07:00
Compare
claude-secretary force-pushed fix/deterministic-verdict-todo-spaces from 9b7e3f39dd
All checks were successful
ci / smoke (push) Successful in 19s
ci / smoke (pull_request) Successful in 19s
to 3c3292ba03
All checks were successful
ci / smoke (push) Successful in 19s
ci / smoke (pull_request) Successful in 19s
2026-08-27 14:33:13 +07:00
Compare
Author
Owner

Ревью (субагент, свежий контекст): блокер подтверждён и исправлен — вместе с ним ушли ещё две неточности, которые я в первой редакции написал уверенно и неверно.

B1 — снос конфига забирал safe.directory. Проверил: он читается только из protected-конфига, так что GIT_CONFIG_GLOBAL=/dev/null + GIT_CONFIG_SYSTEM=/dev/null выкидывает и то, что кладёт туда actions/checkout. В контейнере с UID-mismatch каждый вызов git отвечал бы 128, а хук трактует ненулевой код как «путь не игнорируется» — то есть я обменял бы одну недетерминированность на волну ложных FAIL'ов, причём молча.

Заменено на git -c core.excludesFile=/dev/null. Проверил, что это строго лучше: перебивает все уровни, включая локальный .git/config (который снос конфига не трогал вовсе — нит 1 закрыт заодно), и safe.directory остаётся на месте.

Две мои формулировки оказались неверными, обе сняты:

  • «-c у rev-parse защищает от глобального core.worktree» — проверил, core.worktree из глобального конфига на rev-parse --show-toplevel не влияет. Флаги оставлены на всех вызовах ради единообразия, но обоснование теперь честное: «сегодня ручка влияет только на check-ignore, флаги едины, чтобы следующая команда не осталась без них».
  • «отключение конфига заодно гасит diff.external» — да, гасило, но моя замена на -c diff.external= ломает git: он пытается выполнить пустую команду и падает. Правильный инструмент — --no-ext-diff у самой команды diff, добавлен.

Ниты:

  • 2 — обе половины покрыты: кейсы на глобальный и локальный core.excludesFile. Мутации «убрать -c у check-ignore» и «убрать core.excludesFile из списка» роняют по два кейса.
  • 3run_case_out с пином пути в выводе (трейсбек давал тот же exit=1) и && вместо ; между setup и запуском.
  • 4 — описание no-issue-refs-in-comments в манифесте теперь говорит про пробелы: весь смысл правки в том, что два хука обязаны говорить об одном формате одно и то же.
  • 5 — граница названа и закреплена: TODO( # 456 ) остаётся нарушением, # 456 — не ссылка. Мутация «разрешить пробел внутри ссылки» роняет новый кейс.
  • 6 — в примере конфига был обещан формат TODO(arg, #42, ...), который regex отвергает с v0.11.0; это ровно тот дефект, на который жалуется issue, — переписано.
  • 7 — неактуален после перехода на -c: флаг поддерживается всеми живыми версиями git, no-op'а из-за старого git больше нет.

Про кейс на --no-ext-diff отдельно. Первые две редакции были вакуумными: при сломанном diff нарушения не появляются, а исчезают, поэтому кейс с ожиданием 0 проходил в обе стороны. Правильный кейс ожидает 1 на branch-долге: регрессия здесь — пропажа вердикта, а не его появление. Мутация ловится только в такой постановке.

V1 — ветка отребейзена и force-запушена, версия 0.23.0, гейт из #29 на ней зелёный. Тег v0.23.0 поставлю сразу после мержа.

Ревью (субагент, свежий контекст): блокер подтверждён и исправлен — вместе с ним ушли ещё две неточности, которые я в первой редакции написал уверенно и неверно. **B1 — снос конфига забирал `safe.directory`.** Проверил: он читается только из protected-конфига, так что `GIT_CONFIG_GLOBAL=/dev/null` + `GIT_CONFIG_SYSTEM=/dev/null` выкидывает и то, что кладёт туда `actions/checkout`. В контейнере с UID-mismatch каждый вызов git отвечал бы 128, а хук трактует ненулевой код как «путь не игнорируется» — то есть я обменял бы одну недетерминированность на волну ложных FAIL'ов, причём молча. Заменено на `git -c core.excludesFile=/dev/null`. Проверил, что это строго лучше: перебивает **все** уровни, включая локальный `.git/config` (который снос конфига не трогал вовсе — нит 1 закрыт заодно), и `safe.directory` остаётся на месте. **Две мои формулировки оказались неверными, обе сняты:** - «`-c` у `rev-parse` защищает от глобального `core.worktree`» — проверил, `core.worktree` из глобального конфига на `rev-parse --show-toplevel` не влияет. Флаги оставлены на всех вызовах ради единообразия, но обоснование теперь честное: «сегодня ручка влияет только на `check-ignore`, флаги едины, чтобы следующая команда не осталась без них». - «отключение конфига заодно гасит `diff.external`» — да, гасило, но моя замена на `-c diff.external=` **ломает git**: он пытается выполнить пустую команду и падает. Правильный инструмент — `--no-ext-diff` у самой команды `diff`, добавлен. **Ниты:** - **2** — обе половины покрыты: кейсы на глобальный и локальный `core.excludesFile`. Мутации «убрать `-c` у check-ignore» и «убрать `core.excludesFile` из списка» роняют по два кейса. - **3** — `run_case_out` с пином пути в выводе (трейсбек давал тот же exit=1) и `&&` вместо `;` между setup и запуском. - **4** — описание `no-issue-refs-in-comments` в манифесте теперь говорит про пробелы: весь смысл правки в том, что два хука обязаны говорить об одном формате одно и то же. - **5** — граница названа и закреплена: `TODO( # 456 )` остаётся нарушением, `# 456` — не ссылка. Мутация «разрешить пробел внутри ссылки» роняет новый кейс. - **6** — в примере конфига был обещан формат `TODO(arg, #42, ...)`, который regex отвергает с `v0.11.0`; это ровно тот дефект, на который жалуется issue, — переписано. - **7** — неактуален после перехода на `-c`: флаг поддерживается всеми живыми версиями git, no-op'а из-за старого git больше нет. **Про кейс на `--no-ext-diff` отдельно.** Первые две редакции были вакуумными: при сломанном diff нарушения не появляются, а **исчезают**, поэтому кейс с ожиданием 0 проходил в обе стороны. Правильный кейс ожидает 1 на branch-долге: регрессия здесь — пропажа вердикта, а не его появление. Мутация ловится только в такой постановке. **V1** — ветка отребейзена и force-запушена, версия `0.23.0`, гейт из [#29](https://git.homedevlab.ru/senokosov/pre-commit-hooks/pulls/29) на ней зелёный. Тег `v0.23.0` поставлю сразу после мержа.
claude-secretary deleted branch fix/deterministic-verdict-todo-spaces 2026-08-27 14:33:51 +07:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
senokosov/pre-commit-hooks!30
No description provided.