| ... | @@ -1698,6 +1698,64 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => { | ... | @@ -1698,6 +1698,64 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => { |
| 1698 | }); | 1698 | }); |
| 1699 | | 1699 | |
| 1700 | describe("worktree operations", () => { | 1700 | describe("worktree operations", () => { |
| | 1701 | it.effect("uses parallel checkout without skipping filters or hooks", () => |
| | 1702 | Effect.gen(function* () { |
| | 1703 | const cwd = yield* makeTmpDir(); |
| | 1704 | const { initialBranch } = yield* initRepoWithCommit(cwd); |
| | 1705 | const fs = yield* FileSystem.FileSystem; |
| | 1706 | const path = yield* Path.Path; |
| | 1707 | const driver = yield* GitVcsDriver.GitVcsDriver; |
| | 1708 | yield* git(cwd, ["config", "filter.test.smudge", "sed s/original/filtered/g"]); |
| | 1709 | yield* writeTextFile(cwd, ".gitattributes", "asset.txt filter=test\n"); |
| | 1710 | yield* writeTextFile(cwd, "asset.txt", "original\n"); |
| | 1711 | yield* git(cwd, ["add", "."]); |
| | 1712 | yield* git(cwd, ["commit", "-m", "filtered asset"]); |
| | 1713 | yield* writeTextFile( |
| | 1714 | cwd, |
| | 1715 | ".git/hooks/post-checkout", |
| | 1716 | "#!/bin/sh\ngit config checkout.workers > checkout-workers\nexit 0\n", |
| | 1717 | ); |
| | 1718 | yield* fs.chmod(path.join(cwd, ".git/hooks/post-checkout"), 0o755); |
| | 1719 | const worktreePath = path.join(yield* makeTmpDir("git-worktrees-"), "parallel"); |
| | 1720 | |
| | 1721 | yield* driver.createWorktree({ |
| | 1722 | cwd, |
| | 1723 | path: worktreePath, |
| | 1724 | refName: initialBranch, |
| | 1725 | newRefName: "feature/parallel", |
| | 1726 | baseRefName: initialBranch, |
| | 1727 | }); |
| | 1728 | |
| | 1729 | assert.notInclude(yield* git(cwd, ["worktree", "list", "--porcelain"]), "locked"); |
| | 1730 | assert.equal(yield* fs.readFileString(path.join(worktreePath, "checkout-workers")), "0\n"); |
| | 1731 | assert.equal(yield* fs.readFileString(path.join(worktreePath, "asset.txt")), "filtered\n"); |
| | 1732 | assert.equal( |
| | 1733 | yield* git(worktreePath, ["rev-parse", "HEAD"]), |
| | 1734 | yield* git(cwd, ["rev-parse", "HEAD"]), |
| | 1735 | ); |
| | 1736 | assert.equal( |
| | 1737 | yield* git(cwd, ["config", "branch.feature/parallel.gh-merge-base"]), |
| | 1738 | initialBranch, |
| | 1739 | ); |
| | 1740 | for (const [configured, expected] of [ |
| | 1741 | ["1", "1"], |
| | 1742 | ["", "0"], |
| | 1743 | ] as const) { |
| | 1744 | yield* git(cwd, ["config", "checkout.workers", configured]); |
| | 1745 | const configuredPath = path.join(yield* makeTmpDir("git-worktrees-"), "configured"); |
| | 1746 | yield* driver.createWorktree({ |
| | 1747 | cwd, |
| | 1748 | path: configuredPath, |
| | 1749 | refName: initialBranch, |
| | 1750 | newRefName: `feature/configured-${expected}`, |
| | 1751 | }); |
| | 1752 | assert.equal( |
| | 1753 | yield* fs.readFileString(path.join(configuredPath, "checkout-workers")), |
| | 1754 | `${expected}\n`, |
| | 1755 | ); |
| | 1756 | } |
| | 1757 | }), |
| | 1758 | ); |
| 1701 | it("parses checkout progress lines from git's stderr", () => { | 1759 | it("parses checkout progress lines from git's stderr", () => { |
| 1702 | assert.deepStrictEqual(parseGitCheckoutProgressLine("Updating files: 78% (2104/2700)"), { | 1760 | assert.deepStrictEqual(parseGitCheckoutProgressLine("Updating files: 78% (2104/2700)"), { |
| 1703 | percent: 78, | 1761 | percent: 78, |
| ... | @@ -1827,11 +1885,11 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => { | ... | @@ -1827,11 +1885,11 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => { |
| 1827 | }), | 1885 | }), |
| 1828 | ); | 1886 | ); |
| 1829 | | 1887 | |
| 1830 | it.effect("reports checkout progress while creating a worktree", () => | 1888 | it.effect("reports checkout progress during parallel worktree creation", () => |
| 1831 | Effect.gen(function* () { | 1889 | Effect.gen(function* () { |
| 1832 | const cwd = yield* makeTmpDir(); | 1890 | const cwd = yield* makeTmpDir(); |
| 1833 | const { initialBranch } = yield* initRepoWithCommit(cwd); | 1891 | const { initialBranch } = yield* initRepoWithCommit(cwd); |
| 1834 | for (let index = 0; index < 5; index += 1) { | 1892 | for (let index = 0; index < 200; index += 1) { |
| 1835 | yield* writeTextFile(cwd, `file-${index}.txt`, `${index}\n`); | 1893 | yield* writeTextFile(cwd, `file-${index}.txt`, `${index}\n`); |
| 1836 | } | 1894 | } |
| 1837 | yield* git(cwd, ["add", "."]); | 1895 | yield* git(cwd, ["add", "."]); |
| ... | @@ -1865,7 +1923,7 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => { | ... | @@ -1865,7 +1923,7 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => { |
| 1865 | const updates = yield* Ref.get(seen); | 1923 | const updates = yield* Ref.get(seen); |
| 1866 | assert.isAbove(updates.length, 1); | 1924 | assert.isAbove(updates.length, 1); |
| 1867 | assert.equal(updates.at(-1)?.percent, 100); | 1925 | assert.equal(updates.at(-1)?.percent, 100); |
| 1868 | assert.equal(updates.at(-1)?.total, 6); | 1926 | assert.equal(updates.at(-1)?.total, 201); |
| 1869 | const completed = updates.map((update) => update.completed); | 1927 | const completed = updates.map((update) => update.completed); |
| 1870 | assert.deepEqual( | 1928 | assert.deepEqual( |
| 1871 | completed, | 1929 | completed, |
| ... | @@ -2098,6 +2156,61 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => { | ... | @@ -2098,6 +2156,61 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => { |
| 2098 | }); | 2156 | }); |
| 2099 | | 2157 | |
| 2100 | describe("remote operations", () => { | 2158 | describe("remote operations", () => { |
| | 2159 | for (const failure of ["offline", "auth", "timeout"] as const) { |
| | 2160 | it.effect(`does not retry a scoped fetch after ${failure}`, () => |
| | 2161 | Effect.gen(function* () { |
| | 2162 | const cwd = yield* makeTmpDir(); |
| | 2163 | const delegate = yield* ChildProcessSpawner.ChildProcessSpawner; |
| | 2164 | const started = yield* Deferred.make<void>(); |
| | 2165 | const attempts: Array<ReadonlyArray<string>> = []; |
| | 2166 | const spawner = ChildProcessSpawner.make((command) => |
| | 2167 | Effect.gen(function* () { |
| | 2168 | if (!ChildProcess.isStandardCommand(command)) |
| | 2169 | return yield* Effect.die("unexpected command"); |
| | 2170 | if (command.args[0] !== "fetch") return yield* delegate.spawn(command); |
| | 2171 | attempts.push(command.args); |
| | 2172 | yield* Deferred.succeed(started, undefined); |
| | 2173 | return ChildProcessSpawner.makeHandle({ |
| | 2174 | ...makeNonRepositoryHandle(), |
| | 2175 | exitCode: |
| | 2176 | failure === "timeout" |
| | 2177 | ? Effect.never |
| | 2178 | : Effect.succeed(ChildProcessSpawner.ExitCode(128)), |
| | 2179 | stderr: Stream.encodeText( |
| | 2180 | Stream.make( |
| | 2181 | failure === "auth" |
| | 2182 | ? "fatal: Authentication failed" |
| | 2183 | : "fatal: Could not resolve host", |
| | 2184 | ), |
| | 2185 | ), |
| | 2186 | }); |
| | 2187 | }), |
| | 2188 | ); |
| | 2189 | const driver = yield* makeGitVcsDriverCore().pipe( |
| | 2190 | Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), |
| | 2191 | Effect.provide(ServerConfigLayer), |
| | 2192 | ); |
| | 2193 | const fetching = yield* driver |
| | 2194 | .fetchRemote({ cwd, remoteName: "origin", refName: "main" }) |
| | 2195 | .pipe(Effect.result, Effect.forkChild({ startImmediately: true })); |
| | 2196 | yield* Deferred.await(started); |
| | 2197 | if (failure === "timeout") { |
| | 2198 | yield* TestClock.adjust("31 seconds"); |
| | 2199 | yield* TestClock.adjust("31 seconds"); |
| | 2200 | } |
| | 2201 | const result = yield* Fiber.join(fetching); |
| | 2202 | assert.isTrue(Result.isFailure(result)); |
| | 2203 | assert.equal(attempts.length, 1); |
| | 2204 | if (Result.isFailure(result)) { |
| | 2205 | assert.equal( |
| | 2206 | result.failure.detail, |
| | 2207 | failure === "timeout" ? "Git command timed out." : "git fetch origin failed", |
| | 2208 | ); |
| | 2209 | } |
| | 2210 | }), |
| | 2211 | ); |
| | 2212 | } |
| | 2213 | |
| 2101 | it.effect("creates a worktree from the latest fetched remote commit", () => | 2214 | it.effect("creates a worktree from the latest fetched remote commit", () => |
| 2102 | Effect.gen(function* () { | 2215 | Effect.gen(function* () { |
| 2103 | const cwd = yield* makeTmpDir(); | 2216 | const cwd = yield* makeTmpDir(); |
| ... | @@ -2120,8 +2233,16 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => { | ... | @@ -2120,8 +2233,16 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => { |
| 2120 | const remoteHead = yield* git(peer, ["rev-parse", "HEAD"]); | 2233 | const remoteHead = yield* git(peer, ["rev-parse", "HEAD"]); |
| 2121 | assert.notEqual(beforeFetch, remoteHead); | 2234 | assert.notEqual(beforeFetch, remoteHead); |
| 2122 | | 2235 | |
| | 2236 | yield* git(peer, ["push", "origin", "HEAD:refs/heads/unrelated"]); |
| 2123 | const driver = yield* GitVcsDriver.GitVcsDriver; | 2237 | const driver = yield* GitVcsDriver.GitVcsDriver; |
| 2124 | yield* driver.fetchRemote({ cwd, remoteName: "origin" }); | 2238 | yield* driver.fetchRemote({ |
| | 2239 | cwd, |
| | 2240 | remoteName: "origin", |
| | 2241 | refName: `origin/${initialBranch}`, |
| | 2242 | }); |
| | 2243 | assert.isFalse( |
| | 2244 | yield* driver.remoteBranchExists({ cwd, remoteName: "origin", refName: "unrelated" }), |
| | 2245 | ); |
| 2125 | | 2246 | |
| 2126 | assert.equal( | 2247 | assert.equal( |
| 2127 | yield* driver.remoteBranchExists({ | 2248 | yield* driver.remoteBranchExists({ |
| ... | @@ -2183,6 +2304,11 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => { | ... | @@ -2183,6 +2304,11 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => { |
| 2183 | const status = yield* driver.statusDetails(worktreePath); | 2304 | const status = yield* driver.statusDetails(worktreePath); |
| 2184 | assert.equal(status.aheadCount, 0); | 2305 | assert.equal(status.aheadCount, 0); |
| 2185 | assert.equal(status.aheadOfDefaultCount, 0); | 2306 | assert.equal(status.aheadOfDefaultCount, 0); |
| | 2307 | |
| | 2308 | yield* driver.fetchRemote({ cwd, remoteName: "origin", refName: "local-only" }); |
| | 2309 | assert.isTrue( |
| | 2310 | yield* driver.remoteBranchExists({ cwd, remoteName: "origin", refName: "unrelated" }), |
| | 2311 | ); |
| 2186 | }), | 2312 | }), |
| 2187 | ); | 2313 | ); |
| 2188 | | 2314 | |