diff --git a/packages/server/src/server/agent/providers/claude-agent.ts b/packages/server/src/server/agent/providers/claude-agent.ts index f9124ba55..abfb21fdb 100644 --- a/packages/server/src/server/agent/providers/claude-agent.ts +++ b/packages/server/src/server/agent/providers/claude-agent.ts @@ -365,7 +365,7 @@ type ClaudeAgentSessionOptions = { function whichClaude(): string | null { try { - return execSync("which claude", { encoding: "utf8", env: process.env }).trim() || null; + return execSync("which claude", { encoding: "utf8" }).trim() || null; } catch { return null; } diff --git a/packages/server/src/utils/checkout-git.test.ts b/packages/server/src/utils/checkout-git.test.ts index cc9f82994..40914e467 100644 --- a/packages/server/src/utils/checkout-git.test.ts +++ b/packages/server/src/utils/checkout-git.test.ts @@ -6,6 +6,7 @@ import { tmpdir } from "os"; import { commitAll, getCheckoutDiff, + getCheckoutShortstat, getPullRequestStatus, getCheckoutStatus, getCheckoutStatusLite, @@ -138,6 +139,39 @@ describe("checkout git utilities", () => { expect(divergedStatus.behindOfOrigin).toBe(1); }); + it("uses the freshest comparison base for status and shortstat when local main is stale", async () => { + const remoteDir = join(tempDir, "remote.git"); + const cloneDir = join(tempDir, "clone"); + execSync(`git init --bare -b main ${remoteDir}`); + execSync(`git remote add origin ${remoteDir}`, { cwd: repoDir }); + execSync("git push -u origin main", { cwd: repoDir }); + + execSync(`git clone ${remoteDir} ${cloneDir}`); + execSync("git config user.email 'test@test.com'", { cwd: cloneDir }); + execSync("git config user.name 'Test'", { cwd: cloneDir }); + writeFileSync(join(cloneDir, "upstream.txt"), "upstream 1\nupstream 2\n"); + execSync("git add upstream.txt", { cwd: cloneDir }); + execSync("git -c commit.gpgsign=false commit -m 'remote update'", { cwd: cloneDir }); + execSync("git push", { cwd: cloneDir }); + + execSync("git fetch origin", { cwd: repoDir }); + execSync("git checkout -b feature origin/main", { cwd: repoDir }); + writeFileSync(join(repoDir, "feature.txt"), "feature\n"); + execSync("git add feature.txt", { cwd: repoDir }); + execSync("git -c commit.gpgsign=false commit -m 'feature update'", { cwd: repoDir }); + + const status = await getCheckoutStatus(repoDir); + expect(status.isGit).toBe(true); + if (!status.isGit) { + return; + } + expect(status.baseRef).toBe("main"); + expect(status.aheadBehind).toEqual({ ahead: 1, behind: 0 }); + + const shortstat = await getCheckoutShortstat(repoDir); + expect(shortstat).toEqual({ additions: 1, deletions: 0 }); + }); + it("commits messages with quotes safely", async () => { const message = `He said "hello" and it's fine`; writeFileSync(join(repoDir, "file.txt"), "quoted\n"); diff --git a/packages/server/src/utils/checkout-git.ts b/packages/server/src/utils/checkout-git.ts index b2a876cf2..40c2c551a 100644 --- a/packages/server/src/utils/checkout-git.ts +++ b/packages/server/src/utils/checkout-git.ts @@ -771,7 +771,7 @@ async function doesGitRefExist(cwd: string, fullRef: string): Promise { } } -async function resolveBestBaseRefForMerge(cwd: string, normalizedBaseRef: string): Promise { +async function resolveBestComparisonBaseRef(cwd: string, normalizedBaseRef: string): Promise { const [hasLocal, hasOrigin] = await Promise.all([ doesGitRefExist(cwd, `refs/heads/${normalizedBaseRef}`), doesGitRefExist(cwd, `refs/remotes/origin/${normalizedBaseRef}`), @@ -811,8 +811,9 @@ async function getAheadBehind(cwd: string, baseRef: string, currentBranch: strin if (!normalizedBaseRef || !currentBranch || normalizedBaseRef === currentBranch) { return null; } + const comparisonBaseRef = await resolveBestComparisonBaseRef(cwd, normalizedBaseRef); const { stdout } = await execAsync( - `git rev-list --left-right --count ${normalizedBaseRef}...${currentBranch}`, + `git rev-list --left-right --count ${comparisonBaseRef}...${currentBranch}`, { cwd, env: READ_ONLY_GIT_ENV } ); const [behindRaw, aheadRaw] = stdout.trim().split(/\s+/); @@ -1103,19 +1104,24 @@ export async function getCheckoutShortstat( } const configured = await getConfiguredBaseRefForCwd(cwd, context); - const baseRef = configured.baseRef ?? (await resolveBaseRef(cwd)); - if (!baseRef) { + const localBaseRef = configured.baseRef ?? (await resolveBaseRef(cwd)); + if (!localBaseRef) { return null; } const currentBranch = await getCurrentBranch(cwd); - if (currentBranch === baseRef) { + if (currentBranch === localBaseRef) { return null; } + const comparisonBaseRef = await resolveBestComparisonBaseRef( + cwd, + normalizeLocalBranchRefName(localBaseRef) + ); + let mergeBase: string; try { - const { stdout } = await execAsync(`git merge-base HEAD ${baseRef}`, { + const { stdout } = await execAsync(`git merge-base HEAD ${comparisonBaseRef}`, { cwd, env: READ_ONLY_GIT_ENV, }); @@ -1128,7 +1134,7 @@ export async function getCheckoutShortstat( } try { - const { stdout } = await execAsync(`git diff --shortstat ${mergeBase}`, { + const { stdout } = await execAsync(`git diff --shortstat ${mergeBase} HEAD`, { cwd, env: READ_ONLY_GIT_ENV, }); @@ -1180,7 +1186,7 @@ export async function getCheckoutDiff( } const normalizedBaseRef = normalizeLocalBranchRefName(baseRef); - const bestBaseRef = await resolveBestBaseRefForMerge(cwd, normalizedBaseRef); + const bestBaseRef = await resolveBestComparisonBaseRef(cwd, normalizedBaseRef); refForDiff = (await tryResolveMergeBase(cwd, bestBaseRef)) ?? bestBaseRef; } @@ -1521,7 +1527,7 @@ export async function mergeFromBase( } const normalizedBaseRef = normalizeLocalBranchRefName(baseRef); - const bestBaseRef = await resolveBestBaseRefForMerge(cwd, normalizedBaseRef); + const bestBaseRef = await resolveBestComparisonBaseRef(cwd, normalizedBaseRef); if (bestBaseRef === currentBranch) { return; }