From 91ffdbc8efa16e2779fc26e940c5f8de4e831dfa Mon Sep 17 00:00:00 2001 From: Harsh Kashyap Date: Tue, 18 Aug 2026 18:31:43 +0530 Subject: [PATCH] fix(backend): decode percent-encoded characters in generic git URL config `compileGenericGitHostConfig_url` in `packages/backend/src/repoCompileUtils.ts` derived the repo `name` / `displayName` / zoekt metadata from `remoteUrl.pathname` without decoding percent-encoded characters. The file-origin counterpart (`compileGenericGitHostConfig_file`) already calls `decodeURIComponent` on the origin URL pathname, so the same remote configured as a direct URL vs. discovered from a local repository origin ended up with two different identifiers. Concretely, a URL like `https://github.com/test/Project%20Name%20With%20Spaces.git` and a file config pointing at the same remote would derive: - direct URL: `github.com/test/Project%20Name%20With%20Spaces` - file origin: `github.com/test/Project Name With Spaces` That breaks search-index metadata (the value is written into `zoekt.name` / `zoekt.display-name` / `name` / `displayName`) and makes the same repository look like two different repos depending on how it was configured. The fix mirrors the existing file-origin pattern: pull `decodeURIComponent` on `remoteUrl.pathname` before stripping the `.git` suffix and joining with the host. One production-line change. Add a vitest case in `repoCompileUtils.test.ts` that asserts the direct URL config produces a decoded `name` and `displayName`, plus the matching zoekt metadata fields. This mirrors the existing test for the file-origin path (`'should decode URL-encoded characters in origin url pathname'`). Out of scope: malformed percent escapes like `Project%GGName`. The file-origin path also throws on these via the raw `decodeURIComponent` call, so this PR preserves the same behavior. A separate change could wrap the decode with a malformed-escape fallback if the maintainer wants to relax that. Validation: - `yarn workspace @sourcebot/backend test --run src/repoCompileUtils.test.ts` -> 12 passed (11 pre-existing + 1 new) - `yarn workspace @sourcebot/backend test --run` -> 262 passed (261 baseline + 1 new), 15 pre-existing failures unchanged - `yarn workspace @sourcebot/backend exec tsc --noEmit` -> 0 new errors introduced (all 11 pre-existing tsc errors are in the new BullMQ JobManager files added in #1427 and are unrelated to this PR) Fixes #1384 --- packages/backend/src/repoCompileUtils.test.ts | 29 ++++++++++++++++++- packages/backend/src/repoCompileUtils.ts | 8 ++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/repoCompileUtils.test.ts b/packages/backend/src/repoCompileUtils.test.ts index 344079f87..9180e4228 100644 --- a/packages/backend/src/repoCompileUtils.test.ts +++ b/packages/backend/src/repoCompileUtils.test.ts @@ -255,8 +255,35 @@ describe('compileGenericGitHostConfig_url', () => { expect(result.repoData).toHaveLength(1); expect(result.repoData[0].name).toBe('github.com/test/repo'); - + const metadata = result.repoData[0].metadata as { gitConfig?: Record }; expect(metadata.gitConfig!['zoekt.name']).toBe('github.com/test/repo'); }); + + test('should decode URL-encoded characters in the direct url pathname', async () => { + // Regression for #1384: the file-origin path already calls + // decodeURIComponent on the origin URL pathname, but the direct-URL + // path did not. The same remote configured two different ways + // produced different `name`/`displayName` values, which broke + // search-index metadata and gave the same repo inconsistent + // identifiers depending on how it was configured. + mockedIsUrlAValidGitRepo.mockResolvedValue(true); + + const config = { + type: 'git' as const, + url: 'https://github.com/test/Project%20Name%20With%20Spaces.git', + }; + + const result = await compileGenericGitHostConfig_url(config, 1); + + expect(result.repoData).toHaveLength(1); + expect(result.warnings).toHaveLength(0); + // The repo name should have decoded spaces, not %20 + expect(result.repoData[0].name).toBe('github.com/test/Project Name With Spaces'); + expect(result.repoData[0].displayName).toBe('github.com/test/Project Name With Spaces'); + + const metadata = result.repoData[0].metadata as { gitConfig?: Record }; + expect(metadata.gitConfig!['zoekt.name']).toBe('github.com/test/Project Name With Spaces'); + expect(metadata.gitConfig!['zoekt.display-name']).toBe('github.com/test/Project Name With Spaces'); + }); }); diff --git a/packages/backend/src/repoCompileUtils.ts b/packages/backend/src/repoCompileUtils.ts index 1e65565f1..f43a66969 100644 --- a/packages/backend/src/repoCompileUtils.ts +++ b/packages/backend/src/repoCompileUtils.ts @@ -726,7 +726,13 @@ export const compileGenericGitHostConfig_url = async ( // @note: matches the naming here: // https://github.com/sourcebot-dev/zoekt/blob/main/gitindex/index.go#L293 - const repoName = path.join(remoteUrl.host, remoteUrl.pathname.replace(/\.git$/, '')); + // Decode URL-encoded characters (e.g., %20 -> space) so that a direct URL + // config derives the same repo name as a file-origin config whose origin + // URL gets decoded by the same call below. Without this, a URL like + // `https://github.com/test/Project%20Name.git` and a file config pointing + // at the same remote end up with different `name`/`displayName` values. + const decodedPathname = decodeURIComponent(remoteUrl.pathname); + const repoName = path.join(remoteUrl.host, decodedPathname.replace(/\.git$/, '')); const repo: RepoData = { external_codeHostType: 'genericGitHost',