authorgravatar for snorteman@gmail.comScott Norteman <snorteman@gmail.com> 2026-10-10 07:34:21-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-10 06:34:21-05:00
log33c1e36fd097fe4f8e4498db653ee97b17184791
tree3d7a8b8759b46b3a7f4e7029b0a989b748fbe8ce
parent91474e56f66888c5ab3856a782a72613e758c401
signature Signed by PGP key B5690EEEBB952194

fix(server): a bare repository name resolves to the signed-in account again (#17379)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

2 files changed, 48 insertions(+), 1 deletions(-)

packages/source-control-github/src/server/GitHubSourceControlProvider.test.ts+41
......@@ -513,6 +513,47 @@ describe("GitHubSourceControlProvider writes", () => {
513513 assert.deepStrictEqual(requests, ["GET user", "POST orgs/acme/repos"]);
514514 }).pipe(Effect.provide(layer));
515515 });
516
517 it.effect("looks up a bare repository name under the signed-in account on that host", () => {
518 const requests: Array<string> = [];
519 return Effect.gen(function* () {
520 const gh = yield* makeProvider({
521 rest: (input) =>
522 Effect.sync(() => {
523 requests.push(`${input.host} ${input.method ?? "GET"} ${input.path}`);
524 return input.path === "user"
525 ? restResponse({ login: "me" })
526 : restResponse({
527 full_name: "me/notes",
528 html_url: `https://${input.host}/me/notes`,
529 ssh_url: `git@${input.host}:me/notes.git`,
530 });
531 }),
532 });
533
534 const urls = yield* gh.getRepositoryCloneUrls({
535 cwd: "/repo",
536 context: githubContext("code.example.test"),
537 repository: " notes.git ",
538 });
539
540 assert.deepStrictEqual(urls, {
541 nameWithOwner: "me/notes",
542 url: "https://code.example.test/me/notes",
543 sshUrl: "git@code.example.test:me/notes.git",
544 });
545 assert.deepStrictEqual(requests, [
546 "code.example.test GET user",
547 "code.example.test GET repos/me/notes",
548 ]);
549 // Malformed names are still refused before any request.
550 for (const repository of ["me/", "/notes", "a/b/c/d", "", "two words"]) {
551 const error = yield* Effect.flip(gh.getRepositoryCloneUrls({ cwd: "/repo", repository }));
552 assert.include(error.message, "Repositories are named owner/name.");
553 }
554 assert.strictEqual(requests.length, 2);
555 });
556 });
516557});
517558
518559describe("GitHubSourceControlProvider.checkoutChangeRequest", () => {
packages/source-control-github/src/server/GitHubSourceControlProvider.ts+7-1
......@@ -1025,7 +1025,13 @@ export const make = Effect.gen(function* () {
10251025 Effect.map((locator) => locator.host),
10261026 Effect.orElseSucceed(() => environment.GH_HOST ?? "github.com"),
10271027 )).toLowerCase();
1028 const locator = parseGitHubRepositorySelector(input.repository, fallbackHost);
1028 // A bare name is the signed-in account's repository, the way `gh repo view` reads it.
1029 const bareName = input.repository.trim().replace(/\.git$/i, "");
1030 const locator =
1031 parseGitHubRepositorySelector(input.repository, fallbackHost) ??
1032 (/^[^/\s]+$/.test(bareName)
1033 ? { host: fallbackHost, owner: yield* readViewerLogin(fallbackHost), name: bareName }
1034 : null);
10291035 if (locator === null) return yield* failure("Repositories are named owner/name.");
10301036 return repositoryCloneUrls(yield* readRepository(locator));
10311037 }).pipe(