Skip to content
Open
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
125 changes: 125 additions & 0 deletions apps/server/src/sourceControl/GitHubSourceControlProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,131 @@ it("reports an update hint instead of unauthenticated when gh predates --json",
);
});

const authStatusJson = (
accounts: ReadonlyArray<{
readonly host: string;
readonly login: string;
readonly state?: string;
readonly active?: boolean;
}>,
): string =>
JSON.stringify({
hosts: Object.fromEntries(
accounts.map((account) => [
account.host,
[
{
state: account.state ?? "success",
active: account.active ?? false,
host: account.host,
login: account.login,
tokenSource: "keyring",
gitProtocol: "https",
},
],
]),
),
});

const refineUnknownRemote = (input: { readonly host: string; readonly stdout: string }) =>
GitHubSourceControlProvider.discovery.refineUnknownRemote?.({
cwd: "/repo",
context: {
provider: {
kind: "unknown",
name: input.host,
baseUrl: `https://${input.host}`,
},
remoteName: "origin",
remoteUrl: `https://${input.host}/org/repo.git`,
},
auth: processResult(input.stdout),
});

it("refines unknown remotes that gh is signed in to, whether or not the account is active", () => {
assert.deepStrictEqual(
refineUnknownRemote({
host: "git.example.edu",
stdout: authStatusJson([
{ host: "github.com", login: "active-user", active: true },
{ host: "git.example.edu", login: "enterprise-user" },
]),
}),
{
kind: "github",
name: "GitHub Self-Hosted",
baseUrl: "https://git.example.edu",
},
);
});

it("leaves unknown remotes alone when gh has no working account for the host", () => {
assert.strictEqual(
refineUnknownRemote({
host: "git.example.edu",
stdout: authStatusJson([
{ host: "git.example.edu", login: "enterprise-user", state: "failure" },
]),
}),
null,
);

assert.strictEqual(
refineUnknownRemote({
host: "git.example.edu",
stdout: authStatusJson([{ host: "github.com", login: "active-user", active: true }]),
}),
null,
);
});

it("refuses a port-bearing remote when gh only knows the bare hostname", () => {
// One hostname can serve two forges on separate ports. Refinement takes the first
// provider that claims the remote, so a bare-hostname match here would beat another
// CLI's exact host:port match and route operations through the wrong provider.
assert.strictEqual(
refineUnknownRemote({
host: "git.example.edu:8443",
stdout: authStatusJson([{ host: "git.example.edu", login: "enterprise-user" }]),
}),
null,
);
});

it("refines port-bearing remotes when gh reports the same host and port", () => {
assert.deepStrictEqual(
refineUnknownRemote({
host: "git.example.edu:8443",
stdout: authStatusJson([{ host: "git.example.edu:8443", login: "enterprise-user" }]),
}),
{
kind: "github",
name: "GitHub Self-Hosted",
baseUrl: "https://git.example.edu:8443",
},
);
});

it("ignores stderr noise when refining, so gh warnings do not break host matching", () => {
const provider = GitHubSourceControlProvider.discovery.refineUnknownRemote?.({
cwd: "/repo",
context: {
provider: {
kind: "unknown",
name: "git.example.edu",
baseUrl: "https://git.example.edu",
},
remoteName: "origin",
remoteUrl: "https://git.example.edu/org/repo.git",
},
auth: processResult(authStatusJson([{ host: "git.example.edu", login: "enterprise-user" }]), {
stderr: "warning: ignored diagnostic from gh\n",
}),
});

assert.strictEqual(provider?.kind, "github");
});

it.effect.each(["pull", "issues"])(
"resolves %s subjects on the linked host without using the checkout",
(kind) =>
Expand Down
50 changes: 49 additions & 1 deletion apps/server/src/sourceControl/GitHubSourceControlProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
type SourceControlAuthProbeInput,
type SourceControlCliDiscoverySpec,
type SourceControlManagedCliDiscoverySpec,
type SourceControlUnknownRemoteRefinementInput,
} from "./SourceControlProviderDiscovery.ts";
import * as VcsProcess from "../vcs/VcsProcess.ts";

Expand Down Expand Up @@ -165,6 +166,31 @@ export function parseGitHubAuth(
});
}

// A GitHub Enterprise host is named by whoever installed it, so hostname guessing misses
// installs like `git.example.edu`. When detection lands on `unknown`, let a host `gh` is
// signed in to claim the remote. Any signed-in account counts rather than only the host's
// active one, so a second login on the same host still resolves the remote; whether `gh`
// knows the host at all is the question, not which account is currently selected.
function refineUnknownGitHubRemote(input: SourceControlUnknownRemoteRefinementInput) {
// Compare the remote host verbatim, port included. A host serving two forges on separate
// ports must not be claimed off a bare-hostname match, since refinement takes the first
// provider that claims the remote and would beat another CLI's exact match.
const host = input.context.provider.name.toLowerCase();
const authenticated = parseGitHubAuthStatus(input.auth.stdout).accounts.some(
(account) => account.authenticated && account.host === host,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
);

if (!authenticated) {
return null;
}

return {
kind: "github",
name: "GitHub Self-Hosted",
baseUrl: input.context.provider.baseUrl,
} as const;
}

