diff --git a/apps/server/src/git/Layers/GitCore.test.ts b/apps/server/src/git/Layers/GitCore.test.ts index 52a201e321c5bd76f2cbc083e829016b84fc69cf..5aed70a78b560811c80c4a558873cb98f60d0a4e 100644 --- a/apps/server/src/git/Layers/GitCore.test.ts +++ b/apps/server/src/git/Layers/GitCore.test.ts @@ -1323,6 +1323,25 @@ it.layer(TestLayer)("git integration", (it) => { }), ); + it.effect("reuses an existing remote when the target URL only differs by a trailing slash after .git", () => + Effect.gen(function* () { + const tmp = yield* makeTmpDir(); + yield* initRepoWithCommit(tmp); + const core = yield* GitCore; + + yield* git(tmp, ["remote", "add", "origin", "git@github.com:pingdotgg/t3code.git"]); + + const remoteName = yield* core.ensureRemote({ + cwd: tmp, + preferredName: "origin", + url: "git@github.com:pingdotgg/t3code.git/", + }); + + expect(remoteName).toBe("origin"); + expect((yield* git(tmp, ["remote"])).split("\n").filter(Boolean)).toEqual(["origin"]); + }), + ); + it.effect("reports status details and dirty state", () => Effect.gen(function* () { const tmp = yield* makeTmpDir(); diff --git a/apps/server/src/git/Layers/GitCore.ts b/apps/server/src/git/Layers/GitCore.ts index 57a2390dd4b9031ec88f7c76203b05ae4e5b943b..86eeb3fb4fba12def1ce6f590d99299036da8591 100644 --- a/apps/server/src/git/Layers/GitCore.ts +++ b/apps/server/src/git/Layers/GitCore.ts @@ -106,7 +106,7 @@ function sanitizeRemoteName(value: string): string { } function normalizeRemoteUrl(value: string): string { - return value.trim().replace(/\.git$/i, "").replace(/\/+$/g, "").toLowerCase(); + return value.trim().replace(/\/+$/g, "").replace(/\.git$/i, "").toLowerCase(); } function parseRemoteFetchUrls(stdout: string): Map { diff --git a/apps/server/src/git/Layers/GitHubCli.test.ts b/apps/server/src/git/Layers/GitHubCli.test.ts index e809b4368e3a63fc20d592ac496f8afbe8923566..2a75aa47631e7a5260435829d6aff59f4131bb80 100644 --- a/apps/server/src/git/Layers/GitHubCli.test.ts +++ b/apps/server/src/git/Layers/GitHubCli.test.ts @@ -1,5 +1,6 @@ -import { describe, expect, it, vi } from "vitest"; -import { Effect } from "effect"; +import { assert, it } from "@effect/vitest"; +import { Effect, Layer } from "effect"; +import { afterEach, expect, vi } from "vitest"; vi.mock("../../processRunner", () => ({ runProcess: vi.fn(), @@ -10,113 +11,118 @@ import { GitHubCli } from "../Services/GitHubCli.ts"; import { GitHubCliLive } from "./GitHubCli.ts"; const mockedRunProcess = vi.mocked(runProcess); +const layer = it.layer(Layer.empty); -describe("GitHubCliLive", () => { - it("parses pull request view output", async () => { - mockedRunProcess.mockResolvedValueOnce({ - stdout: JSON.stringify({ +afterEach(() => { + mockedRunProcess.mockReset(); +}); + +layer("GitHubCliLive", (it) => { + it.effect("parses pull request view output", () => + Effect.gen(function* () { + mockedRunProcess.mockResolvedValueOnce({ + stdout: JSON.stringify({ + number: 42, + title: "Add PR thread creation", + url: "https://github.com/pingdotgg/codething-mvp/pull/42", + baseRefName: "main", + headRefName: "feature/pr-threads", + state: "OPEN", + mergedAt: null, + isCrossRepository: true, + headRepository: { + nameWithOwner: "octocat/codething-mvp", + }, + headRepositoryOwner: { + login: "octocat", + }, + }), + stderr: "", + code: 0, + signal: null, + timedOut: false, + }); + + const result = yield* Effect.gen(function* () { + const gh = yield* GitHubCli; + return yield* gh.getPullRequest({ + cwd: "/repo", + reference: "#42", + }); + }).pipe(Effect.provide(GitHubCliLive)); + + assert.deepStrictEqual(result, { number: 42, title: "Add PR thread creation", url: "https://github.com/pingdotgg/codething-mvp/pull/42", baseRefName: "main", headRefName: "feature/pr-threads", - state: "OPEN", - mergedAt: null, + state: "open", isCrossRepository: true, - headRepository: { + headRepositoryNameWithOwner: "octocat/codething-mvp", + headRepositoryOwnerLogin: "octocat", + }); + expect(mockedRunProcess).toHaveBeenCalledWith( + "gh", + [ + "pr", + "view", + "#42", + "--json", + "number,title,url,baseRefName,headRefName,state,mergedAt,isCrossRepository,headRepository,headRepositoryOwner", + ], + expect.objectContaining({ cwd: "/repo" }), + ); + }), + ); + + it.effect("reads repository clone URLs", () => + Effect.gen(function* () { + mockedRunProcess.mockResolvedValueOnce({ + stdout: JSON.stringify({ nameWithOwner: "octocat/codething-mvp", - }, - headRepositoryOwner: { - login: "octocat", - }, - }), - stderr: "", - code: 0, - signal: null, - timedOut: false, - }); + url: "https://github.com/octocat/codething-mvp", + sshUrl: "git@github.com:octocat/codething-mvp.git", + }), + stderr: "", + code: 0, + signal: null, + timedOut: false, + }); - const result = await Effect.runPromise( - Effect.gen(function* () { + const result = yield* Effect.gen(function* () { const gh = yield* GitHubCli; - return yield* gh.getPullRequest({ + return yield* gh.getRepositoryCloneUrls({ cwd: "/repo", - reference: "#42", + repository: "octocat/codething-mvp", }); - }).pipe(Effect.provide(GitHubCliLive)), - ); + }).pipe(Effect.provide(GitHubCliLive)); - expect(result).toEqual({ - number: 42, - title: "Add PR thread creation", - url: "https://github.com/pingdotgg/codething-mvp/pull/42", - baseRefName: "main", - headRefName: "feature/pr-threads", - state: "open", - isCrossRepository: true, - headRepositoryNameWithOwner: "octocat/codething-mvp", - headRepositoryOwnerLogin: "octocat", - }); - expect(mockedRunProcess).toHaveBeenCalledWith( - "gh", - [ - "pr", - "view", - "#42", - "--json", - "number,title,url,baseRefName,headRefName,state,mergedAt,isCrossRepository,headRepository,headRepositoryOwner", - ], - expect.objectContaining({ cwd: "/repo" }), - ); - }); - - it("reads repository clone URLs", async () => { - mockedRunProcess.mockResolvedValueOnce({ - stdout: JSON.stringify({ + assert.deepStrictEqual(result, { nameWithOwner: "octocat/codething-mvp", url: "https://github.com/octocat/codething-mvp", sshUrl: "git@github.com:octocat/codething-mvp.git", - }), - stderr: "", - code: 0, - signal: null, - timedOut: false, - }); - - const result = await Effect.runPromise( - Effect.gen(function* () { - const gh = yield* GitHubCli; - return yield* gh.getRepositoryCloneUrls({ - cwd: "/repo", - repository: "octocat/codething-mvp", - }); - }).pipe(Effect.provide(GitHubCliLive)), - ); - - expect(result).toEqual({ - nameWithOwner: "octocat/codething-mvp", - url: "https://github.com/octocat/codething-mvp", - sshUrl: "git@github.com:octocat/codething-mvp.git", - }); - }); - - it("surfaces a friendly error when the pull request is not found", async () => { - mockedRunProcess.mockRejectedValueOnce( - new Error( - "GraphQL: Could not resolve to a PullRequest with the number of 4888. (repository.pullRequest)", - ), - ); - - const error = await Effect.runPromise( - Effect.gen(function* () { + }); + }), + ); + + it.effect("surfaces a friendly error when the pull request is not found", () => + Effect.gen(function* () { + mockedRunProcess.mockRejectedValueOnce( + new Error( + "GraphQL: Could not resolve to a PullRequest with the number of 4888. (repository.pullRequest)", + ), + ); + + const error = yield* Effect.gen(function* () { const gh = yield* GitHubCli; return yield* gh.getPullRequest({ cwd: "/repo", reference: "4888", }); - }).pipe(Effect.provide(GitHubCliLive), Effect.flip), - ); + }).pipe(Effect.provide(GitHubCliLive), Effect.flip); - expect(error.message).toContain("Pull request not found"); - }); + assert.equal(error.message.includes("Pull request not found"), true); + }), + ); });