Skip to content

checkpoint: accept case-folded shard directories in ParseRef - #2403

Merged
Soph merged 1 commit into
entireio:mainfrom
KC1706:fix/ulid-shard-case-collision
Sep 15, 2026
Merged

Soph merged 1 commit into
entireio:mainfrom
KC1706:fix/ulid-shard-case-collision

Conversation

@KC1706

@KC1706 KC1706 commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Ref #2402

On a case-insensitive-but-case-preserving filesystem (macOS APFS, Windows NTFS defaults), a checkpoint ref's shard directory can get case-folded by the filesystem itself — e.g. a ULID checkpoint whose correct shard is 6B lands inside an already-present 6b directory left by an unrelated legacy hex checkpoint. ParseRef compared the recomputed shard against the ref's actual path component byte-for-byte, so "6B" != "6b" caused a well-formed, intact checkpoint to be rejected as malformed and silently disappear from checkpoint list/explain.

This PR makes that comparison case-insensitive (strings.EqualFold), matching what the filesystem already does, and adds two regression tests reproducing both directions of the collision (ULID→lowercase dir, legacy-hex→uppercase dir).

Verified locally: mise run fmt && mise run lint (0 issues) and mise run test:ci pass.

On a case-insensitive-but-case-preserving filesystem (macOS APFS, Windows
NTFS defaults), git resolves a new checkpoint ref's shard directory against
existing ones case-insensitively. A ULID checkpoint's canonical shard is
uppercase (e.g. "6B"), so if an unrelated legacy hex checkpoint already
shards to the lowercase form ("6b"), the new ref lands inside that same
lowercase directory instead of a distinct one.

ParseRef then rejected such refs outright: it recomputed the ID's shard and
required an exact, case-sensitive match against the ref's actual path
component, so "6B" != "6b" caused a well-formed, intact checkpoint to be
treated as malformed and silently disappear from `entire checkpoint list`/
`explain`.

Fix: compare the shard case-insensitively (strings.EqualFold) instead of
exactly. Adds regression tests reproducing both directions of the collision.

Fixes entireio#2402

@Soph Soph left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for doing this. I'll merge it and do a follow up PR addressing the other places where this is an issue in a few minutes. Will close your issue after merging that one.

Thanks!

@Soph
Soph enabled auto-merge September 15, 2026 13:46
@Soph
Soph merged commit 5ef701f into entireio:main Sep 15, 2026
15 checks passed
Soph added a commit that referenced this pull request Sep 15, 2026
#2403 made ParseRef tolerate a checkpoint ref stored under the other
spelling of its shard directory, which fixes `checkpoint list`. Every
other consumer goes the other way — RefName(cid) then repo.Reference —
and there it was the filesystem masking the collision, not ParseRef: a
case-insensitive filesystem resolves the canonical name onto the folded
directory, but only while the ref is loose. git pack-refs (git gc --auto
runs it) moves the ref into packed-refs under its stored name, where
lookup is an exact string match.

resolveLocalRef falls back to the one alternate-cased spelling, which is
exhaustive rather than a heuristic: each ID format uses one case
exclusively, so a shard bucket has exactly two possible names. That fixes
Read (and so `checkpoint explain <id>`), refBase, and GetCheckpointAuthor.

writeRefName then targets the spelling an existing ref actually uses, so
a write extends the checkpoint instead of forking it. A tolerant read on
its own is not enough: refBase would find the old ref and hand back its
tip while the CAS created a second ref at the canonical name — and the
CAS could not succeed there anyway, since git resolves the expected old
value under the name being updated.

List dedupes the two spellings for repos a pre-fix CLI already forked,
preferring the canonical one so the listing agrees with what Read serves.

doctor reports both conditions. It offers no rename for a folded ref:
on the filesystem that produces it, update-ref to the canonical name and
update-ref -d on the folded one hit the same file while the refs are
loose, so the pair that looks like a rename is a delete.

Refs #2402

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M2JP7ZYMCG44RMG2RGZS7304
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants