| ... | ... | @@ -2,7 +2,7 @@ import { existsSync } from "node:fs"; |
| 2 | 2 | import { mkdtemp, rm, writeFile } from "node:fs/promises"; |
| 3 | 3 | import { tmpdir } from "node:os"; |
| 4 | 4 | import path from "node:path"; |
| 5 | | import { afterEach, beforeEach, describe, expect, it } from "vitest"; |
| 5 | import { describe, expect, it } from "vitest"; |
| 6 | 6 | |
| 7 | 7 | import { |
| 8 | 8 | checkoutGitBranch, |
| ... | ... | @@ -29,6 +29,17 @@ async function git(cwd: string, command: string): Promise<string> { |
| 29 | 29 | return result.stdout.trim(); |
| 30 | 30 | } |
| 31 | 31 | |
| 32 | /** Create a disposable temp directory that cleans up automatically. */ |
| 33 | async function makeTmpDir() { |
| 34 | const dir = await mkdtemp(path.join(tmpdir(), "git-test-")); |
| 35 | return { |
| 36 | path: dir, |
| 37 | [Symbol.asyncDispose]: async () => { |
| 38 | await rm(dir, { recursive: true, force: true }); |
| 39 | }, |
| 40 | }; |
| 41 | } |
| 42 | |
| 32 | 43 | /** Create a repo with an initial commit so branches work. */ |
| 33 | 44 | async function initRepoWithCommit(cwd: string): Promise<void> { |
| 34 | 45 | await initGitRepo({ cwd }); |
| ... | ... | @@ -42,27 +53,19 @@ async function initRepoWithCommit(cwd: string): Promise<void> { |
| 42 | 53 | // ── Tests ── |
| 43 | 54 | |
| 44 | 55 | describe("git integration", () => { |
| 45 | | let tmpDir: string; |
| 46 | | |
| 47 | | beforeEach(async () => { |
| 48 | | tmpDir = await mkdtemp(path.join(tmpdir(), "git-test-")); |
| 49 | | }); |
| 50 | | |
| 51 | | afterEach(async () => { |
| 52 | | await rm(tmpDir, { recursive: true, force: true }); |
| 53 | | }); |
| 54 | | |
| 55 | 56 | // ── initGitRepo ── |
| 56 | 57 | |
| 57 | 58 | describe("initGitRepo", () => { |
| 58 | 59 | it("creates a valid git repo", async () => { |
| 59 | | await initGitRepo({ cwd: tmpDir }); |
| 60 | | expect(existsSync(path.join(tmpDir, ".git"))).toBe(true); |
| 60 | await using tmp = await makeTmpDir(); |
| 61 | await initGitRepo({ cwd: tmp.path }); |
| 62 | expect(existsSync(path.join(tmp.path, ".git"))).toBe(true); |
| 61 | 63 | }); |
| 62 | 64 | |
| 63 | 65 | it("listGitBranches reports isRepo: true after init + commit", async () => { |
| 64 | | await initRepoWithCommit(tmpDir); |
| 65 | | const result = await listGitBranches({ cwd: tmpDir }); |
| 66 | await using tmp = await makeTmpDir(); |
| 67 | await initRepoWithCommit(tmp.path); |
| 68 | const result = await listGitBranches({ cwd: tmp.path }); |
| 66 | 69 | expect(result.isRepo).toBe(true); |
| 67 | 70 | expect(result.branches.length).toBeGreaterThanOrEqual(1); |
| 68 | 71 | }); |
| ... | ... | @@ -72,42 +75,47 @@ describe("git integration", () => { |
| 72 | 75 | |
| 73 | 76 | describe("listGitBranches", () => { |
| 74 | 77 | it("returns isRepo: false for non-git directory", async () => { |
| 75 | | const result = await listGitBranches({ cwd: tmpDir }); |
| 78 | await using tmp = await makeTmpDir(); |
| 79 | const result = await listGitBranches({ cwd: tmp.path }); |
| 76 | 80 | expect(result.isRepo).toBe(false); |
| 77 | 81 | expect(result.branches).toEqual([]); |
| 78 | 82 | }); |
| 79 | 83 | |
| 80 | 84 | it("returns the current branch with current: true", async () => { |
| 81 | | await initRepoWithCommit(tmpDir); |
| 82 | | const result = await listGitBranches({ cwd: tmpDir }); |
| 85 | await using tmp = await makeTmpDir(); |
| 86 | await initRepoWithCommit(tmp.path); |
| 87 | const result = await listGitBranches({ cwd: tmp.path }); |
| 83 | 88 | const current = result.branches.find((b) => b.current); |
| 84 | 89 | expect(current).toBeDefined(); |
| 85 | 90 | expect(current!.current).toBe(true); |
| 86 | 91 | }); |
| 87 | 92 | |
| 88 | 93 | it("sorts current branch first", async () => { |
| 89 | | await initRepoWithCommit(tmpDir); |
| 90 | | await createGitBranch({ cwd: tmpDir, branch: "aaa-first-alpha" }); |
| 91 | | await createGitBranch({ cwd: tmpDir, branch: "zzz-last" }); |
| 94 | await using tmp = await makeTmpDir(); |
| 95 | await initRepoWithCommit(tmp.path); |
| 96 | await createGitBranch({ cwd: tmp.path, branch: "aaa-first-alpha" }); |
| 97 | await createGitBranch({ cwd: tmp.path, branch: "zzz-last" }); |
| 92 | 98 | |
| 93 | | const result = await listGitBranches({ cwd: tmpDir }); |
| 99 | const result = await listGitBranches({ cwd: tmp.path }); |
| 94 | 100 | expect(result.branches[0]!.current).toBe(true); |
| 95 | 101 | }); |
| 96 | 102 | |
| 97 | 103 | it("lists multiple branches after creating them", async () => { |
| 98 | | await initRepoWithCommit(tmpDir); |
| 99 | | await createGitBranch({ cwd: tmpDir, branch: "feature-a" }); |
| 100 | | await createGitBranch({ cwd: tmpDir, branch: "feature-b" }); |
| 104 | await using tmp = await makeTmpDir(); |
| 105 | await initRepoWithCommit(tmp.path); |
| 106 | await createGitBranch({ cwd: tmp.path, branch: "feature-a" }); |
| 107 | await createGitBranch({ cwd: tmp.path, branch: "feature-b" }); |
| 101 | 108 | |
| 102 | | const result = await listGitBranches({ cwd: tmpDir }); |
| 109 | const result = await listGitBranches({ cwd: tmp.path }); |
| 103 | 110 | const names = result.branches.map((b) => b.name); |
| 104 | 111 | expect(names).toContain("feature-a"); |
| 105 | 112 | expect(names).toContain("feature-b"); |
| 106 | 113 | }); |
| 107 | 114 | |
| 108 | 115 | it("isDefault is false when no remote exists", async () => { |
| 109 | | await initRepoWithCommit(tmpDir); |
| 110 | | const result = await listGitBranches({ cwd: tmpDir }); |
| 116 | await using tmp = await makeTmpDir(); |
| 117 | await initRepoWithCommit(tmp.path); |
| 118 | const result = await listGitBranches({ cwd: tmp.path }); |
| 111 | 119 | expect(result.branches.every((b) => b.isDefault === false)).toBe(true); |
| 112 | 120 | }); |
| 113 | 121 | }); |
| ... | ... | @@ -116,57 +124,50 @@ describe("git integration", () => { |
| 116 | 124 | |
| 117 | 125 | describe("checkoutGitBranch", () => { |
| 118 | 126 | it("checks out an existing branch", async () => { |
| 119 | | await initRepoWithCommit(tmpDir); |
| 120 | | await createGitBranch({ cwd: tmpDir, branch: "feature" }); |
| 127 | await using tmp = await makeTmpDir(); |
| 128 | await initRepoWithCommit(tmp.path); |
| 129 | await createGitBranch({ cwd: tmp.path, branch: "feature" }); |
| 121 | 130 | |
| 122 | | await checkoutGitBranch({ cwd: tmpDir, branch: "feature" }); |
| 131 | await checkoutGitBranch({ cwd: tmp.path, branch: "feature" }); |
| 123 | 132 | |
| 124 | | const result = await listGitBranches({ cwd: tmpDir }); |
| 133 | const result = await listGitBranches({ cwd: tmp.path }); |
| 125 | 134 | const current = result.branches.find((b) => b.current); |
| 126 | 135 | expect(current!.name).toBe("feature"); |
| 127 | 136 | }); |
| 128 | 137 | |
| 129 | 138 | it("throws when branch does not exist", async () => { |
| 130 | | await initRepoWithCommit(tmpDir); |
| 131 | | await expect( |
| 132 | | checkoutGitBranch({ cwd: tmpDir, branch: "nonexistent" }), |
| 133 | | ).rejects.toThrow(); |
| 139 | await using tmp = await makeTmpDir(); |
| 140 | await initRepoWithCommit(tmp.path); |
| 141 | await expect(checkoutGitBranch({ cwd: tmp.path, branch: "nonexistent" })).rejects.toThrow(); |
| 134 | 142 | }); |
| 135 | 143 | |
| 136 | 144 | it("throws when checkout would overwrite uncommitted changes", async () => { |
| 137 | | await initRepoWithCommit(tmpDir); |
| 138 | | await createGitBranch({ cwd: tmpDir, branch: "other" }); |
| 145 | await using tmp = await makeTmpDir(); |
| 146 | await initRepoWithCommit(tmp.path); |
| 147 | await createGitBranch({ cwd: tmp.path, branch: "other" }); |
| 139 | 148 | |
| 140 | 149 | // Create a conflicting change: modify README on current branch |
| 141 | | await writeFile(path.join(tmpDir, "README.md"), "modified\n"); |
| 142 | | await git(tmpDir, "add README.md"); |
| 143 | | |
| 144 | | // On the other branch, README has original content — checkout should conflict |
| 145 | | // Actually git checkout with staged changes can sometimes work, so let's |
| 146 | | // create a real conflict: commit different content on other branch first. |
| 147 | | // Simpler: write to a tracked file without committing, then checkout a branch |
| 148 | | // where that file differs. |
| 150 | await writeFile(path.join(tmp.path, "README.md"), "modified\n"); |
| 151 | await git(tmp.path, "add README.md"); |
| 149 | 152 | |
| 150 | 153 | // First, checkout other branch cleanly |
| 151 | | await git(tmpDir, "stash"); |
| 152 | | await checkoutGitBranch({ cwd: tmpDir, branch: "other" }); |
| 153 | | await writeFile(path.join(tmpDir, "README.md"), "other content\n"); |
| 154 | | await git(tmpDir, "add ."); |
| 155 | | await git(tmpDir, "commit -m 'other change'"); |
| 154 | await git(tmp.path, "stash"); |
| 155 | await checkoutGitBranch({ cwd: tmp.path, branch: "other" }); |
| 156 | await writeFile(path.join(tmp.path, "README.md"), "other content\n"); |
| 157 | await git(tmp.path, "add ."); |
| 158 | await git(tmp.path, "commit -m 'other change'"); |
| 156 | 159 | |
| 157 | 160 | // Go back to default branch |
| 158 | | const defaultBranch = ( |
| 159 | | await listGitBranches({ cwd: tmpDir }) |
| 160 | | ).branches.find((b) => !b.current)!.name; |
| 161 | | await checkoutGitBranch({ cwd: tmpDir, branch: defaultBranch }); |
| 161 | const defaultBranch = (await listGitBranches({ cwd: tmp.path })).branches.find( |
| 162 | (b) => !b.current, |
| 163 | )!.name; |
| 164 | await checkoutGitBranch({ cwd: tmp.path, branch: defaultBranch }); |
| 162 | 165 | |
| 163 | 166 | // Make uncommitted changes to the same file |
| 164 | | await writeFile(path.join(tmpDir, "README.md"), "conflicting local\n"); |
| 167 | await writeFile(path.join(tmp.path, "README.md"), "conflicting local\n"); |
| 165 | 168 | |
| 166 | 169 | // Checkout should fail due to uncommitted changes |
| 167 | | await expect( |
| 168 | | checkoutGitBranch({ cwd: tmpDir, branch: "other" }), |
| 169 | | ).rejects.toThrow(); |
| 170 | await expect(checkoutGitBranch({ cwd: tmp.path, branch: "other" })).rejects.toThrow(); |
| 170 | 171 | }); |
| 171 | 172 | }); |
| 172 | 173 | |
| ... | ... | @@ -174,19 +175,19 @@ describe("git integration", () => { |
| 174 | 175 | |
| 175 | 176 | describe("createGitBranch", () => { |
| 176 | 177 | it("creates a new branch visible in listGitBranches", async () => { |
| 177 | | await initRepoWithCommit(tmpDir); |
| 178 | | await createGitBranch({ cwd: tmpDir, branch: "new-feature" }); |
| 178 | await using tmp = await makeTmpDir(); |
| 179 | await initRepoWithCommit(tmp.path); |
| 180 | await createGitBranch({ cwd: tmp.path, branch: "new-feature" }); |
| 179 | 181 | |
| 180 | | const result = await listGitBranches({ cwd: tmpDir }); |
| 182 | const result = await listGitBranches({ cwd: tmp.path }); |
| 181 | 183 | expect(result.branches.some((b) => b.name === "new-feature")).toBe(true); |
| 182 | 184 | }); |
| 183 | 185 | |
| 184 | 186 | it("throws when branch already exists", async () => { |
| 185 | | await initRepoWithCommit(tmpDir); |
| 186 | | await createGitBranch({ cwd: tmpDir, branch: "dupe" }); |
| 187 | | await expect( |
| 188 | | createGitBranch({ cwd: tmpDir, branch: "dupe" }), |
| 189 | | ).rejects.toThrow(); |
| 187 | await using tmp = await makeTmpDir(); |
| 188 | await initRepoWithCommit(tmp.path); |
| 189 | await createGitBranch({ cwd: tmp.path, branch: "dupe" }); |
| 190 | await expect(createGitBranch({ cwd: tmp.path, branch: "dupe" })).rejects.toThrow(); |
| 190 | 191 | }); |
| 191 | 192 | }); |
| 192 | 193 | |
| ... | ... | @@ -194,12 +195,13 @@ describe("git integration", () => { |
| 194 | 195 | |
| 195 | 196 | describe("createGitWorktree", () => { |
| 196 | 197 | it("creates a worktree directory at the expected path", async () => { |
| 197 | | await initRepoWithCommit(tmpDir); |
| 198 | | await createGitBranch({ cwd: tmpDir, branch: "wt-branch" }); |
| 198 | await using tmp = await makeTmpDir(); |
| 199 | await initRepoWithCommit(tmp.path); |
| 200 | await createGitBranch({ cwd: tmp.path, branch: "wt-branch" }); |
| 199 | 201 | |
| 200 | | const wtPath = path.join(tmpDir, "worktree-out"); |
| 202 | const wtPath = path.join(tmp.path, "worktree-out"); |
| 201 | 203 | const result = await createGitWorktree({ |
| 202 | | cwd: tmpDir, |
| 204 | cwd: tmp.path, |
| 203 | 205 | branch: "wt-branch", |
| 204 | 206 | path: wtPath, |
| 205 | 207 | }); |
| ... | ... | @@ -209,17 +211,18 @@ describe("git integration", () => { |
| 209 | 211 | expect(existsSync(wtPath)).toBe(true); |
| 210 | 212 | expect(existsSync(path.join(wtPath, "README.md"))).toBe(true); |
| 211 | 213 | |
| 212 | | // Clean up worktree |
| 213 | | await removeGitWorktree({ cwd: tmpDir, path: wtPath }); |
| 214 | // Clean up worktree before tmp dir disposal |
| 215 | await removeGitWorktree({ cwd: tmp.path, path: wtPath }); |
| 214 | 216 | }); |
| 215 | 217 | |
| 216 | 218 | it("worktree has the correct branch checked out", async () => { |
| 217 | | await initRepoWithCommit(tmpDir); |
| 218 | | await createGitBranch({ cwd: tmpDir, branch: "wt-check" }); |
| 219 | await using tmp = await makeTmpDir(); |
| 220 | await initRepoWithCommit(tmp.path); |
| 221 | await createGitBranch({ cwd: tmp.path, branch: "wt-check" }); |
| 219 | 222 | |
| 220 | | const wtPath = path.join(tmpDir, "wt-check-dir"); |
| 223 | const wtPath = path.join(tmp.path, "wt-check-dir"); |
| 221 | 224 | await createGitWorktree({ |
| 222 | | cwd: tmpDir, |
| 225 | cwd: tmp.path, |
| 223 | 226 | branch: "wt-check", |
| 224 | 227 | path: wtPath, |
| 225 | 228 | }); |
| ... | ... | @@ -228,19 +231,20 @@ describe("git integration", () => { |
| 228 | 231 | const branchOutput = await git(wtPath, "branch --show-current"); |
| 229 | 232 | expect(branchOutput).toBe("wt-check"); |
| 230 | 233 | |
| 231 | | await removeGitWorktree({ cwd: tmpDir, path: wtPath }); |
| 234 | await removeGitWorktree({ cwd: tmp.path, path: wtPath }); |
| 232 | 235 | }); |
| 233 | 236 | |
| 234 | 237 | it("throws when branch is already checked out in main worktree", async () => { |
| 235 | | await initRepoWithCommit(tmpDir); |
| 238 | await using tmp = await makeTmpDir(); |
| 239 | await initRepoWithCommit(tmp.path); |
| 236 | 240 | // Try to create a worktree for the current branch (already checked out) |
| 237 | | const branches = await listGitBranches({ cwd: tmpDir }); |
| 241 | const branches = await listGitBranches({ cwd: tmp.path }); |
| 238 | 242 | const currentBranch = branches.branches.find((b) => b.current)!.name; |
| 239 | 243 | |
| 240 | | const wtPath = path.join(tmpDir, "wt-conflict"); |
| 244 | const wtPath = path.join(tmp.path, "wt-conflict"); |
| 241 | 245 | await expect( |
| 242 | 246 | createGitWorktree({ |
| 243 | | cwd: tmpDir, |
| 247 | cwd: tmp.path, |
| 244 | 248 | branch: currentBranch, |
| 245 | 249 | path: wtPath, |
| 246 | 250 | }), |
| ... | ... | @@ -248,18 +252,19 @@ describe("git integration", () => { |
| 248 | 252 | }); |
| 249 | 253 | |
| 250 | 254 | it("removeGitWorktree cleans up the worktree", async () => { |
| 251 | | await initRepoWithCommit(tmpDir); |
| 252 | | await createGitBranch({ cwd: tmpDir, branch: "wt-remove" }); |
| 255 | await using tmp = await makeTmpDir(); |
| 256 | await initRepoWithCommit(tmp.path); |
| 257 | await createGitBranch({ cwd: tmp.path, branch: "wt-remove" }); |
| 253 | 258 | |
| 254 | | const wtPath = path.join(tmpDir, "wt-remove-dir"); |
| 259 | const wtPath = path.join(tmp.path, "wt-remove-dir"); |
| 255 | 260 | await createGitWorktree({ |
| 256 | | cwd: tmpDir, |
| 261 | cwd: tmp.path, |
| 257 | 262 | branch: "wt-remove", |
| 258 | 263 | path: wtPath, |
| 259 | 264 | }); |
| 260 | 265 | expect(existsSync(wtPath)).toBe(true); |
| 261 | 266 | |
| 262 | | await removeGitWorktree({ cwd: tmpDir, path: wtPath }); |
| 267 | await removeGitWorktree({ cwd: tmp.path, path: wtPath }); |
| 263 | 268 | expect(existsSync(wtPath)).toBe(false); |
| 264 | 269 | }); |
| 265 | 270 | }); |
| ... | ... | @@ -268,11 +273,12 @@ describe("git integration", () => { |
| 268 | 273 | |
| 269 | 274 | describe("full flow: local branch checkout", () => { |
| 270 | 275 | it("init → commit → create branch → checkout → verify current", async () => { |
| 271 | | await initRepoWithCommit(tmpDir); |
| 272 | | await createGitBranch({ cwd: tmpDir, branch: "feature-login" }); |
| 273 | | await checkoutGitBranch({ cwd: tmpDir, branch: "feature-login" }); |
| 276 | await using tmp = await makeTmpDir(); |
| 277 | await initRepoWithCommit(tmp.path); |
| 278 | await createGitBranch({ cwd: tmp.path, branch: "feature-login" }); |
| 279 | await checkoutGitBranch({ cwd: tmp.path, branch: "feature-login" }); |
| 274 | 280 | |
| 275 | | const result = await listGitBranches({ cwd: tmpDir }); |
| 281 | const result = await listGitBranches({ cwd: tmp.path }); |
| 276 | 282 | const current = result.branches.find((b) => b.current); |
| 277 | 283 | expect(current!.name).toBe("feature-login"); |
| 278 | 284 | }); |
| ... | ... | @@ -282,12 +288,13 @@ describe("git integration", () => { |
| 282 | 288 | |
| 283 | 289 | describe("full flow: worktree creation", () => { |
| 284 | 290 | it("creates worktree from a non-current branch", async () => { |
| 285 | | await initRepoWithCommit(tmpDir); |
| 286 | | await createGitBranch({ cwd: tmpDir, branch: "feature-wt" }); |
| 291 | await using tmp = await makeTmpDir(); |
| 292 | await initRepoWithCommit(tmp.path); |
| 293 | await createGitBranch({ cwd: tmp.path, branch: "feature-wt" }); |
| 287 | 294 | |
| 288 | | const wtPath = path.join(tmpDir, "my-worktree"); |
| 295 | const wtPath = path.join(tmp.path, "my-worktree"); |
| 289 | 296 | const result = await createGitWorktree({ |
| 290 | | cwd: tmpDir, |
| 297 | cwd: tmp.path, |
| 291 | 298 | branch: "feature-wt", |
| 292 | 299 | path: wtPath, |
| 293 | 300 | }); |
| ... | ... | @@ -296,7 +303,7 @@ describe("git integration", () => { |
| 296 | 303 | expect(existsSync(result.worktree.path)).toBe(true); |
| 297 | 304 | |
| 298 | 305 | // Main repo still on original branch |
| 299 | | const mainBranches = await listGitBranches({ cwd: tmpDir }); |
| 306 | const mainBranches = await listGitBranches({ cwd: tmp.path }); |
| 300 | 307 | const mainCurrent = mainBranches.branches.find((b) => b.current); |
| 301 | 308 | expect(mainCurrent!.name).not.toBe("feature-wt"); |
| 302 | 309 | |
| ... | ... | @@ -304,7 +311,7 @@ describe("git integration", () => { |
| 304 | 311 | const wtBranch = await git(wtPath, "branch --show-current"); |
| 305 | 312 | expect(wtBranch).toBe("feature-wt"); |
| 306 | 313 | |
| 307 | | await removeGitWorktree({ cwd: tmpDir, path: wtPath }); |
| 314 | await removeGitWorktree({ cwd: tmp.path, path: wtPath }); |
| 308 | 315 | }); |
| 309 | 316 | }); |
| 310 | 317 | |
| ... | ... | @@ -312,23 +319,24 @@ describe("git integration", () => { |
| 312 | 319 | |
| 313 | 320 | describe("full flow: thread switching (checkout toggling)", () => { |
| 314 | 321 | it("checkout a → checkout b → checkout a → current matches", async () => { |
| 315 | | await initRepoWithCommit(tmpDir); |
| 316 | | await createGitBranch({ cwd: tmpDir, branch: "branch-a" }); |
| 317 | | await createGitBranch({ cwd: tmpDir, branch: "branch-b" }); |
| 322 | await using tmp = await makeTmpDir(); |
| 323 | await initRepoWithCommit(tmp.path); |
| 324 | await createGitBranch({ cwd: tmp.path, branch: "branch-a" }); |
| 325 | await createGitBranch({ cwd: tmp.path, branch: "branch-b" }); |
| 318 | 326 | |
| 319 | 327 | // Simulate switching to thread A's branch |
| 320 | | await checkoutGitBranch({ cwd: tmpDir, branch: "branch-a" }); |
| 321 | | let branches = await listGitBranches({ cwd: tmpDir }); |
| 328 | await checkoutGitBranch({ cwd: tmp.path, branch: "branch-a" }); |
| 329 | let branches = await listGitBranches({ cwd: tmp.path }); |
| 322 | 330 | expect(branches.branches.find((b) => b.current)!.name).toBe("branch-a"); |
| 323 | 331 | |
| 324 | 332 | // Simulate switching to thread B's branch |
| 325 | | await checkoutGitBranch({ cwd: tmpDir, branch: "branch-b" }); |
| 326 | | branches = await listGitBranches({ cwd: tmpDir }); |
| 333 | await checkoutGitBranch({ cwd: tmp.path, branch: "branch-b" }); |
| 334 | branches = await listGitBranches({ cwd: tmp.path }); |
| 327 | 335 | expect(branches.branches.find((b) => b.current)!.name).toBe("branch-b"); |
| 328 | 336 | |
| 329 | 337 | // Switch back to thread A |
| 330 | | await checkoutGitBranch({ cwd: tmpDir, branch: "branch-a" }); |
| 331 | | branches = await listGitBranches({ cwd: tmpDir }); |
| 338 | await checkoutGitBranch({ cwd: tmp.path, branch: "branch-a" }); |
| 339 | branches = await listGitBranches({ cwd: tmp.path }); |
| 332 | 340 | expect(branches.branches.find((b) => b.current)!.name).toBe("branch-a"); |
| 333 | 341 | }); |
| 334 | 342 | }); |
| ... | ... | @@ -337,42 +345,37 @@ describe("git integration", () => { |
| 337 | 345 | |
| 338 | 346 | describe("full flow: checkout conflict", () => { |
| 339 | 347 | it("uncommitted changes prevent checkout to a diverged branch", async () => { |
| 340 | | await initRepoWithCommit(tmpDir); |
| 341 | | await createGitBranch({ cwd: tmpDir, branch: "diverged" }); |
| 348 | await using tmp = await makeTmpDir(); |
| 349 | await initRepoWithCommit(tmp.path); |
| 350 | await createGitBranch({ cwd: tmp.path, branch: "diverged" }); |
| 342 | 351 | |
| 343 | 352 | // Make diverged branch have different file content |
| 344 | | await checkoutGitBranch({ cwd: tmpDir, branch: "diverged" }); |
| 345 | | await writeFile(path.join(tmpDir, "README.md"), "diverged content\n"); |
| 346 | | await git(tmpDir, "add ."); |
| 347 | | await git(tmpDir, "commit -m 'diverge'"); |
| 353 | await checkoutGitBranch({ cwd: tmp.path, branch: "diverged" }); |
| 354 | await writeFile(path.join(tmp.path, "README.md"), "diverged content\n"); |
| 355 | await git(tmp.path, "add ."); |
| 356 | await git(tmp.path, "commit -m 'diverge'"); |
| 348 | 357 | |
| 349 | 358 | // Go back to default branch |
| 350 | | const defaultBranch = ( |
| 351 | | await git(tmpDir, "rev-parse --abbrev-ref HEAD") |
| 352 | | ).includes("diverged") |
| 359 | const defaultBranch = (await git(tmp.path, "rev-parse --abbrev-ref HEAD")).includes("diverged") |
| 353 | 360 | ? // we're on diverged, need to find the other one |
| 354 | | (await listGitBranches({ cwd: tmpDir })).branches.find( |
| 361 | (await listGitBranches({ cwd: tmp.path })).branches.find( |
| 355 | 362 | (b) => !b.current && b.name !== "diverged", |
| 356 | 363 | )!.name |
| 357 | | : (await git(tmpDir, "rev-parse --abbrev-ref HEAD")); |
| 364 | : await git(tmp.path, "rev-parse --abbrev-ref HEAD"); |
| 358 | 365 | |
| 359 | 366 | // Actually, let's just get back to the initial branch explicitly |
| 360 | | const allBranches = await listGitBranches({ cwd: tmpDir }); |
| 361 | | const initialBranch = allBranches.branches.find( |
| 362 | | (b) => b.name !== "diverged", |
| 363 | | )!.name; |
| 364 | | await checkoutGitBranch({ cwd: tmpDir, branch: initialBranch }); |
| 367 | const allBranches = await listGitBranches({ cwd: tmp.path }); |
| 368 | const initialBranch = allBranches.branches.find((b) => b.name !== "diverged")!.name; |
| 369 | await checkoutGitBranch({ cwd: tmp.path, branch: initialBranch }); |
| 365 | 370 | |
| 366 | 371 | // Make local uncommitted changes to the same file |
| 367 | | await writeFile(path.join(tmpDir, "README.md"), "local uncommitted\n"); |
| 372 | await writeFile(path.join(tmp.path, "README.md"), "local uncommitted\n"); |
| 368 | 373 | |
| 369 | 374 | // Attempt checkout should fail |
| 370 | | await expect( |
| 371 | | checkoutGitBranch({ cwd: tmpDir, branch: "diverged" }), |
| 372 | | ).rejects.toThrow(); |
| 375 | await expect(checkoutGitBranch({ cwd: tmp.path, branch: "diverged" })).rejects.toThrow(); |
| 373 | 376 | |
| 374 | 377 | // Current branch should still be the initial one |
| 375 | | const result = await listGitBranches({ cwd: tmpDir }); |
| 378 | const result = await listGitBranches({ cwd: tmp.path }); |
| 376 | 379 | expect(result.branches.find((b) => b.current)!.name).toBe(initialBranch); |
| 377 | 380 | }); |
| 378 | 381 | }); |