fix(coding-agent): reconcile pinned git update refs

closes #4869
This commit is contained in:
Armin Ronacher
2026-05-23 00:16:07 +02:00
parent d80bcc367e
commit c85dbb1620
6 changed files with 101 additions and 24 deletions

View File

@@ -282,7 +282,7 @@ describe("DefaultPackageManager git update", () => {
});
describe("pinned sources", () => {
it("should not update pinned git sources (with @ref)", async () => {
it("should not move pinned git sources past their configured ref", async () => {
// Create remote repo first to get the initial commit
mkdirSync(remoteDir, { recursive: true });
initGitRepo(remoteDir);
@@ -301,13 +301,77 @@ describe("DefaultPackageManager git update", () => {
// Add new commit to remote
createCommit(remoteDir, "extension.ts", "// v2", "Second commit");
// Update should be skipped for pinned sources
await packageManager.update();
// Should still be on initial commit
expect(getCurrentCommit(installedDir)).toBe(initialCommit);
expect(getFileContent(installedDir, "extension.ts")).toBe("// v1");
});
it("should checkout the configured pinned git ref during full and targeted updates", async () => {
mkdirSync(remoteDir, { recursive: true });
initGitRepo(remoteDir);
const v1Commit = createCommit(remoteDir, "extension.ts", "// v1", "Initial commit");
git(["tag", "v1"], remoteDir);
const v2Commit = createCommit(remoteDir, "extension.ts", "// v2", "Second commit");
git(["tag", "v2"], remoteDir);
mkdirSync(join(agentDir, "git", "github.com", "test"), { recursive: true });
git(["clone", remoteDir, installedDir], tempDir);
git(["checkout", "v1"], installedDir);
expect(getCurrentCommit(installedDir)).toBe(v1Commit);
const pinnedSource = `${gitSource}@v2`;
settingsManager.setPackages([pinnedSource]);
await packageManager.update();
expect(getCurrentCommit(installedDir)).toBe(v2Commit);
expect(getFileContent(installedDir, "extension.ts")).toBe("// v2");
git(["checkout", "v1"], installedDir);
await packageManager.update(pinnedSource);
expect(getCurrentCommit(installedDir)).toBe(v2Commit);
expect(getFileContent(installedDir, "extension.ts")).toBe("// v2");
});
it("should not reset an annotated tag checkout that already matches the configured ref", async () => {
mkdirSync(remoteDir, { recursive: true });
initGitRepo(remoteDir);
const taggedCommit = createCommit(remoteDir, "extension.ts", "// v1", "Initial commit");
git(["tag", "-a", "v1", "-m", "v1"], remoteDir);
mkdirSync(join(agentDir, "git", "github.com", "test"), { recursive: true });
git(["clone", remoteDir, installedDir], tempDir);
git(["checkout", "v1"], installedDir);
expect(getCurrentCommit(installedDir)).toBe(taggedCommit);
settingsManager.setPackages([`${gitSource}@v1`]);
const executedCommands: string[] = [];
const managerWithInternals = packageManager as unknown as {
runCommand: (command: string, args: string[], options?: { cwd?: string }) => Promise<void>;
};
managerWithInternals.runCommand = async (command, args, options) => {
executedCommands.push(`${command} ${args.join(" ")}`);
const result = spawnSync(command, args, {
cwd: options?.cwd,
encoding: "utf-8",
});
if (result.status !== 0) {
throw new Error(`Command failed: ${command} ${args.join(" ")}\n${result.stderr}`);
}
};
await packageManager.update();
expect(executedCommands).toContain("git fetch origin v1");
expect(executedCommands.some((command) => command.startsWith("git reset --hard"))).toBe(false);
expect(executedCommands).not.toContain("git clean -fdx");
expect(getCurrentCommit(installedDir)).toBe(taggedCommit);
});
});
describe("temporary git sources", () => {

View File

@@ -748,7 +748,7 @@ Content`,
if (args[0] === "rev-parse" && args[1] === "HEAD") {
return "old-head";
}
if (args[0] === "rev-parse" && args[1] === "FETCH_HEAD") {
if (args[0] === "rev-parse" && args[1] === "FETCH_HEAD^{commit}") {
return "new-head";
}
throw new Error(`Unexpected runCommandCapture args: ${args.join(" ")}`);
@@ -758,7 +758,9 @@ Content`,
await packageManager.install(source);
expect(runCommandSpy).toHaveBeenCalledWith("git", ["fetch", "origin", "v2"], { cwd: targetDir });
expect(runCommandSpy).toHaveBeenCalledWith("git", ["reset", "--hard", "FETCH_HEAD"], { cwd: targetDir });
expect(runCommandSpy).toHaveBeenCalledWith("git", ["reset", "--hard", "FETCH_HEAD^{commit}"], {
cwd: targetDir,
});
expect(runCommandSpy).toHaveBeenCalledWith("git", ["clean", "-fdx"], { cwd: targetDir });
expect(runCommandSpy).toHaveBeenCalledWith("npm", ["install", "--omit=dev"], { cwd: targetDir });
});
@@ -779,7 +781,7 @@ Content`,
if (args[0] === "rev-parse" && args[1] === "HEAD") {
return "old-head";
}
if (args[0] === "rev-parse" && args[1] === "origin/HEAD") {
if (args[0] === "rev-parse" && args[1] === "origin/HEAD^{commit}") {
return "new-head";
}
throw new Error(`Unexpected runCommandCapture args: ${args.join(" ")}`);
@@ -789,7 +791,9 @@ Content`,
await packageManager.install(source);
expect(runCommandSpy).toHaveBeenCalledWith("git", fetchArgs, { cwd: targetDir });
expect(runCommandSpy).toHaveBeenCalledWith("git", ["reset", "--hard", "origin/HEAD"], { cwd: targetDir });
expect(runCommandSpy).toHaveBeenCalledWith("git", ["reset", "--hard", "origin/HEAD^{commit}"], {
cwd: targetDir,
});
expect(runCommandSpy).toHaveBeenCalledWith("git", ["clean", "-fdx"], { cwd: targetDir });
});
@@ -832,7 +836,7 @@ Content`,
if (args[0] === "rev-parse" && args[1] === "--abbrev-ref" && args[2] === "@{upstream}") {
return "origin/main";
}
if (args[0] === "rev-parse" && args[1] === "@{upstream}") {
if (args[0] === "rev-parse" && (args[1] === "@{upstream}" || args[1] === "@{upstream}^{commit}")) {
return "remote-head";
}
if (args[0] === "rev-parse" && args[1] === "HEAD") {
@@ -868,7 +872,7 @@ Content`,
if (args[0] === "rev-parse" && args[1] === "--abbrev-ref" && args[2] === "@{upstream}") {
return "origin/main";
}
if (args[0] === "rev-parse" && args[1] === "@{upstream}") {
if (args[0] === "rev-parse" && (args[1] === "@{upstream}" || args[1] === "@{upstream}^{commit}")) {
return "remote-head";
}
if (args[0] === "rev-parse" && args[1] === "HEAD") {
@@ -2057,7 +2061,7 @@ export default function(api) { api.registerTool({ name: "test", description: "te
expect(packageManager.getInstalledPath("npm:legacy-pkg", "user")).toBe(managedPath);
});
it("should batch npm updates per scope and run git updates in parallel while skipping pinned and current packages", async () => {
it("should batch npm updates per scope and run git updates in parallel while skipping pinned npm and current packages", async () => {
const userOldPath = join(agentDir, "npm", "node_modules", "user-old");
const userCurrentPath = join(agentDir, "npm", "node_modules", "user-current");
const userUnknownPath = join(agentDir, "npm", "node_modules", "user-unknown");
@@ -2159,7 +2163,7 @@ export default function(api) { api.registerTool({ name: "test", description: "te
["install", "project-old@latest", "project-missing@latest", "--prefix", join(tempDir, ".pi", "npm")],
undefined,
);
expect(updateGitSpy).toHaveBeenCalledTimes(3);
expect(updateGitSpy).toHaveBeenCalledTimes(4);
expect(maxConcurrentNpmUpdates).toBeGreaterThan(1);
expect(maxConcurrentGitUpdates).toBeGreaterThan(1);
});