Import: SPA canonical-URL hint diverges from server normalization on .git/trailing-slash/uppercase non-GitHub URLs #401
Labels
No labels
actions
bug
cli
duplicate
enhancement
fork
forum
git storage
help wanted
insights
invalid
issues
moderation
oidc
ownership transfer
packages
pr/merge protection rules
projects
pull requests
question
releases
sponsorships
tags
webhooks
wiki
wontfix
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
crueber/walhub#401
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Import: SPA canonical-URL hint diverges from server normalization on
.git/trailing-slash/uppercase non-GitHub URLsRefiles #398 (closed, re-filed) — the earlier ticket was filed inline in violation of the Issue Writer contract and closed in favor of this one. Investigation for this re-file found that the server-side bug #398 described no longer reproduces; what remains is a client-side hint inconsistency. Scope of this ticket is therefore the SPA, not
internal/repoimport.What's requested
The import form's "canonical:" hint (and the prefill of owner/name for generic forges) must match the server's normalization contract so the user sees the same canonical source the server will clone. Today it doesn't for non-GitHub URLs.
Evidence
Server side —
.githandling is correct and pinned (the #398 report does not reproduce)internal/repoimport/url.go:https://github.com/<owner>/<repo>.git; everything else is rebuilt asscheme://host/pathwith host lowercased, default ports stripped, trailing slashes trimmed, and one trailing.gitremoved — so:443/uppercase/trailing-slash/.git/variants of one source collapse to a single canonical string. "The gated string IS the cloned string" —CloneMirrorreceivesNormalized.URL(task.go:77 passesparams.Source.URL).canonicalGenericURL(url.go:196-208) implements exactly that:strings.TrimRight(cu.Path, "/")thenstrings.TrimSuffix(p, ".git")(url.go:204-205).Tests prove the behavior end to end (all pass,
go test ./internal/repoimport/ -count=1→ ok):issue237_test.go:27-35— the variant table includeshttps://git.packden.us/crueber/dotfiles.git,…/dotfiles/,…/dotfiles.git/, and uppercase host; all must collapse to the canonicalhttps://git.packden.us/crueber/dotfiles(TestNormalizeCanonical237, issue237_test.go:41).TestCloneReceivesCanonicalURL237(issue237_test.go:355) proves through the real Begin → drive → CloneMirror path that the clone argv consumes the gated canonical URL — the exact "normalization survives to the clone args" check #398 asked for.TestVariantGateParityHTTP237proves one gate decision per logical source.A
https://git.example.com/owner/repo.git-shaped input lands on the same code path (NormalizeSource→canonicalGenericURL); there is no branch that carries a non-GitHub URL verbatim into the clone.Client side — the hint does not follow the contract (this ticket)
web/sdk/src/import.jsnormalizeSource(import.js:20-41):.gitform (import.js:23-35).return { url: s, kind: … }, import.js:40): no.gitstrip, no trailing-slash trim, no case folding, no default-port strip.web/src/pages/Import.jsx:73, 235-237,suggestion().url) and prefills owner/name from it (Import.jsx:227).So a user pasting
https://git.example.com/owner/repo.gitsees a "canonical" hint with the suffix (and slash/port/case variants produce five different hints), while the server normalizes all of them to one form. The label "canonical" is a lie on the generic path. For mirrored/POST/api/v1/repos/mirrorsusage the same SDK helper is the client's only normalization.Architecture notes
normalizeSourceis documented as client-side prefill only ("the server re-validates everything") — the fix is to make the client mirror the server'scanonicalGenericURLrules for the hint (strip one trailing.gitafter trimming trailing slashes, lowercase host, strip default ports — or simply label the hint as a preview and defer canonicalization to the server response), not to move trust client-side. The server gate and clone URL are already correct and must not change.internal/repoimport/url.go:4-13and the pinned variants inissue237_test.go:27-35.Acceptance criteria
normalizeSourceinweb/sdk/src/import.jsreturns the same canonical URL the server'sNormalizeSourceproduces for every variant row inissue237_test.go:27-35(plus the plain.gitform), for non-GitHub https hosts.https://git.example.com/owner/repo.git-shaped input.normalizeSourceis unchanged (still suggestshttps://github.com/<owner>/<repo>.git).internal/repoimportserver behavior; existing repoimport tests stay green.import.jsnames the server contract it mirrors (url.go canonical rules) so the two sides are cross-referenced.Fixed by #406 (#406): SPA-only change — normalizeSource mirrors canonicalGenericURL for non-GitHub URLs (hint + prefill now show the server canonical form). Server untouched; repoimport tests green.
Review: PR #406 (fix/issue-401) — verified in scratch worktrees, main untouched
MERGE RECOMMENDATION: ready to merge. No fixes pushed (nothing needed fixing); no structural issues. All verification done in scratch worktrees (
origin/fix/issue-401vs pristineorigin/main), both removed afterward. Main worktree left clean. No browser used — not needed (change is pure JS normalization logic + headless tests; verified with node tests + reasoning, as instructed). No docker, no live instances touched.1. Mirror fidelity — canonicalGenericHint matches canonicalGenericURL rule-for-rule
web/sdk/src/import.jsvsinternal/repoimport/url.go:196-208: host lowercase ✓, default-port set{443/80/22/9418}identical to GoisDefaultPort✓, trailing-slash trim (/\ youthful+$/≡TrimRight(p,"/")) ✓, exactly ONE.gitstrip (case-sensitive on both sides) ✓, query/fragment preserved verbatim ✓, path case preserved ✓, IPv6 bracket wrap ✓, userinfo / non-default port / unsupported scheme / unparseable → verbatim for the server to 400 ✓. Edge probes (node, against Go semantics):r.git.git→r.git,r.GITkept,?x=1#fragpreserved,o//r//interior doubles kept,https://…:443/…/folds,HTTPS://HOST/…lowercases host only — all identical to the server. Two degenerate-input observations, both non-blocking: (a)https:///o/r.git— WHATWG URL parses hostowhere Go sees empty host → JS canonicalizes something the server 400s (unfixable parser difference, garbage-in); (b)ssh:///git://URLs are canonicalized in the hint thoughValidateTransportlater 400s them — this still matches the mirroredNormalizeSourcecontract, so it is correct per the issue's stated mirror target.2. Test oracle — exact rows pinned
sdk-import.test.jspins all 5issue237_test.go:27-35variants + the bare canonical form (6 rows → one canonical URL, kind/owner/name asserted), the http twin +:80fold, GitHub.git-form unchanged, and the two verbatim-refusal rows (non-default:8443, embeddeduser:token) — the refusal set matches the server 400s (url.go:93-94, 108-110).sdk-import.test.js: 8/8 pass.3. Owner/name prefill — Import.jsx untouched, claim holds
Diff touches exactly 3 files (
10_git_import.md,sdk/import.js,sdk-import.test.js).Import.jsx:227-229(prefill) and:235-237(hint) both consumenormalizeSource(), so the SDK fix corrects both with zero UI changes.4-5. GitHub path byte-identical; server untouched
GitHub shorthand/regex branches are untouched lines; old assertions pass unchanged. Zero
.gofiles in the diff;go test ./internal/repoimport/ -count=1→ ok (26s).6. Full-suite failures — byte-identical on pristine main, environmental
PR branch: node 797 tests / 795 pass / 2 fail; pristine main: 796 / 794 / same 2 fail — delta is exactly the +1 new passing test. Both failures are
smoke.test.js(needs a live dev server on :8080; this env answers 401 from a foreign instance — environmental, untouched by this change, nowhere near import/markdown paths).vite build+esbuildgreen on both. Go fast tier (go test -short -count=1 ./...) fully green on the PR branch. One count discrepancy to note: the PR description says "13 pre-existing failures" but I reproduce only these 2 (node) + 0 (Go) in my scratch — the failures I do see are all environmental and identical on main, so this changes nothing about the verdict, but the description number looks stale.7. Contract comment, docs, deps
import.jsheader +canonicalGenericHintdocstring both nameinternal/repoimport/url.go:196-208and header contracturl.go:4-13✓ (one-sided cross-ref is correct — server intentionally untouched per acceptance criterion 4). Law-12 decision appended underdocs/features/10_git_import.md ## Decisions, accurate (verbatim-refusal set, GitHub unchanged, #398 non-repro stated) ✓. Nopackage.jsonchange, no new imports (pure JS +new URL) — Law 1 holds ✓.Fixed by PR #406 (review clean — rule-for-rule mirror fidelity incl. edge probes, oracle pinned, server untouched), merged. Closing.