Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,152 @@ describe("GitHubSourceControlProvider repository resolution", () => {
assert.deepStrictEqual(paths, ["github.com repos/acme/web"]);
}).pipe(Effect.provide(layer));
});

it.effect("prefers a marked GitHub subdomain origin over upstream", () => {
const targets: string[] = [];
const { layer } = harness({
remotes: remotesOutput(
["origin", "git@work.github.com:me/web.git"],
["upstream", "git@github.com:acme/web.git"],
),
resolved: "remote.origin.gh-resolved base",
api: {
rest: (input) =>
Effect.sync(() => {
targets.push(`${input.host} ${input.path}`);
return restResponse({
full_name: "me/web",
html_url: "https://github.com/me/web",
ssh_url: "git@github.com:me/web.git",
default_branch: "main",
});
}),
},
});
return Effect.gen(function* () {
const provider = yield* GitHubSourceControlProvider.make;
assert.strictEqual(yield* provider.getDefaultBranch({ cwd: "/repo" }), "main");
assert.deepStrictEqual(targets, ["github.com repos/me/web"]);
}).pipe(Effect.provide(layer), Effect.provideService(HostProcess.Environment, {}));
});

it.effect(
"reads GitHub subdomain remotes through github.com and preserves enterprise ports",
() =>
Effect.gen(function* () {
for (const [remote, host, apiHost] of [
["ssh://git@ssh.github.com:443/acme/web.git", "ssh.github.com", "github.com"],
["git@work.github.com:acme/web.git", "work.github.com", "github.com"],
[
"https://github.example.com:8443/acme/web.git",
"github.example.com:8443",
"github.example.com:8443",
],
] as const) {
const requests: string[] = [];
const { layer } = harness({
remotes: remotesOutput(["origin", remote]),
api: {
rest: (input) =>
Effect.sync(() => {
requests.push(`${input.host} ${input.path}`);
return restResponse({
full_name: "acme/web",
html_url: `https://${apiHost}/acme/web`,
ssh_url: `git@${apiHost}:acme/web.git`,
default_branch: "trunk",
});
}),
},
});
yield* Effect.gen(function* () {
const gh = yield* GitHubSourceControlProvider.make;
assert.strictEqual(yield* gh.getDefaultBranch({ cwd: "/repo" }), "trunk");
assert.strictEqual(
yield* gh.getDefaultBranch({
cwd: "/repo",
context: { ...githubContext(host), remoteUrl: remote },
}),
"trunk",
);
assert.deepStrictEqual(requests, [
`${apiHost} repos/acme/web`,
`${apiHost} repos/acme/web`,
]);
}).pipe(Effect.provide(layer), Effect.provideService(HostProcess.Environment, {}));
}
}),
);

it.effect("reads a GH_REPO owner/name behind a github.com subdomain through github.com", () => {
const hosts: string[] = [];
const { layer } = harness({
remotes: remotesOutput(["origin", "git@work.github.com:acme/web.git"]),
api: {
rest: (input) =>
Effect.sync(() => {
hosts.push(`${input.host} ${input.path}`);
return restResponse({
full_name: "chosen/repo",
html_url: "https://github.com/chosen/repo",
ssh_url: "git@github.com:chosen/repo.git",
default_branch: "main",
});
}),
},
});
return Effect.gen(function* () {
const gh = yield* GitHubSourceControlProvider.make;
assert.strictEqual(
yield* gh.getDefaultBranch({ cwd: "/repo", context: githubContext("work.github.com") }),
"main",
);
assert.strictEqual(yield* gh.getDefaultBranch({ cwd: "/repo" }), "main");
assert.deepStrictEqual(hosts, [
"github.com repos/chosen/repo",
"github.com repos/chosen/repo",
]);
}).pipe(
Effect.provide(layer),
Effect.provideService(HostProcess.Environment, {
GH_REPO: "chosen/repo",
GH_HOST: "ssh.github.com",
}),
);
});

