Created
April 17, 2026 10:22
-
-
Save sueszli/5bd5c6963152e595706f21dc6e537576 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # Reproduce: on a case-insensitive filesystem (macOS APFS default), two remote | |
| # branches whose names differ only by letter case make `git fetch --prune` | |
| # race itself and fail with: | |
| # Unable to create '.../refs/remotes/origin/<name>.lock': File exists | |
| # | |
| # Self-contained. Builds throwaway repos under /tmp, triggers the failure, | |
| # prints a tight summary, and cleans up. Safe to run repeatedly. | |
| # | |
| # Exit codes: | |
| # 0 bug reproduced (your environment is affected) | |
| # 1 bug NOT reproduced (different git version / reftable backend / etc.) | |
| # 2 skipped — filesystem is case-sensitive, bug cannot manifest here | |
| set -u | |
| DIR=$(mktemp -d "/tmp/case-ref-collision.XXXXXX") | |
| trap 'rm -rf "$DIR"' EXIT | |
| # 1. Filesystem check | |
| : > "$DIR/a" | |
| if [ ! -e "$DIR/A" ]; then | |
| echo "SKIP: filesystem is case-sensitive; this bug cannot occur here." | |
| exit 2 | |
| fi | |
| rm -f "$DIR/a" | |
| # 2. Build a bare "remote" with two case-colliding branches. | |
| # `git branch TEST` fails on a case-insensitive FS, so inject directly into | |
| # the bare remote's packed-refs (plain text file, no FS collision). | |
| git init --bare -b main "$DIR/remote.git" >/dev/null | |
| git init -q -b main "$DIR/seed" | |
| ( | |
| cd "$DIR/seed" | |
| git config user.email reproducer@example.com | |
| git config user.name reproducer | |
| echo x > f | |
| git add f | |
| git -c commit.gpgsign=false commit -q -m init | |
| git remote add origin ../remote.git | |
| git push -q origin main | |
| ) | |
| SHA=$(git -C "$DIR/seed" rev-parse HEAD) | |
| cat > "$DIR/remote.git/packed-refs" <<EOF | |
| # pack-refs with: peeled fully-peeled sorted | |
| $SHA refs/heads/main | |
| $SHA refs/heads/test | |
| $SHA refs/heads/TEST | |
| EOF | |
| # 3. Clone. Both colliding refs land in the client's packed-refs. | |
| git clone -q "$DIR/remote.git" "$DIR/work" | |
| # 4. Delete both colliding branches upstream. | |
| cat > "$DIR/remote.git/packed-refs" <<EOF | |
| # pack-refs with: peeled fully-peeled sorted | |
| $SHA refs/heads/main | |
| EOF | |
| # 5. Trigger the race. | |
| OUT=$(git -C "$DIR/work" fetch --prune 2>&1) && RC=0 || RC=$? | |
| ERR=$(printf '%s\n' "$OUT" | grep -E "Unable to create .*\.lock.*File exists" | head -n1) | |
| if [ -n "$ERR" ]; then | |
| echo "REPRODUCED on $(git --version):" | |
| echo " $ERR" | |
| echo | |
| echo "Root cause:" | |
| echo " On a case-insensitive FS, refs/remotes/origin/test.lock and .../TEST.lock" | |
| echo " resolve to the same path. git fetch --prune creates one .lock then tries to" | |
| echo " create the other at the same path -> File exists. git is racing itself." | |
| echo | |
| echo "Fix in an affected real-world repo:" | |
| echo " git update-ref -d refs/remotes/origin/<one-of-the-two-colliding-refs>" | |
| echo " git fetch --prune" | |
| exit 0 | |
| fi | |
| echo "NOT REPRODUCED. git fetch --prune exit=$RC" | |
| echo "--- full output ---" | |
| printf '%s\n' "$OUT" | |
| echo "-------------------" | |
| echo "Your git may use the reftable backend or include a fix for this case." | |
| exit 1 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment