KhueApps
Home/DevOps/Fix Git error: cannot lock ref is at X but expected Y

Fix Git error: cannot lock ref is at X but expected Y

Last updated: October 07, 2025

What this error means

Git refused to update a ref because its current value did not match the value Git expected when it started the update. This usually indicates:

  • A concurrent Git operation updated the same ref.
  • A stale lock file or packed-refs inconsistency.
  • Case-insensitive filesystem ref collisions.
  • Permissions or read-only files preventing ref updates.

You’ll typically see it during fetch/pull/push, e.g. updating refs/remotes/origin/<branch> or refs/heads/<branch>.

Quickstart (safe fixes)

  1. Stop concurrent Git operations

    • Close IDE auto-fetch, background watchers, or parallel CI steps.
  2. Retry the operation

    • Run: git fetch --prune --tags or rerun your original command.
  3. Remove stale lock files (only if no Git process is running)

    • POSIX:
      find .git -type f -name "*.lock" -print -delete
      
    • PowerShell:
      Get-ChildItem -Recurse .git -Filter *.lock | Remove-Item -Force
      
  4. Normalize refs and clean up

    • git gc --prune=now --aggressive
      git pack-refs --all
      git fsck --full
      
  5. Force-refresh remote-tracking refs (trusting the remote)

    • git fetch origin +refs/heads/*:refs/remotes/origin/* --prune --tags
      

If the error persists, follow the scenarios below.

Minimal working example (why the error happens)

This demonstrates two conflicting updates to the same ref.

# Start a scratch repo
rm -rf demo && mkdir demo && cd demo

git init

echo a > f && git add f && git commit -m "a"
old=$(git rev-parse HEAD)

# Create two different future commit IDs
git commit --allow-empty -m "b"; new1=$(git rev-parse HEAD)
git commit --allow-empty -m "c"; new2=$(git rev-parse HEAD)

# Create a ref at the old value
git update-ref refs/heads/tmp "$old"

# First update succeeds
git update-ref refs/heads/tmp "$new1" "$old"

# Second update expects the same old value, but the ref is now new1
# This reproduces: "cannot lock ref ... is at <new1> but expected <old>"
git update-ref refs/heads/tmp "$new2" "$old" || true

Root cause: Git performs an atomic compare-and-swap. If the ref changed since it was read, the update is aborted to prevent races.

Diagnose the failure

  • Identify the failing ref from the message, e.g. refs/remotes/origin/feature/x.
  • Check current value vs. expected (from the error):
    git rev-parse refs/remotes/origin/feature/x 2>/dev/null || echo "ref missing"
    
  • Look for leftover locks:
    find .git -type f -name "*.lock"
    
  • Detect case-colliding refs (on case-insensitive filesystems):
    git for-each-ref --format='%(refname)' | awk '{print tolower($0)}' | sort | uniq -d
    
  • Verify repository health:
    git fsck --full
    

Scenario-based fixes

  1. Concurrent fetch/pull/push
  • Symptom: Occurs intermittently; rerun often succeeds.
  • Fix:
    • Ensure only one Git process runs per repo at a time.
    • Retry: git fetch --prune --tags.
    • In CI, serialize steps or use a clean clone per job.
  1. Stale lock files
  • Symptom: .lock files remain after a crash or kill.
  • Fix:
    • Ensure no Git processes are running.
    • Delete *.lock under .git/ (see Quickstart step 3).
  1. Packed-refs vs. loose refs mismatch
  • Symptom: Repeated errors on fetch; refs appear both in .git/refs/* and .git/packed-refs.
  • Fix:
    git pack-refs --all
    git gc --prune=now
    git fetch --prune --tags
    
  1. Case-colliding refs (Windows/macOS default FS)
  • Symptom: Branches or tags differ only by case (e.g., Feature vs feature).
  • Detect and resolve:
    git for-each-ref --format='%(refname)' | sort -f | uniq -d
    # Rename on the remote and locally to a unique, canonical name
    git branch -m Feature feature
    git push origin :Feature feature
    git fetch --prune
    
  • Consider enforcing naming rules in reviews/CI.
  1. Permissions/read-only files
  • Symptom: Error persists; .git/ is not writable by your user.
  • Fix:
    • POSIX: chown -R "$USER" .git && chmod -R u+rwX .git
    • Windows: Unset read-only attributes on .git and its children.
  1. Force-refresh remote-tracking refs (trust remote state)
  • Symptom: Only remote-tracking refs fail (e.g., refs/remotes/origin/*).
  • Fix:
    git fetch origin +refs/heads/*:refs/remotes/origin/* --prune --tags
    
    The leading + forces updates even if they are non-fast-forward.
  1. Broken or corrupt refs
  • Symptom: git fsck reports issues; ref points to missing object.
  • Fix:
    • If safe to delete a remote-tracking ref:
      git update-ref -d refs/remotes/origin/feature/x
      git fetch --prune
      
    • If a local branch is corrupt, create a new branch from a known good commit and delete the bad one.

Preventing recurrence

  • Enable pruning and avoid stale remote refs:
    git config fetch.prune true
    git config fetch.pruneTags true
    
  • Avoid parallel Git operations on the same working directory.
  • Keep the repo healthy periodically:
    git maintenance run --task=gc
    
  • Enforce branch naming to avoid case collisions; consider receive.denyDeleteCurrent and receive.denyNonFastForwards on servers.

Performance notes

  • git gc --aggressive is CPU/IO intensive; use sparingly on large repos or run during off-peak times.
  • git fsck --full can be slow on big histories; run for diagnostics, not on every CI run.
  • For CI, prefer clean clones over heavy maintenance; they are often faster and more reliable.

Pitfalls

  • Deleting .lock files while a Git command is running can corrupt refs. Always stop all Git processes first.
  • Forcing ref updates (+ in fetch) can move refs backward. Only do this when you trust the remote.
  • Case-colliding refs might reappear if server and clients use different filesystems. Standardize branch naming.

Tiny FAQ

  • Why does this happen?

    • A ref changed between read and write, or Git could not safely create/update the ref due to locks, collisions, or permissions.
  • Is deleting .lock files safe?

    • Yes, if you are certain no Git process is running. Otherwise, wait or kill the process first.
  • I only see this on Windows/macOS. Why?

    • Case-insensitive filesystems can introduce ref name collisions. Also, antivirus or file indexers can transiently lock files.
  • How do I fix it on a server-side bare repo?

    • Stop all hooks/operations using that repo, remove stale locks, run git gc --prune=now, verify with git fsck, then retry the push.

Series: Git

DevOps