it.effect("uses github.com for a GH_HOST alias when cloning without a checkout remote", () => {
const requests: string[] = [];
const { layer } = harness({
remotes: "",
api: {
rest: (input) =>
Effect.sync(() => {
requests.push(`${input.host} ${input.path}`);
return restResponse({
full_name: "chosen/repo",
html_url: "https://github.com/chosen/repo",
ssh_url: "git@github.com:chosen/repo.git",
});
}),
},
});
return Effect.gen(function* () {
const gh = yield* GitHubSourceControlProvider.make;
assert.deepStrictEqual(
yield* gh.getRepositoryCloneUrls({ cwd: "/repo", repository: "chosen/repo" }),
{
nameWithOwner: "chosen/repo",
url: "https://github.com/chosen/repo",
sshUrl: "git@github.com:chosen/repo.git",
},
);
assert.deepStrictEqual(requests, ["github.com repos/chosen/repo"]);
}).pipe(
Effect.provide(layer),
Effect.provideService(HostProcess.Environment, { GH_HOST: "work.github.com" }),
);
});
});

describe("GitHubSourceControlProvider change request lookups by head", () => {
Expand Down Expand Up @@ -458,6 +604,50 @@ describe("GitHubSourceControlProvider writes", () => {
}).pipe(Effect.provide(layer));
});

it.effect("creates repositories on the API host named by GH_HOST", () =>
Effect.gen(function* () {
for (const [host, apiHost] of [
["work.github.com", "github.com"],
["github.example.com:8443", "github.example.com:8443"],
]) {
const requests: string[] = [];
const { layer } = harness({
remotes: "",
api: {
rest: (input) =>
Effect.sync(() => {
requests.push(`${input.host} ${input.method ?? "GET"} ${input.path}`);
return input.path === "user"
? restResponse({ login: "me" })
: restResponse({
full_name: "acme/new",
html_url: `https://${apiHost}/acme/new`,
ssh_url: `git@${apiHost}:acme/new.git`,
});
}),
},
});
yield* Effect.gen(function* () {
const gh = yield* GitHubSourceControlProvider.make;
yield* gh.createRepository({
cwd: "/repo",
repository: "acme/new",
visibility: "private",
});
yield* gh.createRepository({ cwd: "/repo", repository: "new", visibility: "private" });
assert.deepStrictEqual(requests, [
`${apiHost} GET user`,
`${apiHost} POST orgs/acme/repos`,
`${apiHost} POST user/repos`,
]);
}).pipe(
Effect.provide(layer),
Effect.provideService(HostProcess.Environment, { GH_HOST: host }),
);
}
}),
);

