From 407b4b68b08029e22be8ebc3e76493f2ed64d530 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 22 Apr 2026 18:35:01 +0700 Subject: [PATCH] Recover branch during rebase --- .../server/src/utils/checkout-git.test.ts | 18 ++++++++++++++ packages/server/src/utils/checkout-git.ts | 24 ++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/server/src/utils/checkout-git.test.ts b/packages/server/src/utils/checkout-git.test.ts index 266e593e9..8830602de 100644 --- a/packages/server/src/utils/checkout-git.test.ts +++ b/packages/server/src/utils/checkout-git.test.ts @@ -194,6 +194,24 @@ describe("checkout git utilities", () => { expect(branch).toBeNull(); }); + it("returns the branch being rebased when HEAD is detached during a rebase", async () => { + execSync("git checkout -b feature/rebase-test", { cwd: repoDir }); + writeFileSync(join(repoDir, "file.txt"), "feature\n"); + execSync("git add file.txt", { cwd: repoDir }); + execSync("git -c commit.gpgsign=false commit -m 'feature change'", { cwd: repoDir }); + + execSync("git checkout main", { cwd: repoDir }); + writeFileSync(join(repoDir, "file.txt"), "main\n"); + execSync("git add file.txt", { cwd: repoDir }); + execSync("git -c commit.gpgsign=false commit -m 'main change'", { cwd: repoDir }); + + execSync("git checkout feature/rebase-test", { cwd: repoDir }); + expect(() => execSync("git rebase main", { cwd: repoDir, stdio: "pipe" })).toThrow(); + + const branch = await getCurrentBranch(repoDir); + expect(branch).toBe("feature/rebase-test"); + }); + it("handles status/diff/commit in a normal repo", async () => { writeFileSync(join(repoDir, "file.txt"), "updated\n"); diff --git a/packages/server/src/utils/checkout-git.ts b/packages/server/src/utils/checkout-git.ts index c2b2beaf8..3a1743af5 100644 --- a/packages/server/src/utils/checkout-git.ts +++ b/packages/server/src/utils/checkout-git.ts @@ -1,6 +1,6 @@ import { resolve, dirname, basename } from "path"; import { existsSync, realpathSync } from "fs"; -import { open as openFile, stat as statFile } from "fs/promises"; +import { open as openFile, readFile, stat as statFile } from "fs/promises"; import { TTLCache } from "@isaacs/ttlcache"; import type { ParsedDiffFile } from "../server/utils/diff-highlighter.js"; import { parseAndHighlightDiff } from "../server/utils/diff-highlighter.js"; @@ -701,12 +701,34 @@ export async function getCurrentBranch(cwd: string): Promise { env: READ_ONLY_GIT_ENV, }); const branch = stdout.trim(); + if (branch === "HEAD") { + return await getRebaseHeadBranch(cwd); + } return branch.length > 0 ? branch : null; } catch { return null; } } +async function getRebaseHeadBranch(cwd: string): Promise { + for (const path of ["rebase-merge/head-name", "rebase-apply/head-name"]) { + try { + const { stdout } = await runGitCommand(["rev-parse", "--git-path", path], { + cwd, + env: READ_ONLY_GIT_ENV, + }); + const headName = (await readFile(resolve(cwd, stdout.trim()), "utf8")).trim(); + if (headName.startsWith("refs/heads/")) { + return headName.slice("refs/heads/".length) || null; + } + return headName || null; + } catch { + // Try the other rebase backend. + } + } + return null; +} + async function getWorktreeRoot(cwd: string): Promise { try { const { stdout } = await runGitCommand(["rev-parse", "--show-toplevel"], {