export const discovery = {
type: "cli",
kind: "github",
Expand All @@ -173,6 +199,7 @@ export const discovery = {
versionArgs: ["--version"],
authArgs: ["auth", "status", "--json", "hosts"],
parseAuth: parseGitHubAuth,
refineUnknownRemote: refineUnknownGitHubRemote,
installHint:
"Install the GitHub command-line tool (`gh`) via https://cli.github.com/ or your package manager (for example `brew install gh`).",
} satisfies SourceControlCliDiscoverySpec;
Expand Down Expand Up @@ -258,7 +285,28 @@ export const makeDiscovery = Effect.gen(function* () {
},
} satisfies SourceControlProviderDiscoveryItem;
}),
refineUnknownRemote: () => Effect.succeed(null),
refineUnknownRemote: Effect.fn("GitHubSourceControlProvider.refineUnknownRemote")(
function* (input: {
readonly cwd: string;
readonly context: SourceControlProvider.SourceControlProviderContext;
}) {
const auth = yield* process
.run({
operation: "source-control.discovery.refine-unknown-remote",
command: discovery.executable,
args: discovery.authArgs,
cwd: input.cwd,
allowNonZeroExit: true,
timeoutMs: 5_000,
maxOutputBytes: 8_000,
appendTruncationMarker: true,
})
.pipe(Effect.orElseSucceed(() => null));
return auth === null
? null
: refineUnknownGitHubRemote({ cwd: input.cwd, context: input.context, auth });
},
),
} satisfies SourceControlManagedCliDiscoverySpec;
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,54 @@ self-hosted.example.test
}),
);

it.effect("routes authenticated GitHub Enterprise remotes without relying on host naming", () =>
Effect.gen(function* () {
const registry = yield* makeRegistry({
remotes: [{ name: "origin", url: "https://git.example.edu/org/repo.git" }],
process: {
run: () =>
Effect.succeed(
processOutput(
JSON.stringify({
hosts: {
"github.com": [
{
state: "success",
active: true,
host: "github.com",
login: "active-user",
tokenSource: "keyring",
gitProtocol: "https",
},
],
"git.example.edu": [
{
state: "success",
active: false,
host: "git.example.edu",
login: "enterprise-user",
tokenSource: "keyring",
gitProtocol: "https",
},
],
},
}),
),
),
},
});

const handle = yield* registry.resolveHandle({ cwd: "/repo" });

assert.strictEqual(handle.provider.kind, "github");
assert.deepStrictEqual(handle.context?.provider, {
kind: "github",
name: "GitHub Self-Hosted",
baseUrl: "https://git.example.edu",
});
}),
);

it.effect("refines the caller-selected remote instead of choosing another configured remote", () =>
Effect.gen(function* () {
const registry = yield* makeRegistry({
Expand Down
4 changes: 4 additions & 0 deletions docs/user/source-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ If `gh` is signed in to several accounts or hosts, expand **GitHub** in the same
the account each host uses or turn a host off. A saved token or `GH_TOKEN` takes precedence
over that choice; a host turned off stays off either way.

**GitHub Enterprise Server** works the same way, whatever your install is called. Sign in with
`gh auth login --hostname git.example.com`, and T3 Code recognizes projects on that host as GitHub
even when the hostname says nothing about GitHub.

### Forgejo and Gitea

Install [Forgejo CLI (`fj`)](https://codeberg.org/forgejo-contrib/forgejo-cli) or
Expand Down
12 changes: 12 additions & 0 deletions packages/shared/src/sourceControl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,18 @@ describe("detectSourceControlProviderFromRemoteUrl", () => {
).toBe("unknown");
});

it("leaves enterprise hosts without a provider label unknown for CLI refinement", () => {
// GitHub Enterprise hosts are named by the customer, so hostname matching cannot see
// them. Detection reports `unknown` and the provider CLIs settle it during refinement.
const remoteUrl = "https://git.example.edu/org/repo.git";

expect(detectSourceControlProviderFromRemoteUrl(remoteUrl)).toEqual({
kind: "unknown",
name: "git.example.edu",
baseUrl: "https://git.example.edu",
});
});

it("detects SSH remotes with non-git SSH users (e.g. gitlab@, deploy@)", () => {
expect(
detectSourceControlProviderFromRemoteUrl("gitlab@gitlab.example.com:group/project.git")?.kind,
Expand Down
Loading