diff --git a/apps/server/src/vcs/GitVcsDriver.test.ts b/apps/server/src/vcs/GitVcsDriver.test.ts index d33c2b10ed514..1b269e4ebac88 100644 --- a/apps/server/src/vcs/GitVcsDriver.test.ts +++ b/apps/server/src/vcs/GitVcsDriver.test.ts @@ -102,35 +102,54 @@ runVcsDriverContractSuite({ }, }); -it.effect("lists the fetch and push URLs of a partial clone remote", () => - Effect.gen(function* () { - const fileSystem = yield* FileSystem.FileSystem; - const cwd = yield* fileSystem.makeTempDirectoryScoped({ - prefix: "t3-partial-clone-remotes-", - }); - const driver = yield* GitVcsDriver.makeVcsDriverShape(); - yield* runGit(cwd, ["init"]); - yield* runGit(cwd, ["remote", "add", "origin", "https://github.com/pingdotgg/t3code.git"]); - yield* runGit(cwd, [ - "remote", - "set-url", - "--push", - "origin", - "git@github.com:octocat/t3code.git", - ]); - yield* runGit(cwd, ["config", "remote.origin.promisor", "true"]); - yield* runGit(cwd, ["config", "remote.origin.partialclonefilter", "blob:none"]); - - const result = yield* driver.listRemotes(cwd); - assert.deepEqual(result.remotes, [ - { - name: "origin", - url: "https://github.com/pingdotgg/t3code.git", - pushUrl: Option.some("git@github.com:octocat/t3code.git"), - isPrimary: true, - }, - ]); - }).pipe(Effect.scoped, Effect.provide(layerGitContract)), +it.effect.each([ + { + fetchUrl: "https://github.com/pingdotgg/t3code.git", + pushUrl: "git@github.com:octocat/t3code.git", + lineEnding: "\n", + }, + { fetchUrl: "../team repos/shared.git", pushUrl: "../team repos/push.git", lineEnding: "\n" }, + { + fetchUrl: " ../team repos/shared (fetch) [blob:none].git ", + pushUrl: "../team repos/push (push).git\t", + lineEnding: "\r\n", + }, +])( + "lists the fetch and push URLs of a partial clone remote (%j)", + ({ fetchUrl, pushUrl, lineEnding }) => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const cwd = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-partial-clone-remotes-", + }); + const process = yield* VcsProcess.VcsProcess; + const driver = yield* GitVcsDriver.makeVcsDriverShape().pipe( + Effect.provideService(VcsProcess.VcsProcess, { + run: (input) => + process.run(input).pipe( + Effect.map((result) => ({ + ...result, + stdout: result.stdout.replace(/\r?\n/g, lineEnding), + })), + ), + }), + ); + yield* runGit(cwd, ["init"]); + yield* runGit(cwd, ["remote", "add", "origin", fetchUrl]); + yield* runGit(cwd, ["remote", "set-url", "--push", "origin", pushUrl]); + yield* runGit(cwd, ["config", "remote.origin.promisor", "true"]); + yield* runGit(cwd, ["config", "remote.origin.partialclonefilter", "blob:none"]); + + const result = yield* driver.listRemotes(cwd); + assert.deepEqual(result.remotes, [ + { + name: "origin", + url: fetchUrl, + pushUrl: Option.some(pushUrl), + isPrimary: true, + }, + ]); + }).pipe(Effect.scoped, Effect.provide(layerGitContract)), ); const makeCheckpointFixture = Effect.fn("makeCheckpointFixture")(function* ( diff --git a/apps/server/src/vcs/GitVcsDriver.ts b/apps/server/src/vcs/GitVcsDriver.ts index ff129690b7b24..fcdb372f4435f 100644 --- a/apps/server/src/vcs/GitVcsDriver.ts +++ b/apps/server/src/vcs/GitVcsDriver.ts @@ -518,14 +518,10 @@ function parseGitRemoteVerboseOutput( output: string, ): Map { const remotes = new Map(); - for (const line of output.split("\n")) { - const trimmed = line.trim(); - if (trimmed.length === 0) { - continue; - } - + for (const line of output.split(/\r?\n/)) { + // Git uses a tab before the URL and one space before the direction; both can occur in URLs. // Partial clones append the promisor filter, e.g. `(fetch) [blob:none]`. - const match = /^(\S+)\s+(\S+)\s+\((fetch|push)\)(?:\s+\[[^\]]+\])?$/.exec(trimmed); + const match = /^(\S+)\t(.+) \((fetch|push)\)(?:\s+\[[^\]]+\])?$/.exec(line); if (!match) { continue; } diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index 70b2d50eda88e..3130fdc9eda41 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -5593,6 +5593,61 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { }), ); + it.effect.each(["\n", "\r\n"])( + "ensureRemote preserves spaces in fetch URLs and reserves their names (%j)", + (lineEnding) => + Effect.gen(function* () { + const cwd = yield* makeTmpDir(); + const fetchUrl = " ../team repos/shared (fetch) [blob:none].git "; + const pushUrl = "../team repos/push (push).git"; + yield* git(cwd, ["init"]); + yield* git(cwd, ["remote", "add", "origin", fetchUrl]); + yield* git(cwd, ["remote", "set-url", "--push", "origin", pushUrl]); + yield* git(cwd, ["config", "remote.origin.promisor", "true"]); + yield* git(cwd, ["config", "remote.origin.partialclonefilter", "blob:none"]); + const liveDriver = yield* GitVcsDriver.GitVcsDriver; + const listing = yield* liveDriver.execute({ + cwd, + args: ["remote", "-v"], + operation: "test", + }); + const delegate = yield* ChildProcessSpawner.ChildProcessSpawner; + const spawner = ChildProcessSpawner.make((command) => + ChildProcess.isStandardCommand(command) && + command.args[0] === "remote" && + command.args[1] === "-v" + ? Effect.succeed(makeSuccessfulHandle(listing.stdout.replace(/\r?\n/g, lineEnding))) + : delegate.spawn(command), + ); + const driver = yield* makeGitVcsDriverCore().pipe( + Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), + Effect.provide(layerServerConfig), + ); + + assert.equal( + yield* driver.ensureRemote({ cwd, preferredName: "origin", url: fetchUrl }), + "origin", + ); + assert.equal( + yield* driver.ensureRemote({ cwd, preferredName: "origin", url: pushUrl }), + "origin-1", + ); + const fetch = yield* liveDriver.execute({ + cwd, + args: ["config", "--get", "remote.origin.url"], + operation: "test", + }); + const push = yield* liveDriver.execute({ + cwd, + args: ["config", "--get", "remote.origin.pushurl"], + operation: "test", + }); + assert.equal(fetch.stdout, `${fetchUrl}\n`); + assert.equal(push.stdout, `${pushUrl}\n`); + assert.equal(yield* git(cwd, ["config", "--get", "remote.origin-1.url"]), pushUrl); + }), + ); + it.effect("ensureRemote returns the remote a parallel call added for the same URL", () => Effect.gen(function* () { const cwd = yield* makeTmpDir(); diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index 3d8122f197971..f1f5428756689 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -350,11 +350,10 @@ function sanitizeRemoteName(value: string): string { function parseRemoteFetchUrls(stdout: string): Map { const remotes = new Map(); - for (const line of stdout.split("\n")) { - const trimmed = line.trim(); - if (trimmed.length === 0) continue; + for (const line of stdout.split(/\r?\n/)) { + // Git uses a tab before the URL and one space before the direction; both can occur in URLs. // Partial clones append the promisor filter, e.g. `(fetch) [blob:none]`. - const match = /^(\S+)\s+(\S+)\s+\((fetch|push)\)(?:\s+\[[^\]]+\])?$/.exec(trimmed); + const match = /^(\S+)\t(.+) \((fetch|push)\)(?:\s+\[[^\]]+\])?$/.exec(line); if (!match) continue; const [, remoteName = "", remoteUrl = "", direction = ""] = match; if (direction !== "fetch" || remoteName.length === 0 || remoteUrl.length === 0) {