authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-09 11:00:24-08:00
committergravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-11 09:00:18-08:00
log7e201486f15575d587890dbe1fd637a5f552da91
treeba485a94d8a26ea42d0f1c51173ef401b28c62e2
parentf47dc3c78ca9496b80c4b4a2537c74bbd697fd9a

resource management


2 files changed, 131 insertions(+), 128 deletions(-)

apps/desktop/src/git.test.ts+130-127
...@@ -2,7 +2,7 @@ import { existsSync } from "node:fs";...@@ -2,7 +2,7 @@ import { existsSync } from "node:fs";
2import { mkdtemp, rm, writeFile } from "node:fs/promises";2import { mkdtemp, rm, writeFile } from "node:fs/promises";
3import { tmpdir } from "node:os";3import { tmpdir } from "node:os";
4import path from "node:path";4import path from "node:path";
5import { afterEach, beforeEach, describe, expect, it } from "vitest";5import { describe, expect, it } from "vitest";
66
7import {7import {
8 checkoutGitBranch,8 checkoutGitBranch,
...@@ -29,6 +29,17 @@ async function git(cwd: string, command: string): Promise<string> {...@@ -29,6 +29,17 @@ async function git(cwd: string, command: string): Promise<string> {
29 return result.stdout.trim();29 return result.stdout.trim();
30}30}
3131
32/** Create a disposable temp directory that cleans up automatically. */
33async 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/** Create a repo with an initial commit so branches work. */43/** Create a repo with an initial commit so branches work. */
33async function initRepoWithCommit(cwd: string): Promise<void> {44async function initRepoWithCommit(cwd: string): Promise<void> {
34 await initGitRepo({ cwd });45 await initGitRepo({ cwd });
...@@ -42,27 +53,19 @@ async function initRepoWithCommit(cwd: string): Promise<void> {...@@ -42,27 +53,19 @@ async function initRepoWithCommit(cwd: string): Promise<void> {
42// ── Tests ──53// ── Tests ──
4354
44describe("git integration", () => {55describe("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 // ── initGitRepo ──56 // ── initGitRepo ──
5657
57 describe("initGitRepo", () => {58 describe("initGitRepo", () => {
58 it("creates a valid git repo", async () => {59 it("creates a valid git repo", async () => {
59 await initGitRepo({ cwd: tmpDir });60 await using tmp = await makeTmpDir();
60 expect(existsSync(path.join(tmpDir, ".git"))).toBe(true);61 await initGitRepo({ cwd: tmp.path });
62 expect(existsSync(path.join(tmp.path, ".git"))).toBe(true);
61 });63 });
6264
63 it("listGitBranches reports isRepo: true after init + commit", async () => {65 it("listGitBranches reports isRepo: true after init + commit", async () => {
64 await initRepoWithCommit(tmpDir);66 await using tmp = await makeTmpDir();
65 const result = await listGitBranches({ cwd: tmpDir });67 await initRepoWithCommit(tmp.path);
68 const result = await listGitBranches({ cwd: tmp.path });
66 expect(result.isRepo).toBe(true);69 expect(result.isRepo).toBe(true);
67 expect(result.branches.length).toBeGreaterThanOrEqual(1);70 expect(result.branches.length).toBeGreaterThanOrEqual(1);
68 });71 });
...@@ -72,42 +75,47 @@ describe("git integration", () => {...@@ -72,42 +75,47 @@ describe("git integration", () => {
7275
73 describe("listGitBranches", () => {76 describe("listGitBranches", () => {
74 it("returns isRepo: false for non-git directory", async () => {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 expect(result.isRepo).toBe(false);80 expect(result.isRepo).toBe(false);
77 expect(result.branches).toEqual([]);81 expect(result.branches).toEqual([]);
78 });82 });
7983
80 it("returns the current branch with current: true", async () => {84 it("returns the current branch with current: true", async () => {
81 await initRepoWithCommit(tmpDir);85 await using tmp = await makeTmpDir();
82 const result = await listGitBranches({ cwd: tmpDir });86 await initRepoWithCommit(tmp.path);
87 const result = await listGitBranches({ cwd: tmp.path });
83 const current = result.branches.find((b) => b.current);88 const current = result.branches.find((b) => b.current);
84 expect(current).toBeDefined();89 expect(current).toBeDefined();
85 expect(current!.current).toBe(true);90 expect(current!.current).toBe(true);
86 });91 });
8792
88 it("sorts current branch first", async () => {93 it("sorts current branch first", async () => {
89 await initRepoWithCommit(tmpDir);94 await using tmp = await makeTmpDir();
90 await createGitBranch({ cwd: tmpDir, branch: "aaa-first-alpha" });95 await initRepoWithCommit(tmp.path);
91 await createGitBranch({ cwd: tmpDir, branch: "zzz-last" });96 await createGitBranch({ cwd: tmp.path, branch: "aaa-first-alpha" });
97 await createGitBranch({ cwd: tmp.path, branch: "zzz-last" });
9298
93 const result = await listGitBranches({ cwd: tmpDir });99 const result = await listGitBranches({ cwd: tmp.path });
94 expect(result.branches[0]!.current).toBe(true);100 expect(result.branches[0]!.current).toBe(true);
95 });101 });
96102
97 it("lists multiple branches after creating them", async () => {103 it("lists multiple branches after creating them", async () => {
98 await initRepoWithCommit(tmpDir);104 await using tmp = await makeTmpDir();
99 await createGitBranch({ cwd: tmpDir, branch: "feature-a" });105 await initRepoWithCommit(tmp.path);
100 await createGitBranch({ cwd: tmpDir, branch: "feature-b" });106 await createGitBranch({ cwd: tmp.path, branch: "feature-a" });
107 await createGitBranch({ cwd: tmp.path, branch: "feature-b" });
101108
102 const result = await listGitBranches({ cwd: tmpDir });109 const result = await listGitBranches({ cwd: tmp.path });
103 const names = result.branches.map((b) => b.name);110 const names = result.branches.map((b) => b.name);
104 expect(names).toContain("feature-a");111 expect(names).toContain("feature-a");
105 expect(names).toContain("feature-b");112 expect(names).toContain("feature-b");
106 });113 });
107114
108 it("isDefault is false when no remote exists", async () => {115 it("isDefault is false when no remote exists", async () => {
109 await initRepoWithCommit(tmpDir);116 await using tmp = await makeTmpDir();
110 const result = await listGitBranches({ cwd: tmpDir });117 await initRepoWithCommit(tmp.path);
118 const result = await listGitBranches({ cwd: tmp.path });
111 expect(result.branches.every((b) => b.isDefault === false)).toBe(true);119 expect(result.branches.every((b) => b.isDefault === false)).toBe(true);
112 });120 });
113 });121 });
...@@ -116,57 +124,50 @@ describe("git integration", () => {...@@ -116,57 +124,50 @@ describe("git integration", () => {
116124
117 describe("checkoutGitBranch", () => {125 describe("checkoutGitBranch", () => {
118 it("checks out an existing branch", async () => {126 it("checks out an existing branch", async () => {
119 await initRepoWithCommit(tmpDir);127 await using tmp = await makeTmpDir();
120 await createGitBranch({ cwd: tmpDir, branch: "feature" });128 await initRepoWithCommit(tmp.path);
129 await createGitBranch({ cwd: tmp.path, branch: "feature" });
121130
122 await checkoutGitBranch({ cwd: tmpDir, branch: "feature" });131 await checkoutGitBranch({ cwd: tmp.path, branch: "feature" });
123132
124 const result = await listGitBranches({ cwd: tmpDir });133 const result = await listGitBranches({ cwd: tmp.path });
125 const current = result.branches.find((b) => b.current);134 const current = result.branches.find((b) => b.current);
126 expect(current!.name).toBe("feature");135 expect(current!.name).toBe("feature");
127 });136 });
128137
129 it("throws when branch does not exist", async () => {138 it("throws when branch does not exist", async () => {
130 await initRepoWithCommit(tmpDir);139 await using tmp = await makeTmpDir();
131 await expect(140 await initRepoWithCommit(tmp.path);
132 checkoutGitBranch({ cwd: tmpDir, branch: "nonexistent" }),141 await expect(checkoutGitBranch({ cwd: tmp.path, branch: "nonexistent" })).rejects.toThrow();
133 ).rejects.toThrow();
134 });142 });
135143
136 it("throws when checkout would overwrite uncommitted changes", async () => {144 it("throws when checkout would overwrite uncommitted changes", async () => {
137 await initRepoWithCommit(tmpDir);145 await using tmp = await makeTmpDir();
138 await createGitBranch({ cwd: tmpDir, branch: "other" });146 await initRepoWithCommit(tmp.path);
147 await createGitBranch({ cwd: tmp.path, branch: "other" });
139148
140 // Create a conflicting change: modify README on current branch149 // Create a conflicting change: modify README on current branch
141 await writeFile(path.join(tmpDir, "README.md"), "modified\n");150 await writeFile(path.join(tmp.path, "README.md"), "modified\n");
142 await git(tmpDir, "add README.md");151 await git(tmp.path, "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.
149152
150 // First, checkout other branch cleanly153 // First, checkout other branch cleanly
151 await git(tmpDir, "stash");154 await git(tmp.path, "stash");
152 await checkoutGitBranch({ cwd: tmpDir, branch: "other" });155 await checkoutGitBranch({ cwd: tmp.path, branch: "other" });
153 await writeFile(path.join(tmpDir, "README.md"), "other content\n");156 await writeFile(path.join(tmp.path, "README.md"), "other content\n");
154 await git(tmpDir, "add .");157 await git(tmp.path, "add .");
155 await git(tmpDir, "commit -m 'other change'");158 await git(tmp.path, "commit -m 'other change'");
156159
157 // Go back to default branch160 // Go back to default branch
158 const defaultBranch = (161 const defaultBranch = (await listGitBranches({ cwd: tmp.path })).branches.find(
159 await listGitBranches({ cwd: tmpDir })162 (b) => !b.current,
160 ).branches.find((b) => !b.current)!.name;163 )!.name;
161 await checkoutGitBranch({ cwd: tmpDir, branch: defaultBranch });164 await checkoutGitBranch({ cwd: tmp.path, branch: defaultBranch });
162165
163 // Make uncommitted changes to the same file166 // 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");
165168
166 // Checkout should fail due to uncommitted changes169 // Checkout should fail due to uncommitted changes
167 await expect(170 await expect(checkoutGitBranch({ cwd: tmp.path, branch: "other" })).rejects.toThrow();
168 checkoutGitBranch({ cwd: tmpDir, branch: "other" }),
169 ).rejects.toThrow();
170 });171 });
171 });172 });
172173
...@@ -174,19 +175,19 @@ describe("git integration", () => {...@@ -174,19 +175,19 @@ describe("git integration", () => {
174175
175 describe("createGitBranch", () => {176 describe("createGitBranch", () => {
176 it("creates a new branch visible in listGitBranches", async () => {177 it("creates a new branch visible in listGitBranches", async () => {
177 await initRepoWithCommit(tmpDir);178 await using tmp = await makeTmpDir();
178 await createGitBranch({ cwd: tmpDir, branch: "new-feature" });179 await initRepoWithCommit(tmp.path);
180 await createGitBranch({ cwd: tmp.path, branch: "new-feature" });
179181
180 const result = await listGitBranches({ cwd: tmpDir });182 const result = await listGitBranches({ cwd: tmp.path });
181 expect(result.branches.some((b) => b.name === "new-feature")).toBe(true);183 expect(result.branches.some((b) => b.name === "new-feature")).toBe(true);
182 });184 });
183185
184 it("throws when branch already exists", async () => {186 it("throws when branch already exists", async () => {
185 await initRepoWithCommit(tmpDir);187 await using tmp = await makeTmpDir();
186 await createGitBranch({ cwd: tmpDir, branch: "dupe" });188 await initRepoWithCommit(tmp.path);
187 await expect(189 await createGitBranch({ cwd: tmp.path, branch: "dupe" });
188 createGitBranch({ cwd: tmpDir, branch: "dupe" }),190 await expect(createGitBranch({ cwd: tmp.path, branch: "dupe" })).rejects.toThrow();
189 ).rejects.toThrow();
190 });191 });
191 });192 });
192193
...@@ -194,12 +195,13 @@ describe("git integration", () => {...@@ -194,12 +195,13 @@ describe("git integration", () => {
194195
195 describe("createGitWorktree", () => {196 describe("createGitWorktree", () => {
196 it("creates a worktree directory at the expected path", async () => {197 it("creates a worktree directory at the expected path", async () => {
197 await initRepoWithCommit(tmpDir);198 await using tmp = await makeTmpDir();
198 await createGitBranch({ cwd: tmpDir, branch: "wt-branch" });199 await initRepoWithCommit(tmp.path);
200 await createGitBranch({ cwd: tmp.path, branch: "wt-branch" });
199201
200 const wtPath = path.join(tmpDir, "worktree-out");202 const wtPath = path.join(tmp.path, "worktree-out");
201 const result = await createGitWorktree({203 const result = await createGitWorktree({
202 cwd: tmpDir,204 cwd: tmp.path,
203 branch: "wt-branch",205 branch: "wt-branch",
204 path: wtPath,206 path: wtPath,
205 });207 });
...@@ -209,17 +211,18 @@ describe("git integration", () => {...@@ -209,17 +211,18 @@ describe("git integration", () => {
209 expect(existsSync(wtPath)).toBe(true);211 expect(existsSync(wtPath)).toBe(true);
210 expect(existsSync(path.join(wtPath, "README.md"))).toBe(true);212 expect(existsSync(path.join(wtPath, "README.md"))).toBe(true);
211213
212 // Clean up worktree214 // Clean up worktree before tmp dir disposal
213 await removeGitWorktree({ cwd: tmpDir, path: wtPath });215 await removeGitWorktree({ cwd: tmp.path, path: wtPath });
214 });216 });
215217
216 it("worktree has the correct branch checked out", async () => {218 it("worktree has the correct branch checked out", async () => {
217 await initRepoWithCommit(tmpDir);219 await using tmp = await makeTmpDir();
218 await createGitBranch({ cwd: tmpDir, branch: "wt-check" });220 await initRepoWithCommit(tmp.path);
221 await createGitBranch({ cwd: tmp.path, branch: "wt-check" });
219222
220 const wtPath = path.join(tmpDir, "wt-check-dir");223 const wtPath = path.join(tmp.path, "wt-check-dir");
221 await createGitWorktree({224 await createGitWorktree({
222 cwd: tmpDir,225 cwd: tmp.path,
223 branch: "wt-check",226 branch: "wt-check",
224 path: wtPath,227 path: wtPath,
225 });228 });
...@@ -228,19 +231,20 @@ describe("git integration", () => {...@@ -228,19 +231,20 @@ describe("git integration", () => {
228 const branchOutput = await git(wtPath, "branch --show-current");231 const branchOutput = await git(wtPath, "branch --show-current");
229 expect(branchOutput).toBe("wt-check");232 expect(branchOutput).toBe("wt-check");
230233
231 await removeGitWorktree({ cwd: tmpDir, path: wtPath });234 await removeGitWorktree({ cwd: tmp.path, path: wtPath });
232 });235 });
233236
234 it("throws when branch is already checked out in main worktree", async () => {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 // Try to create a worktree for the current branch (already checked out)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 const currentBranch = branches.branches.find((b) => b.current)!.name;242 const currentBranch = branches.branches.find((b) => b.current)!.name;
239243
240 const wtPath = path.join(tmpDir, "wt-conflict");244 const wtPath = path.join(tmp.path, "wt-conflict");
241 await expect(245 await expect(
242 createGitWorktree({246 createGitWorktree({
243 cwd: tmpDir,247 cwd: tmp.path,
244 branch: currentBranch,248 branch: currentBranch,
245 path: wtPath,249 path: wtPath,
246 }),250 }),
...@@ -248,18 +252,19 @@ describe("git integration", () => {...@@ -248,18 +252,19 @@ describe("git integration", () => {
248 });252 });
249253
250 it("removeGitWorktree cleans up the worktree", async () => {254 it("removeGitWorktree cleans up the worktree", async () => {
251 await initRepoWithCommit(tmpDir);255 await using tmp = await makeTmpDir();
252 await createGitBranch({ cwd: tmpDir, branch: "wt-remove" });256 await initRepoWithCommit(tmp.path);
257 await createGitBranch({ cwd: tmp.path, branch: "wt-remove" });
253258
254 const wtPath = path.join(tmpDir, "wt-remove-dir");259 const wtPath = path.join(tmp.path, "wt-remove-dir");
255 await createGitWorktree({260 await createGitWorktree({
256 cwd: tmpDir,261 cwd: tmp.path,
257 branch: "wt-remove",262 branch: "wt-remove",
258 path: wtPath,263 path: wtPath,
259 });264 });
260 expect(existsSync(wtPath)).toBe(true);265 expect(existsSync(wtPath)).toBe(true);
261266
262 await removeGitWorktree({ cwd: tmpDir, path: wtPath });267 await removeGitWorktree({ cwd: tmp.path, path: wtPath });
263 expect(existsSync(wtPath)).toBe(false);268 expect(existsSync(wtPath)).toBe(false);
264 });269 });
265 });270 });
...@@ -268,11 +273,12 @@ describe("git integration", () => {...@@ -268,11 +273,12 @@ describe("git integration", () => {
268273
269 describe("full flow: local branch checkout", () => {274 describe("full flow: local branch checkout", () => {
270 it("init → commit → create branch → checkout → verify current", async () => {275 it("init → commit → create branch → checkout → verify current", async () => {
271 await initRepoWithCommit(tmpDir);276 await using tmp = await makeTmpDir();
272 await createGitBranch({ cwd: tmpDir, branch: "feature-login" });277 await initRepoWithCommit(tmp.path);
273 await checkoutGitBranch({ cwd: tmpDir, branch: "feature-login" });278 await createGitBranch({ cwd: tmp.path, branch: "feature-login" });
279 await checkoutGitBranch({ cwd: tmp.path, branch: "feature-login" });
274280
275 const result = await listGitBranches({ cwd: tmpDir });281 const result = await listGitBranches({ cwd: tmp.path });
276 const current = result.branches.find((b) => b.current);282 const current = result.branches.find((b) => b.current);
277 expect(current!.name).toBe("feature-login");283 expect(current!.name).toBe("feature-login");
278 });284 });
...@@ -282,12 +288,13 @@ describe("git integration", () => {...@@ -282,12 +288,13 @@ describe("git integration", () => {
282288
283 describe("full flow: worktree creation", () => {289 describe("full flow: worktree creation", () => {
284 it("creates worktree from a non-current branch", async () => {290 it("creates worktree from a non-current branch", async () => {
285 await initRepoWithCommit(tmpDir);291 await using tmp = await makeTmpDir();
286 await createGitBranch({ cwd: tmpDir, branch: "feature-wt" });292 await initRepoWithCommit(tmp.path);
293 await createGitBranch({ cwd: tmp.path, branch: "feature-wt" });
287294
288 const wtPath = path.join(tmpDir, "my-worktree");295 const wtPath = path.join(tmp.path, "my-worktree");
289 const result = await createGitWorktree({296 const result = await createGitWorktree({
290 cwd: tmpDir,297 cwd: tmp.path,
291 branch: "feature-wt",298 branch: "feature-wt",
292 path: wtPath,299 path: wtPath,
293 });300 });
...@@ -296,7 +303,7 @@ describe("git integration", () => {...@@ -296,7 +303,7 @@ describe("git integration", () => {
296 expect(existsSync(result.worktree.path)).toBe(true);303 expect(existsSync(result.worktree.path)).toBe(true);
297304
298 // Main repo still on original branch305 // Main repo still on original branch
299 const mainBranches = await listGitBranches({ cwd: tmpDir });306 const mainBranches = await listGitBranches({ cwd: tmp.path });
300 const mainCurrent = mainBranches.branches.find((b) => b.current);307 const mainCurrent = mainBranches.branches.find((b) => b.current);
301 expect(mainCurrent!.name).not.toBe("feature-wt");308 expect(mainCurrent!.name).not.toBe("feature-wt");
302309
...@@ -304,7 +311,7 @@ describe("git integration", () => {...@@ -304,7 +311,7 @@ describe("git integration", () => {
304 const wtBranch = await git(wtPath, "branch --show-current");311 const wtBranch = await git(wtPath, "branch --show-current");
305 expect(wtBranch).toBe("feature-wt");312 expect(wtBranch).toBe("feature-wt");
306313
307 await removeGitWorktree({ cwd: tmpDir, path: wtPath });314 await removeGitWorktree({ cwd: tmp.path, path: wtPath });
308 });315 });
309 });316 });
310317
...@@ -312,23 +319,24 @@ describe("git integration", () => {...@@ -312,23 +319,24 @@ describe("git integration", () => {
312319
313 describe("full flow: thread switching (checkout toggling)", () => {320 describe("full flow: thread switching (checkout toggling)", () => {
314 it("checkout a → checkout b → checkout a → current matches", async () => {321 it("checkout a → checkout b → checkout a → current matches", async () => {
315 await initRepoWithCommit(tmpDir);322 await using tmp = await makeTmpDir();
316 await createGitBranch({ cwd: tmpDir, branch: "branch-a" });323 await initRepoWithCommit(tmp.path);
317 await createGitBranch({ cwd: tmpDir, branch: "branch-b" });324 await createGitBranch({ cwd: tmp.path, branch: "branch-a" });
325 await createGitBranch({ cwd: tmp.path, branch: "branch-b" });
318326
319 // Simulate switching to thread A's branch327 // Simulate switching to thread A's branch
320 await checkoutGitBranch({ cwd: tmpDir, branch: "branch-a" });328 await checkoutGitBranch({ cwd: tmp.path, branch: "branch-a" });
321 let branches = await listGitBranches({ cwd: tmpDir });329 let branches = await listGitBranches({ cwd: tmp.path });
322 expect(branches.branches.find((b) => b.current)!.name).toBe("branch-a");330 expect(branches.branches.find((b) => b.current)!.name).toBe("branch-a");
323331
324 // Simulate switching to thread B's branch332 // Simulate switching to thread B's branch
325 await checkoutGitBranch({ cwd: tmpDir, branch: "branch-b" });333 await checkoutGitBranch({ cwd: tmp.path, branch: "branch-b" });
326 branches = await listGitBranches({ cwd: tmpDir });334 branches = await listGitBranches({ cwd: tmp.path });
327 expect(branches.branches.find((b) => b.current)!.name).toBe("branch-b");335 expect(branches.branches.find((b) => b.current)!.name).toBe("branch-b");
328336
329 // Switch back to thread A337 // Switch back to thread A
330 await checkoutGitBranch({ cwd: tmpDir, branch: "branch-a" });338 await checkoutGitBranch({ cwd: tmp.path, branch: "branch-a" });
331 branches = await listGitBranches({ cwd: tmpDir });339 branches = await listGitBranches({ cwd: tmp.path });
332 expect(branches.branches.find((b) => b.current)!.name).toBe("branch-a");340 expect(branches.branches.find((b) => b.current)!.name).toBe("branch-a");
333 });341 });
334 });342 });
...@@ -337,42 +345,37 @@ describe("git integration", () => {...@@ -337,42 +345,37 @@ describe("git integration", () => {
337345
338 describe("full flow: checkout conflict", () => {346 describe("full flow: checkout conflict", () => {
339 it("uncommitted changes prevent checkout to a diverged branch", async () => {347 it("uncommitted changes prevent checkout to a diverged branch", async () => {
340 await initRepoWithCommit(tmpDir);348 await using tmp = await makeTmpDir();
341 await createGitBranch({ cwd: tmpDir, branch: "diverged" });349 await initRepoWithCommit(tmp.path);
350 await createGitBranch({ cwd: tmp.path, branch: "diverged" });
342351
343 // Make diverged branch have different file content352 // Make diverged branch have different file content
344 await checkoutGitBranch({ cwd: tmpDir, branch: "diverged" });353 await checkoutGitBranch({ cwd: tmp.path, branch: "diverged" });
345 await writeFile(path.join(tmpDir, "README.md"), "diverged content\n");354 await writeFile(path.join(tmp.path, "README.md"), "diverged content\n");
346 await git(tmpDir, "add .");355 await git(tmp.path, "add .");
347 await git(tmpDir, "commit -m 'diverge'");356 await git(tmp.path, "commit -m 'diverge'");
348357
349 // Go back to default branch358 // Go back to default branch
350 const defaultBranch = (359 const defaultBranch = (await git(tmp.path, "rev-parse --abbrev-ref HEAD")).includes("diverged")
351 await git(tmpDir, "rev-parse --abbrev-ref HEAD")
352 ).includes("diverged")
353 ? // we're on diverged, need to find the other one360 ? // 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 (b) => !b.current && b.name !== "diverged",362 (b) => !b.current && b.name !== "diverged",
356 )!.name363 )!.name
357 : (await git(tmpDir, "rev-parse --abbrev-ref HEAD"));364 : await git(tmp.path, "rev-parse --abbrev-ref HEAD");
358365
359 // Actually, let's just get back to the initial branch explicitly366 // Actually, let's just get back to the initial branch explicitly
360 const allBranches = await listGitBranches({ cwd: tmpDir });367 const allBranches = await listGitBranches({ cwd: tmp.path });
361 const initialBranch = allBranches.branches.find(368 const initialBranch = allBranches.branches.find((b) => b.name !== "diverged")!.name;
362 (b) => b.name !== "diverged",369 await checkoutGitBranch({ cwd: tmp.path, branch: initialBranch });
363 )!.name;
364 await checkoutGitBranch({ cwd: tmpDir, branch: initialBranch });
365370
366 // Make local uncommitted changes to the same file371 // 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");
368373
369 // Attempt checkout should fail374 // Attempt checkout should fail
370 await expect(375 await expect(checkoutGitBranch({ cwd: tmp.path, branch: "diverged" })).rejects.toThrow();
371 checkoutGitBranch({ cwd: tmpDir, branch: "diverged" }),
372 ).rejects.toThrow();
373376
374 // Current branch should still be the initial one377 // Current branch should still be the initial one
375 const result = await listGitBranches({ cwd: tmpDir });378 const result = await listGitBranches({ cwd: tmp.path });
376 expect(result.branches.find((b) => b.current)!.name).toBe(initialBranch);379 expect(result.branches.find((b) => b.current)!.name).toBe(initialBranch);
377 });380 });
378 });381 });
apps/desktop/tsconfig.json+1-1
...@@ -3,7 +3,7 @@...@@ -3,7 +3,7 @@
3 "compilerOptions": {3 "compilerOptions": {
4 "composite": true,4 "composite": true,
5 "types": ["node", "electron"],5 "types": ["node", "electron"],
6 "lib": ["ES2023", "DOM"]6 "lib": ["ES2023", "DOM", "esnext.disposable"]
7 },7 },
8 "include": ["src", "tsup.config.ts"]8 "include": ["src", "tsup.config.ts"]
9}9}