it.effect("looks up a bare repository name under the signed-in account on that host", () => {
const requests: Array<string> = [];
return Effect.gen(function* () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
type NormalizedGitHubPullRequestRecord,
} from "./gitHubPullRequests.ts";
import {
apiHostForHint,
gitHubApiHostForRemote,
parseFetchRemotes,
parseGitHubRepositorySelector,
Expand Down Expand Up @@ -446,6 +447,7 @@ export const make = Effect.gen(function* () {
const sourceControlHost = yield* SourceControlHost.SourceControlHost;
const process = sourceControlHost.process;
const environment = yield* HostProcess.Environment;
const environmentHost = apiHostForHint(environment.GH_HOST?.trim() || "github.com");
const git = sourceControlHost.git;
const fileSystem = yield* FileSystem.FileSystem;

Expand All @@ -467,7 +469,7 @@ export const make = Effect.gen(function* () {
}) {
const envRepository = environment.GH_REPO?.trim();
const hostHint = input.host ?? contextHost(input.context);
const defaultHost = (hostHint ?? environment.GH_HOST ?? "github.com").toLowerCase();
const defaultHost = apiHostForHint(hostHint ?? environmentHost);
if (envRepository) {
const locator = parseGitHubRepositorySelector(envRepository, defaultHost);
if (locator !== null) return locator;
Expand Down Expand Up @@ -995,7 +997,7 @@ export const make = Effect.gen(function* () {
function* (input) {
const fallbackHost = (yield* resolveRepository(input).pipe(
Effect.map((locator) => locator.host),
Effect.orElseSucceed(() => environment.GH_HOST ?? "github.com"),
Effect.orElseSucceed(() => environmentHost),
)).toLowerCase();
const bareName = input.repository.trim().replace(/\.git$/i, "");
const locator =
Expand All @@ -1021,14 +1023,11 @@ export const make = Effect.gen(function* () {
),
createRepository: Effect.fn("GitHubSourceControlProvider.createRepository")(
function* (input) {
const locator = parseGitHubRepositorySelector(
input.repository,
(environment.GH_HOST ?? "github.com").toLowerCase(),
);
const locator = parseGitHubRepositorySelector(input.repository, environmentHost);
const viewer = locator === null ? null : yield* readViewerLogin(locator.host);
const owner = locator?.owner ?? viewer;
const name = locator?.name ?? input.repository.trim();
const host = locator?.host ?? (environment.GH_HOST?.trim().toLowerCase() || "github.com");
const host = locator?.host ?? environmentHost;
const isViewer = viewer !== null && owner?.toLowerCase() === viewer.toLowerCase();
const response = yield* rest(
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { normalizeGitRemoteUrl } from "@t3tools/shared/git";
import { isSshRemoteUrl } from "@t3tools/shared/sourceControl";
import { parseGitRemoteHost } from "@t3tools/source-control-core/shared/definition";
import { definition } from "../shared/definition.ts";
import { definition, gitHubApiHostOf } from "../shared/definition.ts";
import { isGitHubHost } from "./GitHubCredentials.ts";

export interface GitHubRepositoryLocator {
Expand All @@ -21,7 +21,10 @@ export function parseFetchRemotes(
.map((match) => ({ name: match[1]!, url: match[2]! }));
}

/** gh priority: marked default, upstream/github/origin, sole remote. Null if ambiguous or SSH-aliased. */
/**
* gh priority: marked default, upstream/github/origin, sole remote. Null if ambiguous,
* SSH-aliased, or on another API host.
*/
export function selectGitHubBaseRepository(input: {
readonly remotes: string;
readonly resolved: string;
Expand All @@ -31,7 +34,8 @@ export function selectGitHubBaseRepository(input: {
const repositories = new Map<string, { readonly owner: string; readonly name: string }>();
for (const remote of parseFetchRemotes(input.remotes)) {
const [remoteHost, owner, name, ...rest] = normalizeGitRemoteUrl(remote.url).split("/");
if (remoteHost !== host || !owner || !name || rest.length > 0) return null;
if (!remoteHost || gitHubApiHostOf(remoteHost) !== host || !owner || !name || rest.length > 0)
return null;
repositories.set(remote.name, { owner, name });
}
const marks = input.resolved
Expand Down Expand Up @@ -146,18 +150,21 @@ export function parsePullRequestReference(
return { kind: "branch", headSelector: trimmed };
}

/** SSH aliases without a domain cannot identify an API host; default to github.com. */
/** Dotless GitHub SSH aliases and github.com subdomains use github.com for API requests. */
export function gitHubApiHostForRemote(remoteUrl: string): string | null {
const remote = parseGitRemoteHost(remoteUrl);
if (remote === null) return null;
const host = new URL(`https://${remote.host}`).host.toLowerCase();
if (gitHubApiHostOf(host) === "github.com") return "github.com";
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
if (isSshRemoteUrl(remoteUrl) && !host.includes(".")) {
return host.includes("github") ? "github.com" : null;
}
return definition.recognizeRemote(remote) === null ? null : host;
}

function apiHostForHint(host: string): string {
export function apiHostForHint(host: string): string {
const normalized = host.toLowerCase();
return !normalized.includes(".") && normalized.includes("github") ? "github.com" : normalized;
return !normalized.includes(".") && normalized.includes("github")
? "github.com"
: gitHubApiHostOf(normalized);
}
26 changes: 26 additions & 0 deletions packages/source-control-github/src/shared/definition.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ChangeRequestProjectIdentity } from "@t3tools/source-control-core/shared/definition";
import { recognizeRemoteWith } from "@t3tools/source-control-core/shared/definition";
import { describe, expect, it } from "vite-plus/test";

Expand Down Expand Up @@ -122,6 +123,31 @@ describe("GitHub remotes", () => {
});
});

describe("GitHub change request hosts", () => {
const project = (host: string): ChangeRequestProjectIdentity => ({
canonicalKey: `${host}/acme/web`,
displayName: "acme/web",
locator: { source: "git-remote", remoteName: "origin", remoteUrl: `git@${host}:acme/web.git` },
});

it("names GitHub subdomain checkouts by their API host and preserves enterprise ports", () => {
for (const [host, expected] of [
["ssh.github.com", "github.com"],
["work.github.com:8443", "github.com"],
["github.example.com:8443", "github.example.com:8443"],
] as const) {
expect(definition.changeRequestHost(project(host))).toBe(expected);
expect(definition.checkoutChangeRequestHost(project(host))).toBe(expected);
}
});

it("matches github.com pull requests to a checkout behind a github.com subdomain", () => {
const link = { host: "github.com", repository: "acme/web", number: 7 };
expect(definition.isChangeRequestInRepository(project("work.github.com"), link)).toBe(true);
expect(definition.canReadChangeRequestOnHost(project("work.github.com"), link)).toBe(true);
});
});

describe("GitHub media URLs", () => {
const media = (url: string) => definition.mediaUrl(new URL(url));

Expand Down
18 changes: 15 additions & 3 deletions packages/source-control-github/src/shared/definition.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { makeProviderSettingsSchema, SourceControlProviderKind } from "@t3tools/contracts";
import {
changeRequestUrlBelow,
type ChangeRequestHostIdentity,
defineSourceControl,
hostCredentialSettingsFields,
isChangeRequestInProjectRepository,
Expand Down Expand Up @@ -33,6 +34,16 @@ function isGitHubHost(hostname: string): boolean {
);
}

/** SSH remotes under github.com share github.com's API host and credentials. */
export function gitHubApiHostOf(host: string): string {
const normalized = host.toLowerCase();
return normalized.replace(/:\d+$/, "").endsWith(".github.com") ? "github.com" : normalized;
}

function changeRequestHost(identity: ChangeRequestHostIdentity): string {
return gitHubApiHostOf(projectChangeRequestHost(identity, KIND));
}

function isRepositoryPath(repository: string): boolean {
const segments = repository.split("/");
return (
Expand Down Expand Up @@ -129,7 +140,8 @@ export const definition = defineSourceControl({
reviewSummaryRequired: () => false,
checkoutCommandArgument: (input) => CHECKOUT_COMMAND.exec(input)?.[1]?.trim() ?? null,
isChangeRequestReference: (url) => CHANGE_REQUEST_REFERENCE.test(url),
checkoutChangeRequestHost: (identity) => projectChangeRequestHost(identity, KIND),
checkoutChangeRequestHost: changeRequestHost,
changeRequestHost,
changeRequestHeadRef: (number) => `refs/pull/${number}/head`,
// `*.ghe.com` is GitHub Enterprise Cloud with data residency, one subdomain per enterprise.
recognizeRemote: ({ host, hostname }) =>
Expand All @@ -143,9 +155,9 @@ export const definition = defineSourceControl({
}
: null,
isChangeRequestInRepository: (identity, link) =>
isChangeRequestInProjectRepository(projectChangeRequestHost(identity, KIND), identity, link),
isChangeRequestInProjectRepository(changeRequestHost(identity), identity, link),
canReadChangeRequestOnHost: (identity, link) =>
isChangeRequestOnProjectHost(projectChangeRequestHost(identity, KIND), identity, link),
isChangeRequestOnProjectHost(changeRequestHost(identity), identity, link),
mediaUrl: gitHubMediaUrl,
settings,
});
Loading