Skip to content

JevGate 0.31.0: the commit moment, and the release (Git hooks judge what a push sends or a commit records) - #48

Merged
tauanbinato merged 6 commits into
mainfrom
v0.31
Sep 29, 2026
Merged

tauanbinato merged 6 commits into
mainfrom
v0.31

Conversation

@tauanbinato

@tauanbinato tauanbinato commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

This pull request is the release too: its last commit is Release 0.31.0 (version, lockfile, packages, pins, CHANGELOG), after the release check's three considers were fixed in Give billing, the headline and Markdown spans modules of their own. JevGate's check of the whole repository with every rule then passed with no review or consider left, apart from the one already baselined in 0.30.0 and one on a gitignored local note. Merged, main gets tagged v0.31.0.

The roadmap's commit moment, to be released as 0.31.0. A Git hook runs the gate before each push or commit, on what the push sends or the commit records, read from Git rather than the working tree. A check that cannot finish lets the change through and says so, instead of holding a commit while its retries run. jevgate init --git-hook writes the hook.

What changes

  • check --pre-push judges what a push sends.
    • Refs: it reads the refs Git passes a pre-push hook on stdin. Under pre-commit and prek it reads the ref they pass as PRE_COMMIT_TO_REF; on a terminal, the current branch.
    • Base: each pushed commit is compared with the last commit on its first-parent line that a remote already has, as Qlty's pre-push check does (feat(slop-one): compare changed files and print a refactoring prompt when one declines qltysh/qlty#2867): a branch pushed before with its last push, a new branch with where it leaves the remote's history, a rebased branch with where it leaves that history now.
    • Merges: where Qlty stops, a push that merges in commits a remote has is compared with them merged as git merge-tree merges them, conflict markers included. After git merge origin/main, the first-parent comparison judged main's changes as the push's own; this one judges the push's commits and its conflict resolutions.
    • Content and refs: files are read as committed. Deleted refs are skipped, and each distinct change is checked once. A branch none of whose history is on a remote is not checked, and a line says so.
  • check --staged judges what a commit records: the index against HEAD, never untracked files.
    • What changes for current users: the pre-commit hooks used to run check --base HEAD, which judges the working tree plus untracked files. A file staged in part with git add -p was judged with lines the commit didn't hold, and an untracked file could stop a commit it wasn't part of.
    • Where it reads: the files the change touched are read from the index Git commits (GIT_INDEX_FILE, so commit -a and git commit PATHS work). So are the stale-source rechecks and the jevgate: allow comments, which read the working tree and would have failed or misplaced findings on a partly staged file.
    • Edge cases: a commit that concludes a merge is judged for its resolution. Before the first commit, every staged file is new.
  • Fail open, loudly:
    • Setting: on_incomplete = "pass" | "fail" (--on-incomplete). It passes by default with --staged and --pre-push, and fails everywhere else, so CI still exits 2.
    • Covers: a missing key, a 402, a provider that stops answering, a budget, an unloadable configuration or a Git failure. The check exits 0, and its last two lines on stderr say the commit or push was not checked, and why. Ctrl-C still stops the commit, and usage errors still exit 2.
  • Budgets:
    • max_seconds / --max-seconds: 60 by default for the hooks. No request starts and no retry waits past it, and an attempt under way gets only the time left.
    • max_cost / --max-cost: dollars. Each request is priced once from its size before it is sent, with notices at 75% and 90%.
    • Outage memory: after a provider failure that passes with time, the hooks' checks use cached answers only for five minutes. This is the agent hook's outage memory, sharing .jevgate/turns/outage.json.
  • Enter to skip: after a second, on a terminal, only when stderr is one, so an agent's pipe never offers it. Checked in a pseudo-terminal. Under lefthook it reaches the check only in a job with use_stdin: true (the pre-push recipe) or interactive: true: other jobs run in lefthook's own pseudo-terminal, which gets no keys. The docs say so.
  • What an agent reads: a stopped commit or push ends with "JevGate stopped this push: fix the findings above and push again". It adds that a person may allow or baseline a finding, and that a coding agent never bypasses the hook with --no-verify, an allow comment or the baseline.
  • jevgate init --git-hook pre-push|pre-commit [--remove] [--dry-run]:
    • It writes .git/hooks/pre-push or pre-commit, never over a hook it didn't write.
    • Where pre-commit, prek, lefthook or core.hooksPath (husky) keep the hooks, it names what to add there instead.
    • The hook lets the change through, saying so, when jevgate isn't on the PATH.
  • .pre-commit-hooks.yaml: jevgate and jevgate-system now run check --staged at the pre-commit stage only. Before, they ran --base HEAD at every installed stage. New jevgate-push and jevgate-push-system run check --pre-push. All four are verbose, since pre-commit hides a passing hook's output, and they would otherwise hide the not-checked notice. They need pre-commit 3.2 or later.
  • Report: "staged": true and pushed_revision are additive fields. The headline reads staged lines since … or changed lines from … to ….
  • Docs: a new Git hooks page with recipes. The pre-commit snippet moves there from the CI page. The configuration, output, stability, coding agents and troubleshooting pages and the README are updated.

Measurements

Latency, uncached. On the last commits of the 83 corpus projects whose last commit asks something (the release build, --base HEAD~1 --refresh, the content a one-commit push sends), run one at a time as a hook runs:

