What's wrong
MirrorStore.IsSafeSegment (GitBranchStateCache/Mirrors/MirrorStore.cs:148-163) only allows [A-Za-z0-9._-] in each path segment. Its remarks (lines ~137-141) justify this with "every repository path a forge actually issues is already within this set". That is not true for Azure DevOps, where project names, and repository names, routinely contain spaces (e.g. https://dev.azure.com/myorg/My Project/_git/game).
Nothing earlier rejects such a path:
RepositoryPattern and GitBranchStateCacheOptionsValidator accept "My Project/_git/game" as an allow-list entry. The README says startup validation "reports every problem at once".
UpstreamUrl.TryCombine would escape the path correctly to %20.
So the service starts cleanly with a configuration that can never serve that repository.
At request time, BranchStateHandler.Resolve (Endpoints/BranchStateHandler.cs:412-416) returns null without logging when mirrors.TryResolve fails. Unknown-upstream and not-allow-listed requests both log (lines ~400, ~406). The operator sees only 404s from clients and nothing explaining them.
The refusal itself is deliberate (see MirrorStoreTests.TryResolve_UnsafeRepositoryPath_IsRefused, "studio/re po"). The bug is that it is invisible: startup accepts the configuration and runtime rejects it silently.
Reproduction
- Config:
Upstreams:ado:BaseUrl=https://dev.azure.example/myorg, Upstreams:ado:Repositories:0="My Project/_git/game".
- Request:
POST /v1/ado/My%20Project/_git/game/state.
- Result:
404 {"error":"no-such-repository",...}. The only git invocation was the startup git --version, and no log line mentioned the refusal. This was reproduced with the repo's own ServiceFixture and ScriptedGit.
Suggested fix
- Minimum:
- Have the options validator reject any allow-list pattern whose literal segments, or any upstream key,
MirrorStore would refuse, with a message naming the entry.
- Add an
EndpointLog line on the TryResolve failure path.
- Better, for Azure DevOps users: support such names by mapping a segment to a reversible, readable directory name (e.g. percent-encoding the characters outside the safe set), so
My Project becomes My%20Project on disk and cannot collide with another repository.
Acceptance criteria
- A configuration containing
"My Project/_git/game" either serves that repository, or fails startup validation with a clear message.
- A request refused by
MirrorStore.TryResolve produces a log entry.
What's wrong
MirrorStore.IsSafeSegment(GitBranchStateCache/Mirrors/MirrorStore.cs:148-163) only allows[A-Za-z0-9._-]in each path segment. Its remarks (lines ~137-141) justify this with "every repository path a forge actually issues is already within this set". That is not true for Azure DevOps, where project names, and repository names, routinely contain spaces (e.g.https://dev.azure.com/myorg/My Project/_git/game).Nothing earlier rejects such a path:
RepositoryPatternandGitBranchStateCacheOptionsValidatoraccept"My Project/_git/game"as an allow-list entry. The README says startup validation "reports every problem at once".UpstreamUrl.TryCombinewould escape the path correctly to%20.So the service starts cleanly with a configuration that can never serve that repository.
At request time,
BranchStateHandler.Resolve(Endpoints/BranchStateHandler.cs:412-416) returnsnullwithout logging whenmirrors.TryResolvefails. Unknown-upstream and not-allow-listed requests both log (lines ~400, ~406). The operator sees only 404s from clients and nothing explaining them.The refusal itself is deliberate (see
MirrorStoreTests.TryResolve_UnsafeRepositoryPath_IsRefused,"studio/re po"). The bug is that it is invisible: startup accepts the configuration and runtime rejects it silently.Reproduction
Upstreams:ado:BaseUrl=https://dev.azure.example/myorg,Upstreams:ado:Repositories:0="My Project/_git/game".POST /v1/ado/My%20Project/_git/game/state.404 {"error":"no-such-repository",...}. The only git invocation was the startupgit --version, and no log line mentioned the refusal. This was reproduced with the repo's ownServiceFixtureandScriptedGit.Suggested fix
MirrorStorewould refuse, with a message naming the entry.EndpointLogline on theTryResolvefailure path.My ProjectbecomesMy%20Projecton disk and cannot collide with another repository.Acceptance criteria
"My Project/_git/game"either serves that repository, or fails startup validation with a clear message.MirrorStore.TryResolveproduces a log entry.