Skip to content

Commit 7bf2a43

Browse files
fix(git): refuse remote switches that would silently detach HEAD (#35687)
Co-authored-by: Julius Marminge <[email protected]>
1 parent c2931a6 commit 7bf2a43

2 files changed

Lines changed: 59 additions & 9 deletions

File tree

‎apps/server/src/vcs/GitVcsDriverCore.test.ts‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4939,6 +4939,58 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => {
49394939
}),
49404940
);
49414941

4942+
it.effect.each(["untracked", "other-remote"] as const)(
4943+
"refuses a remote switch that collides with a local branch (%s)",
4944+
(tracking) =>
4945+
Effect.gen(function* () {
4946+
const cwd = yield* makeTmpDir();
4947+
const { initialBranch } = yield* initRepoWithCommit(cwd);
4948+
const remote = yield* makeTmpDir("git-remote-");
4949+
yield* git(remote, ["init", "--bare"]);
4950+
yield* git(cwd, ["remote", "add", "upstream", remote]);
4951+
yield* git(cwd, ["push", "upstream", "HEAD:refs/heads/feature"]);
4952+
yield* git(cwd, ["fetch", "upstream"]);
4953+
yield* git(cwd, ["checkout", "--no-track", "-b", "feature"]);
4954+
yield* git(cwd, ["commit", "--allow-empty", "-m", "Local work"]);
4955+
if (tracking === "other-remote") {
4956+
yield* git(cwd, ["remote", "add", "origin", remote]);
4957+
yield* git(cwd, ["fetch", "origin"]);
4958+
yield* git(cwd, ["branch", "--set-upstream-to=origin/feature", "feature"]);
4959+
}
4960+
const featureHead = yield* git(cwd, ["rev-parse", "feature"]);
4961+
const upstream = yield* git(cwd, [
4962+
"for-each-ref",
4963+
"--format=%(upstream)",
4964+
"refs/heads/feature",
4965+
]);
4966+
yield* git(cwd, ["checkout", initialBranch]);
4967+
const head = yield* git(cwd, ["rev-parse", "HEAD"]);
4968+
yield* writeTextFile(cwd, "README.md", "Uncommitted work\n");
4969+
const driver = yield* GitVcsDriver.GitVcsDriver;
4970+
4971+
const result = yield* driver
4972+
.switchRef({ cwd, refName: "upstream/feature" })
4973+
.pipe(Effect.result);
4974+
assert.isTrue(Result.isFailure(result));
4975+
assert.equal(yield* git(cwd, ["branch", "--show-current"]), initialBranch);
4976+
assert.equal(yield* git(cwd, ["rev-parse", "HEAD"]), head);
4977+
assert.equal(yield* git(cwd, ["rev-parse", "feature"]), featureHead);
4978+
assert.equal(
4979+
yield* git(cwd, ["for-each-ref", "--format=%(upstream)", "refs/heads/feature"]),
4980+
upstream,
4981+
);
4982+
assert.equal(
4983+
yield* (yield* FileSystem.FileSystem).readFileString(
4984+
(yield* Path.Path).join(cwd, "README.md"),
4985+
),
4986+
"Uncommitted work\n",
4987+
);
4988+
// Selecting the existing local branch remains an explicit, attached checkout.
4989+
assert.equal((yield* driver.switchRef({ cwd, refName: "feature" })).refName, "feature");
4990+
assert.equal(yield* git(cwd, ["symbolic-ref", "HEAD"]), "refs/heads/feature");
4991+
}),
4992+
);
4993+
49424994
it.effect("rejects a missing branch without restoring a matching dirty file", () =>
49434995
Effect.gen(function* () {
49444996
const cwd = yield* makeTmpDir();

‎apps/server/src/vcs/GitVcsDriverCore.ts‎

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5309,15 +5309,13 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
53095309

53105310
const checkoutArgs = localInputExists
53115311
? ["checkout", input.refName]
5312-
: remoteExists && !localTrackingBranch && localTrackedBranchTargetExists
5313-
? ["checkout", input.refName]
5314-
: newTrackingBranch
5315-
? ["checkout", newTrackingBranch]
5316-
: remoteExists && !localTrackingBranch
5317-
? ["checkout", "--track", input.refName]
5318-
: remoteExists && localTrackingBranch
5319-
? ["checkout", localTrackingBranch]
5320-
: ["checkout", input.refName];
5312+
: newTrackingBranch
5313+
? ["checkout", newTrackingBranch]
5314+
: remoteExists && !localTrackingBranch
5315+
? ["checkout", "--track", input.refName]
5316+
: remoteExists && localTrackingBranch
5317+
? ["checkout", localTrackingBranch]
5318+
: ["checkout", input.refName];
53215319

53225320
yield* refuseOptionLikeRefName("GitVcsDriver.switchRef", input.cwd, checkoutArgs.at(-1));
53235321

0 commit comments

Comments
 (0)