repo refs: the .git suffix is reserved in every case

main

Commit

hiddecoyesterday

The CLI spelled one rule five ways -- a HasSuffix in repo create, three TrimSuffix calls across the ref parsers, and a ToLower-wrapped one in the mirror-URL parse. All case-sensitive, each with its own list of examples. Three were wrong, and each one's tests passed.

repo create refused "test.git" with a message naming the spelling to use, and let "test.GIT" through to the server, which rejects it for carrying uppercase: a true statement about a different problem, leaving the suffix rule to be discovered on a second attempt.

parseMirrorCloneURL ran ToLower(TrimSuffix(x)). "CLI.GIT" missed the case-sensitive cut and folded to "cli.git" -- a spelling stored mirrors never carry, so the EqualFold match below it could not find a mirror that plainly exists.

The ref parsers disagreed with the server they dial. A repo transport path accepts the suffix whatever its case, so git clone entire://.../et/acme/widgets.GIT succeeds while entire repo clone /et/acme/widgets.GIT reported no such repo.

One helper now answers the question. Canonical git draws this line by purpose, not by taste: case-sensitive when it is merely guessing a local directory name from a URL (strncmp in git_url_basename), and aggressively case-insensitive -- plus HFS ignorable codepoints and NTFS short names -- when asking whether a path IS the reserved .git (is_hfs_dotgit, is_ntfs_dotgit). Both of the CLI's uses are the latter.

The helper lives in gitremote, exported with the constant, and the cli package calls it: cli already imports gitremote, so there is one copy of the rule rather than two kept in sync by comments.

The fold is Entire's, so gitremote.ParseURL applies it only to entire:// remotes. A remote dialed straight at a forge keeps git's case-sensitive cut, because that is what the forge does: GitHub's transport does not find git/git.GIT, and reporting "repo" for github.com/org/repo.GIT would name a repo the URL never reaches.

The .git miss hint now covers both bare-name routes. With --project given by name, a bare name resolves through repos/resolve, which matches the name as sent, and its miss (noRepoAtPathErr) was the one that never said why.

Also send the name that was checked. repo create validated TrimSpace(args[0]) and put the raw args[0] on the wire, which only ever worked because the server trims too.

The fixture tables are shared by every call site's tests, so none can pass behind a narrower list again. They cover the case variants, whitespace at both ends, single-cut semantics, the near-misses git pins in its own tests (gitops, .gitignore), and multi-byte final runes.

Refs COR-1892

Entire-Checkpoint: 01M3SM7D59ZJGE3CBT8BABWQ7Q

Checkpoints

Standardize Repository Name Validation Logic

Claude CodeOpus 5.[1m]
View session
Checkpoint 1