From 6d43a8c5b05123d945ebd8995c5645818aff31cb Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:02:03 +0200 Subject: [PATCH] Restore selected changes from subdirectory workspaces (#6777) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(git): discard repository paths from the worktree root * test(git): cover discard from nested workspaces through daemon RPC * test(git): close the daemon before removing discard fixtures Windows CI reaches the discard assertions but cannot remove the observed repository while the test daemon is alive. Run fixture removal after the daemon's afterEach cleanup, preserving all behavioral assertions. Co-authored-by: 见安 <73627805+Mar-garet@users.noreply.github.com> --------- Co-authored-by: 见安 <73627805+Mar-garet@users.noreply.github.com> Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../server/daemon-e2e/filesystem.e2e.test.ts | 75 +++++++++++++++++++ packages/server/src/utils/checkout-git.ts | 12 +-- 2 files changed, 81 insertions(+), 6 deletions(-) diff --git a/packages/server/src/server/daemon-e2e/filesystem.e2e.test.ts b/packages/server/src/server/daemon-e2e/filesystem.e2e.test.ts index 7fee55e216..0c99744e62 100644 --- a/packages/server/src/server/daemon-e2e/filesystem.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/filesystem.e2e.test.ts @@ -31,6 +31,81 @@ afterEach(async () => { await ctx.cleanup(); }, 60000); +describe("checkout discard", () => { + test.each([".", "apps/web"])( + "discards selected Changes from the %s workspace without changing siblings", + async (workspacePath) => { + const cwd = tmpCwd(); + function git(args: string[]): string { + return execFileSync("git", args, { cwd, encoding: "utf8", stdio: "pipe" }); + } + try { + git(["init", "-b", "main"]); + git(["config", "user.name", "Paseo Test"]); + git(["config", "user.email", "test@getpaseo.local"]); + git(["config", "core.autocrlf", "false"]); + const nested = path.join(cwd, "apps", "web"); + mkdirSync(nested, { recursive: true }); + writeFileSync(path.join(nested, "target[ab].txt"), "target base\n"); + writeFileSync(path.join(nested, "targeta.txt"), "sibling base\n"); + writeFileSync(path.join(nested, "deleted.txt"), "deleted base\n"); + git(["add", "."]); + git(["-c", "commit.gpgsign=false", "commit", "-m", "initial"]); + const creation = await ctx.client.createWorkspace({ + source: { kind: "directory", path: path.join(cwd, workspacePath) }, + title: "Discard regression", + }); + const workspaceCwd = creation.workspace?.workspaceDirectory; + if (!workspaceCwd) { + throw new Error(creation.error ?? "Workspace creation failed"); + } + + writeFileSync(path.join(nested, "target[ab].txt"), "target edited\n"); + writeFileSync(path.join(nested, "targeta.txt"), "sibling edited\n"); + writeFileSync(path.join(nested, "staged.txt"), "staged new file\n"); + writeFileSync(path.join(nested, "scratch.txt"), "untracked file\n"); + git(["--literal-pathspecs", "add", "--", "apps/web/target[ab].txt", "apps/web/staged.txt"]); + git(["rm", "apps/web/deleted.txt"]); + + const subscription = ctx.client.observeCheckoutDiff(workspaceCwd, { mode: "uncommitted" }); + try { + const changes = await subscription.ready; + expect(changes.error).toBeNull(); + expect(changes.files.map((file) => file.path).sort()).toEqual([ + "apps/web/deleted.txt", + "apps/web/scratch.txt", + "apps/web/staged.txt", + "apps/web/target[ab].txt", + "apps/web/targeta.txt", + ]); + const selectedPaths = changes.files + .filter((file) => file.path !== "apps/web/targeta.txt") + .map((file) => file.path); + const discarded = await ctx.client.checkoutDiscardChanges(workspaceCwd, { + paths: selectedPaths, + }); + expect(discarded).toMatchObject({ success: true, error: null }); + + const target = await ctx.client.readFile(cwd, "apps/web/target[ab].txt"); + expect(new TextDecoder().decode(target.bytes)).toBe("target base\n"); + const restored = await ctx.client.readFile(cwd, "apps/web/deleted.txt"); + expect(new TextDecoder().decode(restored.bytes)).toBe("deleted base\n"); + const sibling = await ctx.client.readFile(cwd, "apps/web/targeta.txt"); + expect(new TextDecoder().decode(sibling.bytes)).toBe("sibling edited\n"); + await expect(ctx.client.readFile(cwd, "apps/web/staged.txt")).rejects.toThrow(/ENOENT/); + await expect(ctx.client.readFile(cwd, "apps/web/scratch.txt")).rejects.toThrow(/ENOENT/); + expect(git(["status", "--porcelain=v1", "-z"])).toBe(" M apps/web/targeta.txt\0"); + } finally { + await subscription.release(); + } + } finally { + // afterEach shuts down the daemon and its repository observers first. + onTestFinished(() => rmSync(cwd, { recursive: true, force: true })); + } + }, + ); +}); + describe("file explorer", () => { test.each(["..", "..."])( "workspace suggestions resolve exact paths starting with %s", diff --git a/packages/server/src/utils/checkout-git.ts b/packages/server/src/utils/checkout-git.ts index 0f34a1ea73..bda89506b5 100644 --- a/packages/server/src/utils/checkout-git.ts +++ b/packages/server/src/utils/checkout-git.ts @@ -3553,13 +3553,13 @@ export async function commitAll(cwd: string, message: string): Promise { } export async function discardChanges(cwd: string, pathspecs: string[]): Promise { - await requireGitRepo(cwd); + const worktreeRoot = await requireGitWorktreeRoot(cwd); if (pathspecs.length === 0) { return; } try { await runGitCommand(["--literal-pathspecs", "reset", "-q", "HEAD", "--", ...pathspecs], { - cwd, + cwd: worktreeRoot, timeout: DISCARD_CHANGES_TIMEOUT_MS, }); } catch { @@ -3567,7 +3567,7 @@ export async function discardChanges(cwd: string, pathspecs: string[]): Promise< await runGitCommand( ["--literal-pathspecs", "rm", "--cached", "-r", "-q", "--ignore-unmatch", "--", ...pathspecs], { - cwd, + cwd: worktreeRoot, timeout: DISCARD_CHANGES_TIMEOUT_MS, }, ); @@ -3579,7 +3579,7 @@ export async function discardChanges(cwd: string, pathspecs: string[]): Promise< const status = await runGitCommand( ["--literal-pathspecs", "status", "--porcelain=v1", "-z", "--", ...pathspecs], { - cwd, + cwd: worktreeRoot, timeout: DISCARD_CHANGES_TIMEOUT_MS, }, ); @@ -3605,13 +3605,13 @@ export async function discardChanges(cwd: string, pathspecs: string[]): Promise< } if (tracked.length > 0) { await runGitCommand(["--literal-pathspecs", "checkout", "-q", "--", ...tracked], { - cwd, + cwd: worktreeRoot, timeout: DISCARD_CHANGES_TIMEOUT_MS, }); } if (untracked.length > 0) { await runGitCommand(["--literal-pathspecs", "clean", "-fd", "-q", "--", ...untracked], { - cwd, + cwd: worktreeRoot, timeout: DISCARD_CHANGES_TIMEOUT_MS, }); }