Skip to content

Pre-push hook checks too many files after rebase + force push #2088

@jessrosenfield

Description

@jessrosenfield

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions