diff --git a/packages/source-control-github/src/server/GitHubSourceControlProvider.test.ts b/packages/source-control-github/src/server/GitHubSourceControlProvider.test.ts index 3f17db9bb302957846dcbd8dfa4e2a8b83085940..7a902965b7aad38f911500db61bb4deff5506773 100644 --- a/packages/source-control-github/src/server/GitHubSourceControlProvider.test.ts +++ b/packages/source-control-github/src/server/GitHubSourceControlProvider.test.ts @@ -513,6 +513,47 @@ describe("GitHubSourceControlProvider writes", () => { assert.deepStrictEqual(requests, ["GET user", "POST orgs/acme/repos"]); }).pipe(Effect.provide(layer)); }); + + it.effect("looks up a bare repository name under the signed-in account on that host", () => { + const requests: Array = []; + return Effect.gen(function* () { + const gh = yield* makeProvider({ + rest: (input) => + Effect.sync(() => { + requests.push(`${input.host} ${input.method ?? "GET"} ${input.path}`); + return input.path === "user" + ? restResponse({ login: "me" }) + : restResponse({ + full_name: "me/notes", + html_url: `https://${input.host}/me/notes`, + ssh_url: `git@${input.host}:me/notes.git`, + }); + }), + }); + + const urls = yield* gh.getRepositoryCloneUrls({ + cwd: "/repo", + context: githubContext("code.example.test"), + repository: " notes.git ", + }); + + assert.deepStrictEqual(urls, { + nameWithOwner: "me/notes", + url: "https://code.example.test/me/notes", + sshUrl: "git@code.example.test:me/notes.git", + }); + assert.deepStrictEqual(requests, [ + "code.example.test GET user", + "code.example.test GET repos/me/notes", + ]); + // Malformed names are still refused before any request. + for (const repository of ["me/", "/notes", "a/b/c/d", "", "two words"]) { + const error = yield* Effect.flip(gh.getRepositoryCloneUrls({ cwd: "/repo", repository })); + assert.include(error.message, "Repositories are named owner/name."); + } + assert.strictEqual(requests.length, 2); + }); + }); }); describe("GitHubSourceControlProvider.checkoutChangeRequest", () => { diff --git a/packages/source-control-github/src/server/GitHubSourceControlProvider.ts b/packages/source-control-github/src/server/GitHubSourceControlProvider.ts index 0e54af539d9fb4f7aff4ed061c11bc18d95a3122..6cf169a1b7546843c4dcc56d05d001371aa177ec 100644 --- a/packages/source-control-github/src/server/GitHubSourceControlProvider.ts +++ b/packages/source-control-github/src/server/GitHubSourceControlProvider.ts @@ -1025,7 +1025,13 @@ export const make = Effect.gen(function* () { Effect.map((locator) => locator.host), Effect.orElseSucceed(() => environment.GH_HOST ?? "github.com"), )).toLowerCase(); - const locator = parseGitHubRepositorySelector(input.repository, fallbackHost); + // A bare name is the signed-in account's repository, the way `gh repo view` reads it. + const bareName = input.repository.trim().replace(/\.git$/i, ""); + const locator = + parseGitHubRepositorySelector(input.repository, fallbackHost) ?? + (/^[^/\s]+$/.test(bareName) + ? { host: fallbackHost, owner: yield* readViewerLogin(fallbackHost), name: bareName } + : null); if (locator === null) return yield* failure("Repositories are named owner/name."); return repositoryCloneUrls(yield* readRepository(locator)); }).pipe(