Skip to content

pre-push fails with fatal: bad revision '<default-branch>' when the default branch has no local ref #1474

Description

@alechemy

Description

pre-push hard-fails and blocks the push in any clone where the remote's default branch does not exist as a local branch, and the branch being pushed has no resolvable upstream (i.e. the normal first push of a new feature branch).

Repo.PushFiles() (internal/git/repo.go#L189) falls back like this:

  1. git diff --name-only HEAD @{push} — fails on a branch with no upstream.
  2. resolveHeadBranch() reads .git/refs/remotes/origin/HEAD and returns the bare branch name (main), then runs git diff --name-only HEAD main --.
  3. If no head branch can be resolved at all → git ls-tree -r --name-only HEAD.

Step 2 reads the remote's default branch name but then diffs against the local branch of that name. If that local branch doesn't exist, git exits 128 with fatal: bad revision 'main'. The error is returned rather than treated as a skip, so every job in the hook fails and the push is rejected.

The push-files computation runs unconditionally for pre-push as the "skip if nothing to push" gate in buildCommand (HookUsesPushFiles), so this happens even when no job references {push_files} or {files} at all — as in the config below, which is two whole-project typechecks. The computed list is only tested for emptiness and then discarded, so the hook dies on a value it never uses.

Setting a hook-level or command-level files: does not help — that only feeds the {files} template and does not replace the built-in push-files source.

Note that step 3 is a graceful fallback (ls-tree always works). It's only reachable when origin/HEAD is absent, so a clone with less remote information works fine while a normal clone with origin/HEAD set fails. Deleting refs/remotes/origin/HEAD is currently a working (if perverse) workaround.

Related: #1454 covers the same fallback picking a stale local default branch (wrong file list); this report is the harder failure where the local branch is missing entirely (push blocked). #1396 previously fixed the pathspec ambiguity on this same line.

lefthook.yml

pre-push:
  commands:
    probe:
      run: echo RAN

No {push_files} / {files} template anywhere — the failure does not depend on one.

Commands to reproduce

export LEFTHOOK_VERBOSE=true

# a remote whose default branch is `main`
git init --bare -q -b main remote.git
git init -q -b main seed && cd seed
echo hi > file.txt && git add -A && git commit -q -m init
git remote add origin ../remote.git && git push -q origin main
cd ..

git clone -q remote.git work && cd work
printf 'pre-push:\n  commands:\n    probe:\n      run: echo RAN\n' > lefthook.yml
git add -A && git commit -q -m cfg
lefthook install

# a clone that has no local `main` — e.g. someone who always branches
# straight off `origin/main` and doesn't keep a local default branch
git switch -q -c feature
git branch -D main

git push origin feature

Output:

╭─────────────────────────────────────╮
│ lefthook  v2.1.10   hook:  pre-push │
╰─────────────────────────────────────╯
│  [lefthook] git: git diff --name-only HEAD @{push}
│  > git diff --name-only HEAD @{push}
│    fatal: no upstream configured for branch 'feature'
│
│  [lefthook] git: git diff --name-only HEAD main --
│  > git diff --name-only HEAD main --
│    fatal: bad revision 'main'
│
│  probe (skip) exit status 128
  ────────────────────────────────────
summary: (done in 0.05 seconds)
✗ probe: exit status 128 (0.05 seconds)
error: failed to push some refs to '.../remote.git'

git branch main origin/main in the clone makes the same push succeed, which confirms the cause. The local branch never needs to be current — it only needs to exist.

Lefthook version

2.1.10 8d9cfec5f52367af6374a5430b4e9568844856e5 (also reproduced on 2.1.9)

Possible solution

Any of these would fix it, roughly in order of preference:

  1. Resolve the fallback against the remote-tracking ref (origin/main) rather than the bare local name, since origin/HEAD is where the name came from. This also addresses the staleness in {push_files} can include stale local default-branch files when @{push} is unavailable #1454.
  2. Fall through to the existing git ls-tree -r --name-only HEAD branch when the fallback diff fails, instead of propagating the error — the graceful path already exists one branch up.
  3. At minimum, don't fail the whole hook on it: a push-files computation that no job consumes should not be able to reject a push.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions