Problem
When force-pushing a rebased branch, prek's pre-push hook runs against a much larger set of files
than expected. It includes all files that changed on the default branch between the old branch base
and the new branch base -- not just the user's changes.
Workflow:
git checkout main && git pull
git checkout -b user/feature
# edit file_a.ts, commit, push
# later...
git fetch origin
git rebase origin/main
git push --force
On the force push, git sends prek:
refs/heads/user/feature <new-sha> refs/heads/user/feature <old-remote-sha>
Since <old-remote-sha> exists locally, prek diffs old-remote-sha...new-sha. But after a rebase,
the old remote sha is no longer an ancestor of the new sha -- the merge-base between them is far
back, and the three-dot diff includes all main-branch churn that was pulled in by the rebase.
Real-world impact: In a fast-moving monorepo, a single-file feature branch that rebases onto a
main branch that's moved 1,000+ commits triggers hooks on 250+ unrelated files. Expensive hooks
(type-checkers, linters, test suites) run on files the user never touched, turning a 5-second push
into a multi-minute wait.
Expected behavior
On force push of a rebased branch, prek should only check files that the user's branch actually
changed relative to the default branch -- the same set that would appear in a PR diff.
Perverse incentives
The current behavior incentivizes workarounds that are worse for the developer experience:
git push --no-verify -- skipping hooks entirely, defeating the purpose of pre-push
validation
- Deleting the remote branch before pushing
(git push origin :branch && git push origin branch) -- forces prek into the "new branch"
codepath which correctly scopes to user's commits only, but is awkward and error-prone
Neither should be necessary for the common "rebase and force push" workflow.
Root cause
In parse_pre_push_info, when the remote sha exists locally (Case A), prek always uses it as
from_ref. This is correct for normal pushes (adding commits to an existing branch) but incorrect
after a rebase, where the old remote tip is no longer in the new branch's ancestry.
Prior art
This is a known problem across pre-commit tools:
An internal fork of pre-commit addressed this by checking whether the old remote sha is an ancestor
of the new local sha, and falling through to "new branch" logic when it isn't (see
pre-commit/pre-commit#860, comment by @nicholasgasior).
Reproduction
# Setup
git clone <repo> && cd <repo>
git checkout -b test-branch
echo "hello" > test-file.txt
git add test-file.txt && git commit -m "add test file"
git push -u origin test-branch
# Simulate time passing (main moves forward)
git checkout main && git pull # main has new commits
# Rebase and force push
git checkout test-branch
git rebase origin/main
git push --force # <-- hooks now run on ALL files that changed on main
Environment
- prek 0.3.4
- macOS, git 2.x
- Large monorepo (~20k files, fast-moving main branch)
Problem
When force-pushing a rebased branch, prek's pre-push hook runs against a much larger set of files
than expected. It includes all files that changed on the default branch between the old branch base
and the new branch base -- not just the user's changes.
Workflow:
On the force push, git sends prek:
Since
<old-remote-sha>exists locally, prek diffsold-remote-sha...new-sha. But after a rebase,the old remote sha is no longer an ancestor of the new sha -- the merge-base between them is far
back, and the three-dot diff includes all main-branch churn that was pulled in by the rebase.
Real-world impact: In a fast-moving monorepo, a single-file feature branch that rebases onto a
main branch that's moved 1,000+ commits triggers hooks on 250+ unrelated files. Expensive hooks
(type-checkers, linters, test suites) run on files the user never touched, turning a 5-second push
into a multi-minute wait.
Expected behavior
On force push of a rebased branch, prek should only check files that the user's branch actually
changed relative to the default branch -- the same set that would appear in a PR diff.
Perverse incentives
The current behavior incentivizes workarounds that are worse for the developer experience:
git push --no-verify-- skipping hooks entirely, defeating the purpose of pre-pushvalidation
(
git push origin :branch && git push origin branch) -- forces prek into the "new branch"codepath which correctly scopes to user's commits only, but is awkward and error-prone
Neither should be necessary for the common "rebase and force push" workflow.
Root cause
In
parse_pre_push_info, when the remote sha exists locally (Case A), prek always uses it asfrom_ref. This is correct for normal pushes (adding commits to an existing branch) but incorrectafter a rebase, where the old remote tip is no longer in the new branch's ancestry.
Prior art
This is a known problem across pre-commit tools:
An internal fork of pre-commit addressed this by checking whether the old remote sha is an ancestor
of the new local sha, and falling through to "new branch" logic when it isn't (see
pre-commit/pre-commit#860, comment by @nicholasgasior).
Reproduction
Environment