fix: use freshest comparison base for git status and shortstat
This commit is contained in:
@@ -365,7 +365,7 @@ type ClaudeAgentSessionOptions = {
|
|||||||
|
|
||||||
function whichClaude(): string | null {
|
function whichClaude(): string | null {
|
||||||
try {
|
try {
|
||||||
return execSync("which claude", { encoding: "utf8", env: process.env }).trim() || null;
|
return execSync("which claude", { encoding: "utf8" }).trim() || null;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { tmpdir } from "os";
|
|||||||
import {
|
import {
|
||||||
commitAll,
|
commitAll,
|
||||||
getCheckoutDiff,
|
getCheckoutDiff,
|
||||||
|
getCheckoutShortstat,
|
||||||
getPullRequestStatus,
|
getPullRequestStatus,
|
||||||
getCheckoutStatus,
|
getCheckoutStatus,
|
||||||
getCheckoutStatusLite,
|
getCheckoutStatusLite,
|
||||||
@@ -138,6 +139,39 @@ describe("checkout git utilities", () => {
|
|||||||
expect(divergedStatus.behindOfOrigin).toBe(1);
|
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 () => {
|
it("commits messages with quotes safely", async () => {
|
||||||
const message = `He said "hello" and it's fine`;
|
const message = `He said "hello" and it's fine`;
|
||||||
writeFileSync(join(repoDir, "file.txt"), "quoted\n");
|
writeFileSync(join(repoDir, "file.txt"), "quoted\n");
|
||||||
|
|||||||
@@ -771,7 +771,7 @@ async function doesGitRefExist(cwd: string, fullRef: string): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveBestBaseRefForMerge(cwd: string, normalizedBaseRef: string): Promise<string> {
|
async function resolveBestComparisonBaseRef(cwd: string, normalizedBaseRef: string): Promise<string> {
|
||||||
const [hasLocal, hasOrigin] = await Promise.all([
|
const [hasLocal, hasOrigin] = await Promise.all([
|
||||||
doesGitRefExist(cwd, `refs/heads/${normalizedBaseRef}`),
|
doesGitRefExist(cwd, `refs/heads/${normalizedBaseRef}`),
|
||||||
doesGitRefExist(cwd, `refs/remotes/origin/${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) {
|
if (!normalizedBaseRef || !currentBranch || normalizedBaseRef === currentBranch) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const comparisonBaseRef = await resolveBestComparisonBaseRef(cwd, normalizedBaseRef);
|
||||||
const { stdout } = await execAsync(
|
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 }
|
{ cwd, env: READ_ONLY_GIT_ENV }
|
||||||
);
|
);
|
||||||
const [behindRaw, aheadRaw] = stdout.trim().split(/\s+/);
|
const [behindRaw, aheadRaw] = stdout.trim().split(/\s+/);
|
||||||
@@ -1103,19 +1104,24 @@ export async function getCheckoutShortstat(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const configured = await getConfiguredBaseRefForCwd(cwd, context);
|
const configured = await getConfiguredBaseRefForCwd(cwd, context);
|
||||||
const baseRef = configured.baseRef ?? (await resolveBaseRef(cwd));
|
const localBaseRef = configured.baseRef ?? (await resolveBaseRef(cwd));
|
||||||
if (!baseRef) {
|
if (!localBaseRef) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentBranch = await getCurrentBranch(cwd);
|
const currentBranch = await getCurrentBranch(cwd);
|
||||||
if (currentBranch === baseRef) {
|
if (currentBranch === localBaseRef) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const comparisonBaseRef = await resolveBestComparisonBaseRef(
|
||||||
|
cwd,
|
||||||
|
normalizeLocalBranchRefName(localBaseRef)
|
||||||
|
);
|
||||||
|
|
||||||
let mergeBase: string;
|
let mergeBase: string;
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execAsync(`git merge-base HEAD ${baseRef}`, {
|
const { stdout } = await execAsync(`git merge-base HEAD ${comparisonBaseRef}`, {
|
||||||
cwd,
|
cwd,
|
||||||
env: READ_ONLY_GIT_ENV,
|
env: READ_ONLY_GIT_ENV,
|
||||||
});
|
});
|
||||||
@@ -1128,7 +1134,7 @@ export async function getCheckoutShortstat(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execAsync(`git diff --shortstat ${mergeBase}`, {
|
const { stdout } = await execAsync(`git diff --shortstat ${mergeBase} HEAD`, {
|
||||||
cwd,
|
cwd,
|
||||||
env: READ_ONLY_GIT_ENV,
|
env: READ_ONLY_GIT_ENV,
|
||||||
});
|
});
|
||||||
@@ -1180,7 +1186,7 @@ export async function getCheckoutDiff(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const normalizedBaseRef = normalizeLocalBranchRefName(baseRef);
|
const normalizedBaseRef = normalizeLocalBranchRefName(baseRef);
|
||||||
const bestBaseRef = await resolveBestBaseRefForMerge(cwd, normalizedBaseRef);
|
const bestBaseRef = await resolveBestComparisonBaseRef(cwd, normalizedBaseRef);
|
||||||
refForDiff = (await tryResolveMergeBase(cwd, bestBaseRef)) ?? bestBaseRef;
|
refForDiff = (await tryResolveMergeBase(cwd, bestBaseRef)) ?? bestBaseRef;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1521,7 +1527,7 @@ export async function mergeFromBase(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const normalizedBaseRef = normalizeLocalBranchRefName(baseRef);
|
const normalizedBaseRef = normalizeLocalBranchRefName(baseRef);
|
||||||
const bestBaseRef = await resolveBestBaseRefForMerge(cwd, normalizedBaseRef);
|
const bestBaseRef = await resolveBestComparisonBaseRef(cwd, normalizedBaseRef);
|
||||||
if (bestBaseRef === currentBranch) {
|
if (bestBaseRef === currentBranch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user