authorgravatar for 153842990+yashranaway@users.noreply.github.comAditya Garud <153842990+yashranaway@users.noreply.github.com> 2026-09-18 13:03:35+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-18 13:03:35+05:30
log56a9bf2bd7d3dcdae722a5de84578909dc11aeda
tree6165d5e74d594f2883ff60c2c82b8902239040a4
parentc5bbf3d1495234236e7ad5d7e8d091b0ab081a95
signature Signed by PGP key B5690EEEBB952194

fix(git): prevent stale branch selections from restoring files (#10574)

Co-authored-by: shivam <91240327+shivamhwp@users.noreply.github.com>

2 files changed, 55 insertions(+), 1 deletions(-)

apps/server/src/vcs/GitVcsDriverCore.test.ts+53
......@@ -1982,6 +1982,59 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
19821982 assert.equal(result.branch, current);
19831983 }),
19841984 );
1985
1986 it.effect("rejects a missing branch without restoring a matching dirty file", () =>
1987 Effect.gen(function* () {
1988 const cwd = yield* makeTmpDir();
1989 const { initialBranch } = yield* initRepoWithCommit(cwd);
1990 const driver = yield* GitVcsDriver.GitVcsDriver;
1991 yield* writeTextFile(cwd, "obsolete-branch", "original\n");
1992 yield* git(cwd, ["add", "obsolete-branch"]);
1993 yield* git(cwd, ["commit", "-m", "tracked file"]);
1994 yield* git(cwd, ["branch", "obsolete-branch"]);
1995 yield* git(cwd, ["branch", "-D", "obsolete-branch"]);
1996 yield* writeTextFile(cwd, "obsolete-branch", "uncommitted work\n");
1997
1998 const result = yield* driver
1999 .switchRef({ cwd, refName: "obsolete-branch" })
2000 .pipe(Effect.result);
2001
2002 assert.equal(result._tag, "Failure");
2003 const fileSystem = yield* FileSystem.FileSystem;
2004 const path = yield* Path.Path;
2005 assert.equal(
2006 yield* fileSystem.readFileString(path.join(cwd, "obsolete-branch")),
2007 "uncommitted work\n",
2008 );
2009 assert.equal(yield* git(cwd, ["branch", "--show-current"]), initialBranch);
2010 }),
2011 );
2012
2013 it.effect("still creates and reuses remote tracking branches and allows detached refs", () =>
2014 Effect.gen(function* () {
2015 const cwd = yield* makeTmpDir();
2016 const remote = yield* makeTmpDir("git-remote-");
2017 const { initialBranch } = yield* initRepoWithCommit(cwd);
2018 const driver = yield* GitVcsDriver.GitVcsDriver;
2019 yield* git(remote, ["init", "--bare"]);
2020 yield* git(cwd, ["remote", "add", "origin", remote]);
2021 yield* git(cwd, ["push", "origin", "HEAD:refs/heads/remote-only"]);
2022
2023 for (let attempt = 0; attempt < 2; attempt += 1) {
2024 const result = yield* driver.switchRef({ cwd, refName: "origin/remote-only" });
2025 assert.equal(result.refName, "remote-only");
2026 assert.equal(
2027 yield* git(cwd, ["rev-parse", "--abbrev-ref", "@{upstream}"]),
2028 "origin/remote-only",
2029 );
2030 yield* driver.switchRef({ cwd, refName: initialBranch });
2031 }
2032 const commit = yield* git(cwd, ["rev-parse", "HEAD"]);
2033 const detached = yield* driver.switchRef({ cwd, refName: commit });
2034 assert.equal(detached.refName, null);
2035 assert.equal(yield* git(cwd, ["rev-parse", "HEAD"]), commit);
2036 }),
2037 );
19852038 });
19862039
19872040 describe("worktree operations", () => {
apps/server/src/vcs/GitVcsDriverCore.ts+2-1
......@@ -3469,7 +3469,8 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
34693469 ? ["checkout", localTrackingBranch]
34703470 : ["checkout", input.refName];
34713471
3472 yield* executeGit("GitVcsDriver.switchRef.checkout", input.cwd, checkoutArgs, {
3472 // A stale ref must not turn into a path checkout that discards local edits.
3473 yield* executeGit("GitVcsDriver.switchRef.checkout", input.cwd, [...checkoutArgs, "--"], {
34733474 timeoutMs: 10_000,
34743475 fallbackErrorDetail: "git checkout failed",
34753476 });