IW-558: Percent-encode the GitLab repository slug as one api path segment #563

Merged
mph merged 15 commits from IW-558 into main 2026-09-15 09:33:16 +00:00
Member

Summary

GitLabClient.encodedRepo escaped one character, /. GitLab's REST API needs
the whole namespace/project slug as a single percent-encoded path segment, so
every other URL-significant character passed through raw. A slug carrying ?,
#, &, % or a space therefore ended the path or changed its meaning, and
the request addressed a project the caller never named. No exception, no error
message — a wrong request, which is what kept it unnoticed.

encodedRepo now percent-encodes the slug as one path segment. One central
change fixes all four glab api path builders; no call site was edited.

Defect fixed

Phase 1 — encodedRepo leaves URL-significant characters raw in four glab api paths (#561, merged)

The four affected builders:

  • buildPipelineJobsCommand — projects/…/pipelines/$id/jobs
  • buildMergeRequestCommand — projects/…/merge_requests/$n
  • buildJobTraceCommand — projects/…/jobs/$id/trace
  • buildMergeRequestForBranchCommand — the worst of the four, because the slug
    precedes a real query string, so a slug-borne & or ? merged into
    parameters that already existed

The fix uses URLEncoder.encode(repository, "UTF-8").replace("+", "%20"). The
.replace step is required: URLEncoder is form-encoding, so a space would
otherwise arrive as +, which means a literal plus inside a path.

Scope held

encodedQueryValue and the four --repo builders are untouched. Those hand the
repository to glab as a raw process argument, never into a path this code
builds, so encoding them would change what glab receives for no reason this
issue names.

Testing

Two regression tests in core/test/GitLabCiTest.scala, both proved RED before
being accepted:

  • "a repository slug cannot end the api path or add a parameter" drives the
    slug group/pro?a=1&b#x%z w through buildMergeRequestCommand.
  • "a repository slug cannot add a query parameter ahead of source_branch"
    drives the same slug through buildMergeRequestForBranchCommand and pins the
    encoded segment against the ?source_branch= that follows it.

No-regression proof: the existing assertions pinning group%2Fsub%2Fproject
pass unchanged. Full percent-encoding is byte-identical to /-only escaping for
a legal slug, so the fix carries no risk for a correctly configured project.

  • ./iw ./test unit → exit 0 (core.test 186/186, dashboard.test 241/241)
  • ./iw ./test lint → exit 0

Code review: 1 iteration, 0 critical, 0 warnings.

Release notes

project-management/issues/IW-558/release-notes.md (Czech, user-facing).

Follow-up

Filed as #562 — GitRemote.extractRepositoryPath and the config parser
disagree on which characters a repository slug may hold.

Closes #558

🤖 Generated with Claude Code

## Summary `GitLabClient.encodedRepo` escaped one character, `/`. GitLab's REST API needs the whole `namespace/project` slug as a single percent-encoded path segment, so every other URL-significant character passed through raw. A slug carrying `?`, `#`, `&`, `%` or a space therefore ended the path or changed its meaning, and the request addressed a project the caller never named. No exception, no error message — a wrong request, which is what kept it unnoticed. `encodedRepo` now percent-encodes the slug as one path segment. One central change fixes all four `glab api` path builders; no call site was edited. ## Defect fixed **Phase 1 — `encodedRepo` leaves URL-significant characters raw in four `glab api` paths** (#561, merged) The four affected builders: - `buildPipelineJobsCommand` — `projects/…/pipelines/$id/jobs` - `buildMergeRequestCommand` — `projects/…/merge_requests/$n` - `buildJobTraceCommand` — `projects/…/jobs/$id/trace` - `buildMergeRequestForBranchCommand` — the worst of the four, because the slug precedes a real query string, so a slug-borne `&` or `?` merged into parameters that already existed The fix uses `URLEncoder.encode(repository, "UTF-8").replace("+", "%20")`. The `.replace` step is required: `URLEncoder` is form-encoding, so a space would otherwise arrive as `+`, which means a literal plus inside a path. ## Scope held `encodedQueryValue` and the four `--repo` builders are untouched. Those hand the repository to glab as a raw process argument, never into a path this code builds, so encoding them would change what glab receives for no reason this issue names. ## Testing Two regression tests in `core/test/GitLabCiTest.scala`, both proved RED before being accepted: - *"a repository slug cannot end the api path or add a parameter"* drives the slug `group/pro?a=1&b#x%z w` through `buildMergeRequestCommand`. - *"a repository slug cannot add a query parameter ahead of source_branch"* drives the same slug through `buildMergeRequestForBranchCommand` and pins the encoded segment against the `?source_branch=` that follows it. No-regression proof: the existing assertions pinning `group%2Fsub%2Fproject` pass unchanged. Full percent-encoding is byte-identical to `/`-only escaping for a legal slug, so the fix carries no risk for a correctly configured project. - `./iw ./test unit` → exit 0 (core.test 186/186, dashboard.test 241/241) - `./iw ./test lint` → exit 0 Code review: 1 iteration, 0 critical, 0 warnings. ## Release notes `project-management/issues/IW-558/release-notes.md` (Czech, user-facing). ## Follow-up Filed as #562 — `GitRemote.extractRepositoryPath` and the config parser disagree on which characters a repository slug may hold. Closes #558 🤖 Generated with [Claude Code](https://claude.com/claude-code)
GitLabClient.encodedRepo escapes only /, so a repo slug with ? # & or %
corrupts every glab api path

Artifacts:
- analysis.md

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011M2HjEYoJpcfD43KYP6zxt
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011M2HjEYoJpcfD43KYP6zxt
GitLabClient.encodedRepo escapes only /, so a repo slug with ? # & or %
corrupts every glab api path

Artifacts:
- tasks.md
- review-state.json (tasks_ready)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011M2HjEYoJpcfD43KYP6zxt
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011M2HjEYoJpcfD43KYP6zxt
Issue: IW-558
Phase: 1 - encodedRepo leaves URL-significant characters raw in four glab api paths

This commit marks the checkpoint for phase 1 context generation.
Use dx-fix IW-558 to return to this point.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
encodedRepo leaves URL-significant characters raw in four glab api paths

Artifacts:
- phase-01-tasks.md

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The FOLLOW-UPS.md line about extractRepositoryPath vs the config parser
disagreeing on valid slug characters is now tracker issue #562.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs(IW-558): add Czech release notes
Some checks failed
CI / changes (pull_request) Successful in 2s
CI / contract (pull_request) Has been skipped
CI / format (pull_request) Has been cancelled
CI / lint (pull_request) Has been cancelled
CI / test (pull_request) Has been cancelled
CI / dashboard-build (pull_request) Has been cancelled
CI / compile (pull_request) Has been cancelled
78445848a6
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
chore(IW-558): update review-state to all_complete
All checks were successful
CI / changes (pull_request) Successful in 2s
CI / contract (pull_request) Has been skipped
CI / compile (pull_request) Successful in 1m16s
CI / format (pull_request) Successful in 7s
CI / lint (pull_request) Successful in 8s
CI / dashboard-build (pull_request) Successful in 59s
CI / test (pull_request) Successful in 14m44s
224ed1aead
mph merged commit 3ff0e4119c into main 2026-09-15 09:33:16 +00:00
Sign in to join this conversation.
No description provided.