Repository navigation
fix(utils): percent-encode branch names in provider branch URLs - #17107
Conversation
A branch name such as `feature/ui#123` is a valid git ref but was interpolated raw into the branch link for every provider, so the browser treated `OpenHands#123` as a fragment and requested `feature/ui` instead. A ref containing `%2F` is also valid, and produces a path that decodes back to a `/`, pointing the link at a different branch again. Encode per context rather than blanket-encoding, which would turn `release/1.0` into `release%2F1.0` on path-based links: - github, gitlab, bitbucket and forgejo place the branch in the URL path, so encode each `/`-separated segment and leave the separators alone. - azure_devops (`?version=GB`) and bitbucket_data_center (`?at=refs/heads/`) place it in a query value, so encode the whole name including `/`, where an unencoded `&` would otherwise start a new query parameter. Visible branch text is unchanged; only the href is affected.
❌ Mock-LLM E2E Tests69/70 passed · 1 failed Commit: Details
🔍 Failure details (1)❌ settings/mock-llm-profile-management.spec.ts › OpenHands provider hidden base_url preservation › re-saving an OpenHands profile from Basic view preserves hidden base_urlPosted by the Mock-LLM E2E workflow · results are deterministic (scripted LLM responses) |
all-hands-bot
left a comment
There was a problem hiding this comment.
This review was created by an AI agent (OpenHands) on behalf of the repository maintainers.
Verdict: no material findings. The change is small, well-scoped, and correct.
What I verified against the workspace code:
- Ordering guarantee holds.
BaseConversation.compose_callbacksiterates callbacks in a plainforloop with notry/except, socomposed_list = [_default_callback] + callback_listmeans aValueErrorraised byEventLog.append(duplicate ID / missing parent) short-circuits the loop and the caller-supplied publish callback never runs. That directly delivers the "never announce an event that isn't durable" invariant. - Return-type change is safe. I searched the codebase: no production caller consumes
append_event's previousEventreturn value, and everyevents.append(...)call site ignores the return.EventLog.appendnow capturesself._lengthintoidxonce and returns it, which is the correct assigned sequence number (O(1), no extra work). RemoteEventsList.append's placeholder return is appropriately documented. It returnslen(self._cached_events) - 1only to satisfy the sharedEventsListBaseinterface; the PR's follow-up (step 3, session socket) targets the localEventLogpath, so the non-meaningful remote value isn't consumed anywhere.- Tests are meaningful and exercise real code paths (no mocks):
test_user_callback_runs_after_event_is_persistedandtest_persist_failure_blocks_downstream_callbackboth assert observable behavior that regresses if the order is reverted. I ran the two touched test files locally: 34 passed.
Scope check: this is event persistence/ordering plumbing, not agent decision logic, tool behavior, prompt templates, or condenser/planning code, so it is not in the eval-risk category.
Risk assessment: 🟢 LOW — backward-compatible return-type widening on a method whose prior return was unused; the only behavior change is the callback invocation order, which is the explicit point of the fix.
Review decision: COMMENT (no blocking issues). This is a clean, self-contained fix ready for a human maintainer to merge.
all-hands-bot
left a comment
There was a problem hiding this comment.
This review was created by an AI agent (OpenHands) on behalf of the repository maintainers.
Verdict: no material findings. The change is small, well-scoped, and correct.
This PR fixes #17106 by percent-encoding branch names when building provider branch URLs, and it correctly splits the encoding strategy by where each provider places the branch:
- Path providers (
github,forgejo,gitlab,bitbucket) use the newencodeBranchPathhelper, which encodes each/-separated segment while preserving the slashes. Sofeature/ui#123→feature/ui%23123, butrelease/1.0keeps its literal slash (nested refs are expressed as nested path segments). This is the right behavior. - Query providers (
bitbucket_data_center?at=refs/heads/…,azure_devops?version=GB…) useencodeURIComponenton the whole value, encoding/too, so an unencoded&,+, or#can't be misinterpreted as a query delimiter. Correct for a single query value.
I traced the callers (git-control-bar-branch-button.tsx, conversation-overview-git-section.tsx): constructBranchUrl is used only to build the href, so the displayed branch text is untouched and there's no UI regression.
Tests are real-logic and proportionate: 52 cases across all six providers plus a release/1.0 regression control, and the PR documents that the suite also fails against both plausible wrong fixes (blanket encodeURIComponent on path providers, and reusing encodeBranchPath on query providers). No mocks.
Scope check: this is pure URL-string construction, not agent decision logic, tool selection, prompts, or evaluation paths, so it's outside the eval-risk category.
Risk assessment: 🟢 LOW — a small, pure, well-tested change to link generation. No dependency, security, or contract changes. The only theoretical edge is encodeURIComponent throwing on a lone-surrogate branch name, which is not realistically reachable from a git ref and predates this change in spirit (unencoded interpolation already produced invalid URLs).
Automated review used the wrong decision (APPROVED instead of COMMENT) and is dismissed. Findings are reposted as a comment.
|
The correct review is posted. Note: an initial APPROVED review with unrelated stale content (from a shared temp file that leaked in) was also posted in error, but I cannot delete already-submitted reviews — only PENDING reviews can be deleted. The authoritative review for this PR is the second one (COMMENTED), which contains the correct branch-URL-encoding analysis. The correct review content is now live on the PR. GITHUB_REVIEW_POSTED This comment was posted by an AI agent (OpenHands). |
|
🚦 CI is currently failing on this PR's latest commit. Please fix the failing checks before OpenHands reviews it - this is re-checked automatically once you push a new commit. (A maintainer can also request This is an automated check - no AI was used to generate this comment. |
|
🚀 Released in v1.17.0. |
HUMAN:
I hit this with a branch named feature/ui#123. The chip showed the correct name, but the link opened to feature/ui. I ran the dev server, created that branch in a scratch repo, and watched the actual request in DevTools stop at feature/ui with a 404. After the change, the link keeps the %23, and release/1.0 still works.
AGENT:
Why
constructBranchUrlinterpolated the branch name into the provider URL without percent-encoding it.#and%are both legal in a git ref, and both change what the browser or the server resolves:feature/ui#123:#opens a URL fragment, which is not sent. The href reads.../tree/feature/ui#123while the request path is.../tree/feature/ui.100%2Fdone: the raw path decodes server-side to.../tree/100/done.Blanket-encoding would be wrong in the other direction, since
release/1.0would becomerelease%2F1.0on the path-based providers. The encoding has to follow where each provider puts the branch.Summary
github,gitlab,bitbucketandforgejoput the branch in the URL path, so a newencodeBranchPathhelper encodes each/-separated segment and leaves the separators alone.azure_devops(?version=GB) andbitbucket_data_center(?at=refs/heads/) put it in a query value, so the whole name is encoded,/included. An unencoded&there would otherwise start a new query parameter.constructBranchUrlcases across all six providers, coveringmain,feature/ui#123,feature&x,feature+x,100%done,100%2Fdoneandrelease/1.0.Only the href changes. The displayed branch text is untouched.
Issue Number
Fixes #17106
How to Test
npm ci && npx vitest run __tests__/utils/utils.test.tsgives 52 passing: the 42 newconstructBranchUrlcases plus 10 pre-existinggetStatusTexttests in the same file.The tests fail without the fix, and they also fail against both plausible wrong fixes, which is the point of splitting path from query. I checked each by swapping the implementation and re-running:
main(unencoded)encodeURIComponenton the four path providersrelease/1.0becomesrelease%2F1.0on every path providerencodeBranchPathon the two query providers?version=GBrelease/1.0leaves the slash rawEnd to end, against the running app rather than the unit tests:
npm ci && npm run dev(needsuvon PATH per README option 3).Create the scratch repository:
The commit is required. On an unborn branch that last command prints
HEADand exits 128,use-local-git-info.ts:64mapsHEADtonull, andgit-control-bar.tsx:238then renders no branch chip at all.Open a conversation on that workspace and follow the branch chip's link.
Also ran on the full tree:
__tests__/utilsis 69 files / 549 tests passing, the four branch-link component suites are 30 tests passing, andtsc,eslintandprettier --checkare clean on both changed files.Video/Screenshots
Before. The control bar shows
feature/ui#123, but DevTools records the document request as.../tree/feature/uiwith a 404:After. The whole branch name now reaches GitHub as
feature/ui%23123:Both destination pages are errors because this scratch branch does not exist on the public remote. The difference the screenshots establish is the requested path:
feature/uibefore,feature/ui%23123after. On a repository where the branch exists, the first would resolve to a different branch and the second to the right one.Regression control. With the branch switched to
release/1.0, the link keeps its unencoded slash:Type
Notes
Unrelated to this change, the JSDoc example above
constructBranchUrlshows abitbucketcall returning a/projects/.../browse?at=URL, which is thebitbucket_data_centershape. I left it alone rather than widen this PR.