p50 p90 p95 max over 10 s cost
1.0 s 4.1 s 5.6 s 8.4 s 0 $0.097 (1,073 requests)
  • 60-second default: it is 7 times the slowest of these commits. It bounds an outage at a minute, where a provider that failed every attempt held a run of 100 requests for 8 minutes.
  • Stops: the default gate stopped 12 of the 83, all on function-simplification reviews, 8 of them in the maintainer's own repositories.

Recipes, end to end. Each tool ran in a scratch repository against a local mock provider. A review stops, and a 402 lets the change through with the notice shown.

commit, review commit, 402 push, review push, 402
jevgate init --git-hook stopped (1) through (0) + notice stopped (1) through (0) + notice
pre-commit 4.6.2 stopped through + notice stopped through + notice
prek 0.5.4 stopped through + notice stopped through + notice
lefthook 2.1.14 (use_stdin: true) stopped through + notice stopped through + notice
husky 9.1.7 stopped through + notice stopped through + notice

Self-check. With every rule, on this branch's diff: the gate passes with no review or consider. Four considers from an earlier run were fixed:

  • a shared raw-diff parser;
  • the rules listing moved out of configured;
  • Report::incomplete_reason;
  • a shared test setup.

Done when

  • Partial staging: a commit of a partially staged file is judged as staged, and untracked files are never judged. staged_judges_what_the_index_holds_at_the_lines_it_holds_them gets the finding at the index's line 1, while the working tree holds another function two lines lower and an untracked copy. staged_reads_the_index_git_commits_from covers a temporary GIT_INDEX_FILE.
  • Checks that can't finish: an outage, a 402 or a missing key lets a commit or push through within the time budget, with the notice, and stops it under on_incomplete = "fail". Tests: a_commit_or_a_push_goes_ahead_when_the_check_cannot_finish_and_says_so, budgets_stop_the_asking_and_leave_the_run_incomplete (a 5 s provider cut at 1 s), and a_provider_failure_is_waited_out_by_the_next_commits.
  • Exact pushes: a pre-push check judges exactly the pushed commits. Unit tests cover a new branch, a fast-forward, a force push after a rebase, a merge of main, a resolved conflict, a deleted ref, a tag and an unrooted history. a_push_of_several_refs_checks_each_distinct_change_once covers several refs.
  • Recipes: each one runs end to end in a scratch repository (table above). the_hooks_stop_a_commit_and_a_push_and_let_through_what_they_cannot_check runs a real git commit and git push through the installed hooks.

Decisions

  • Approved with the plan:
    • on_incomplete passes by default only for the two hooks.
    • The old pre-commit hook IDs stay at the pre-commit stage, with --staged, and the new push IDs are what the docs lead with. Moving the old IDs to pre-push would quietly stop them for anyone who installed only the pre-commit hook.
    • init --git-hook is in.
  • Made while building:
    • Merges are compared with the merged base, for pushes and merge commits.
    • The outage memory is shared with the agent hook.
    • Enter is offered only when stderr is a terminal.
    • The pre-commit hooks are verbose.
    • on_incomplete = "fail" also stops a push that cannot be compared, because none of its history is on a remote.

`check --pre-push` judges what a push sends: the refs Git passes a
pre-push hook, or the one pre-commit and prek pass as PRE_COMMIT_TO_REF,
each compared with the last commit on its first-parent line that a
remote has, or with the commits a remote has that the push merges in,
merged as Git merges them. `check --staged` judges what a commit records:
the index against HEAD, never untracked files, and a merge commit for its
resolution. The files the change touched are read from Git, and so are
the source recheck and the allow comments.

A Git hook's check that cannot finish lets the change through and says
so (`on_incomplete`, pass by default for the hooks, fail elsewhere),
stops asking after 60 seconds (`max_seconds`) or a dollar budget
(`max_cost`), waits out a provider failure for five minutes as the agent
hook does, and can be skipped with Enter on a terminal. A stopped commit
or push tells a coding agent to fix the findings and never to bypass the
hook. `jevgate init --git-hook pre-push|pre-commit` writes the hook, and
the pre-commit hooks run the new checks.
…nd husky

A Git hooks page says what each hook judges, what happens when a check
cannot finish, and how to run the hooks from pre-commit, prek, lefthook,
husky or any hook runner; each recipe ran end to end. The CI,
configuration, output, stability, coding-agent and troubleshooting pages
and the README point to it, and the pre-commit snippet moves there from
the CI page.
The release check asked, in considers, for members of requests.rs,
output/mod.rs and docs/references.rs to move to modules of their own:
the dollar budget and the bill go to requests/billing.rs, the headline to
output/headline.rs, and the inline code spans and fences the references
rule reads to docs/spans.rs. Nothing else changes.
@tauanbinato tauanbinato changed the title JevGate 0.31.0: the commit moment (Git hooks judge what a push sends or a commit records) JevGate 0.31.0: the commit moment, and the release (Git hooks judge what a push sends or a commit records) Sep 29, 2026
@tauanbinato
tauanbinato merged commit a98a116 into main Sep 29, 2026
17 checks passed
@tauanbinato
tauanbinato deleted the v0.31 branch September 29, 2026 01:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant