Skip to content
Merged
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
164 changes: 163 additions & 1 deletion apps/server/src/git/GitManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,12 @@ function createFakeSourceControlProvider(scenario: FakeProviderScenario = {}): {
repository: input.repository,
}).pipe(
Effect.flatMap(() => {
const cloneUrls = scenario.repositoryCloneUrls?.[input.repository];
const repository = input.host ? `${input.host}/${input.repository}` : input.repository;
const cloneUrls =
scenario.repositoryCloneUrls?.[repository] ??
(input.host === "github.com"
? scenario.repositoryCloneUrls?.[input.repository]
: undefined);
return cloneUrls
? Effect.succeed({ nameWithOwner: input.repository, ...cloneUrls })
: Effect.fail(
Expand Down Expand Up @@ -3906,6 +3911,163 @@ it.layer(layerGitManagerTest)("GitManager", (it) => {
}),
);

it.effect.each([
{ mode: "local", repositoryType: "same repo" },
{ mode: "local", repositoryType: "fork" },
{ mode: "worktree", repositoryType: "same repo" },
{ mode: "worktree", repositoryType: "fork" },
] as const)(
"checks out same-named repositories on the pull request host ($mode, $repositoryType)",
({ mode, repositoryType }) =>
Effect.gen(function* () {
const isCrossRepository = repositoryType === "fork";
const repoDir = yield* makeTempDir("t3code-git-manager-");
yield* initRepo(repoDir);
// The checkout is on github.com, which also has a same-named repository,
// and the pull request is on another host. Clone lookups that fall back
// to the checkout's host find the github.com repository.
const enterpriseRemote = yield* createBareRemote();
const defaultHostRemote = yield* createBareRemote();
const host = "github.enterprise.test";
const repository = isCrossRepository ? "fork/repo" : "owner/base";
const cloneUrl = `https://${host}/${repository}.git`;
yield* runGit(repoDir, ["remote", "add", "origin", defaultHostRemote]);
yield* runGit(repoDir, ["push", "origin", "main", "main:feature/host"]);
yield* runGit(repoDir, ["push", enterpriseRemote, "main"]);
yield* runGit(repoDir, ["checkout", "-b", "feature/host"]);
NodeFS.writeFileSync(NodePath.join(repoDir, "host.txt"), "enterprise head\n");
yield* runGit(repoDir, ["add", "host.txt"]);
yield* runGit(repoDir, ["commit", "-m", "Enterprise head"]);
const expectedHead = (yield* runGit(repoDir, ["rev-parse", "HEAD"])).stdout.trim();
yield* runGit(repoDir, ["push", enterpriseRemote, "feature/host"]);
yield* runGit(repoDir, ["checkout", "main"]);
if (mode === "worktree") {
yield* runGit(repoDir, ["branch", "-D", "feature/host"]);
}
yield* configureVisibleRemoteUrlWithLocalRewrite(
repoDir,
"origin",
"https://github.com/owner/base.git",
defaultHostRemote,
);
yield* runGit(repoDir, ["config", `url.${enterpriseRemote}.insteadOf`, cloneUrl]);
const defaultCloneUrl = `https://github.com/${repository}.git`;
yield* runGit(repoDir, [
"config",
"--add",
`url.${defaultHostRemote}.insteadOf`,
defaultCloneUrl,
]);

const { manager } = yield* makeManager({
providerScenario: {
pullRequest: {
number: 42,
title: "Enterprise head",
url: `https://${host}/owner/base/pull/42`,
baseRefName: "main",
headRefName: "feature/host",
state: "open",
isCrossRepository,
headRepositoryNameWithOwner: repository,
headRepositoryOwnerLogin: isCrossRepository ? "fork" : "owner",
},
repositoryCloneUrls: {
[repository]: { url: defaultCloneUrl, sshUrl: defaultCloneUrl },
[`${host}/${repository}`]: { url: cloneUrl, sshUrl: cloneUrl },
},
},
});
const result = yield* preparePullRequestThread(manager, {
cwd: repoDir,
reference: "42",
mode,
});
const checkoutDir = result.worktreePath ?? repoDir;
const upstreamRemote = (yield* runGit(checkoutDir, [
"config",
`branch.${result.branch}.remote`,
])).stdout.trim();
const upstreamUrl = (yield* runGit(checkoutDir, [
"config",
`remote.${upstreamRemote}.url`,
])).stdout.trim();
const head = (yield* runGit(checkoutDir, ["rev-parse", "HEAD"])).stdout.trim();
expect({ upstreamUrl, head }).toEqual({ upstreamUrl: cloneUrl, head: expectedHead });
expect(NodeFS.readFileSync(NodePath.join(checkoutDir, "host.txt"), "utf8")).toBe(
"enterprise head\n",
);
}),
);

it.effect.each([
{ mode: "local", url: "malformed pull request URL" },
{ mode: "worktree", url: "malformed pull request URL" },
{ mode: "local", url: "file:///owner/base/pull/42" },
{ mode: "worktree", url: "file:///owner/base/pull/42" },
] as const)(
"checks out pull requests with hostless URLs using the default host ($mode, $url)",
({ mode, url }) =>
Effect.gen(function* () {
const repoDir = yield* makeTempDir("t3code-git-manager-");
yield* initRepo(repoDir);
const originDir = yield* createBareRemote();
const forkDir = yield* createBareRemote();
const cloneUrl = "https://github.com/fork/repo.git";
yield* runGit(repoDir, ["remote", "add", "origin", originDir]);
yield* runGit(repoDir, ["push", "origin", "main"]);
yield* runGit(repoDir, ["checkout", "-b", "feature/malformed-url"]);
NodeFS.writeFileSync(NodePath.join(repoDir, "fork.txt"), "fork head\n");
yield* runGit(repoDir, ["add", "fork.txt"]);
yield* runGit(repoDir, ["commit", "-m", "Fork head"]);
const expectedHead = (yield* runGit(repoDir, ["rev-parse", "HEAD"])).stdout.trim();
yield* runGit(repoDir, ["push", forkDir, "feature/malformed-url"]);
yield* runGit(repoDir, ["checkout", "main"]);
if (mode === "worktree") {
yield* runGit(repoDir, ["branch", "-D", "feature/malformed-url"]);
}
yield* runGit(repoDir, ["config", `url.${forkDir}.insteadOf`, cloneUrl]);

const { manager } = yield* makeManager({
providerScenario: {
pullRequest: {
number: 42,
title: "Fork head",
url,
baseRefName: "main",
headRefName: "feature/malformed-url",
state: "open",
isCrossRepository: true,
headRepositoryNameWithOwner: "fork/repo",
headRepositoryOwnerLogin: "fork",
},
repositoryCloneUrls: {
"fork/repo": { url: cloneUrl, sshUrl: cloneUrl },
},
},
});
const result = yield* preparePullRequestThread(manager, {
cwd: repoDir,
reference: "42",
mode,
});
const checkoutDir = result.worktreePath ?? repoDir;
const upstreamRemote = (yield* runGit(checkoutDir, [
"config",
`branch.${result.branch}.remote`,
])).stdout.trim();
const upstreamUrl = (yield* runGit(checkoutDir, [
"config",
`remote.${upstreamRemote}.url`,
])).stdout.trim();
const head = (yield* runGit(checkoutDir, ["rev-parse", "HEAD"])).stdout.trim();
expect({ upstreamUrl, head }).toEqual({ upstreamUrl: cloneUrl, head: expectedHead });
expect(NodeFS.readFileSync(NodePath.join(checkoutDir, "fork.txt"), "utf8")).toBe(
"fork head\n",
);
}),
);

const setUpLocalCheckoutPullRequest = Effect.fnUntraced(function* (
reportedHead: (pullHead: string) => string | undefined = (pullHead) => pullHead,
syncCheckoutWithPullHead = true,
Expand Down
7 changes: 7 additions & 0 deletions apps/server/src/git/GitManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,11 @@ export const make = Effect.gen(function* () {
}),
);

const pullRequestHostOptions = (url: string) => {
const host = URL.parse(url)?.host;
return host ? { host } : {};
};

const configurePullRequestHeadUpstreamBase = Effect.fn("configurePullRequestHeadUpstream")(
function* (
cwd: string,
Expand Down Expand Up @@ -923,6 +928,7 @@ export const make = Effect.gen(function* () {
const cloneUrls = yield* (yield* sourceControlProvider(cwd)).getRepositoryCloneUrls({
cwd,
repository: repositoryNameWithOwner,
...pullRequestHostOptions(pullRequest.url),
});
const originRemoteUrl = yield* gitCore.readConfigValue(cwd, "remote.origin.url");
const remoteUrl = shouldPreferSshRemote(originRemoteUrl) ? cloneUrls.sshUrl : cloneUrls.url;
Expand Down Expand Up @@ -1036,6 +1042,7 @@ export const make = Effect.gen(function* () {
const cloneUrls = yield* (yield* sourceControlProvider(cwd)).getRepositoryCloneUrls({
cwd,
repository: repositoryNameWithOwner,
...pullRequestHostOptions(pullRequest.url),
});
const originRemoteUrl = yield* gitCore.readConfigValue(cwd, "remote.origin.url");
const remoteUrl = shouldPreferSshRemote(originRemoteUrl) ? cloneUrls.sshUrl : cloneUrls.url;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ export class SourceControlProvider extends Context.Service<
readonly cwd: string;
readonly context?: SourceControlProviderContext;
readonly repository: string;
/** The host the repository is on. Without it, the checkout's host. */
readonly host?: string;
}) => Effect.Effect<SourceControlRepositoryCloneUrls, SourceControlProviderError>;
readonly createRepository: (input: {
readonly cwd: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,73 @@ const requestJson = (request: HttpClientRequest.HttpClientRequest): unknown => {
};
const encodeJsonEffect = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));

it.effect("reads clone URLs on the given host rather than the checkout's", () => {
const urls: string[] = [];
const client = HttpClient.make((request) => {
urls.push(request.url);
return Effect.succeed(
HttpClientResponse.fromWeb(
request,
new Response(
encodeJson({
full_name: "maria/project",
clone_url: "https://forgejo.test/maria/project.git",
ssh_url: "git@forgejo.test:maria/project.git",
}),
),
),
);
});
return Effect.gen(function* () {
const cli = yield* ForgejoCli.make;
const provider = yield* ForgejoSourceControlProvider.make.pipe(
Effect.provideService(ForgejoCli.ForgejoCli, cli),
);
const cloneUrls = yield* provider.getRepositoryCloneUrls({
cwd: "/repo",
repository: "maria/project",
host: "forgejo.test",
});
assert.strictEqual(cloneUrls.url, "https://forgejo.test/maria/project.git");
assert.deepStrictEqual(urls, ["https://forgejo.test/api/v1/repos/maria/project"]);
}).pipe(
Effect.provideService(HttpClient.HttpClient, client),
Effect.provideService(
FileSystem.FileSystem,
FileSystem.makeNoop({
exists: () => Effect.succeed(true),
readFileString: () =>
Effect.succeed(
encodeJson({
hosts: {
"forgejo.test": { type: "Application", token: "test-token" },
"code.example": { type: "Application", token: "other-token" },
},
}),
),
}),
),
Effect.provide(
TestSourceControlHost.layer({
process: {
run: (input) => {
if (input.command === "git") {
assert.deepStrictEqual(input.args, ["remote", "-v"]);
// The checkout is on another Forgejo server with a same-named repository.
return Effect.succeed(
processOutput("origin\thttps://code.example/maria/project.git (fetch)"),
);
}
assert.strictEqual(input.command, "fj");
assert.deepStrictEqual(input.args, ["--host", "https://forgejo.test", "whoami"]);
return Effect.succeed(processOutput(""));
},
},
}),
),
);
});

const requiredChecksPullRequest = {
number: 7,
title: "Required checks",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,38 @@ describe("GitHubSourceControlProvider repository resolution", () => {
}).pipe(Effect.provide(layer));
});

it.effect("reads clone URLs on the given host before the checkout's", () => {
const paths: string[] = [];
const { layer } = harness({
remotes: remotesOutput(["origin", "https://github.com/acme/web.git"]),
api: {
rest: (input) =>
Effect.sync(() => {
paths.push(`${input.host} ${input.path}`);
return restResponse({
full_name: "acme/web",
html_url: `https://${input.host}/acme/web`,
ssh_url: `git@${input.host}:acme/web.git`,
});
}),
},
});
return Effect.gen(function* () {
const gh = yield* GitHubSourceControlProvider.make;
const cloneUrls = yield* gh.getRepositoryCloneUrls({
cwd: "/repo",
repository: "acme/web",
host: "GitHub.Enterprise.test",
});
yield* gh.getRepositoryCloneUrls({ cwd: "/repo", repository: "acme/web" });
assert.strictEqual(cloneUrls.url, "https://github.enterprise.test/acme/web");
assert.deepStrictEqual(paths, [
"github.enterprise.test repos/acme/web",
"github.com repos/acme/web",
]);
}).pipe(Effect.provide(layer));
});

it.effect.each([
["github", "git@github:acme/web.git"],
["github.com-scale", "git@github.com-scale:acme/web.git"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -996,10 +996,13 @@ export const make = Effect.gen(function* () {
),
getRepositoryCloneUrls: Effect.fn("GitHubSourceControlProvider.getRepositoryCloneUrls")(
function* (input) {
const fallbackHost = (yield* resolveRepository(input).pipe(
Effect.map((locator) => locator.host),
Effect.orElseSucceed(() => environment.GH_HOST ?? "github.com"),
)).toLowerCase();
const fallbackHost = (
input.host ??
(yield* resolveRepository(input).pipe(
Effect.map((locator) => locator.host),
Effect.orElseSucceed(() => environment.GH_HOST ?? "github.com"),
))
).toLowerCase();
const bareName = input.repository.trim().replace(/\.git$/i, "");
const locator =
parseGitHubRepositorySelector(input.repository, fallbackHost) ??
Expand Down
Loading