fix(coding-agent): refresh temporary git sources pinned to a branch ref

A temporary -e git source with a ref was treated as pinned and never refreshed,
so git:host/repo@main stayed on its first checkout. Refresh refs too, skipping
only a commit-hash ref that is already checked out.
This commit is contained in:
Petr Houška 2026-10-29 21:28:57 +01:00
parent cd6a8f7f3c
commit bec60c54ba
3 changed files with 48 additions and 4 deletions

View file

@ -1441,8 +1441,8 @@ export class DefaultPackageManager implements PackageManager {
if (!existsSync(installedPath)) {
const installed = await installMissing();
if (!installed) continue;
} else if (resolvedScope === "temporary" && !parsed.pinned && !isOfflineModeEnabled()) {
await this.refreshTemporaryGitSource(parsed, resolvedSource);
} else if (resolvedScope === "temporary" && !isOfflineModeEnabled()) {
await this.refreshTemporaryGitSource(parsed, resolvedSource, installedPath);
}
metadata.baseDir = installedPath;
metadata.packageRoot = installedPath;
@ -2132,11 +2132,22 @@ export class DefaultPackageManager implements PackageManager {
await this.cleanAndInstallGitDependencies(targetDir, markerPath);
}
private async refreshTemporaryGitSource(source: GitSource, sourceStr: string): Promise<void> {
private async refreshTemporaryGitSource(source: GitSource, sourceStr: string, installedPath: string): Promise<void> {
if (isOfflineModeEnabled()) {
return;
}
try {
// A ref can name a moving branch, so refresh refs too. Skip the network call only when the
// ref is a commit hash that is already checked out.
if (source.ref && /^[0-9a-f]{7,64}$/i.test(source.ref)) {
const head = await this.runCommandCapture("git", ["rev-parse", "HEAD"], {
cwd: installedPath,
timeoutMs: NETWORK_TIMEOUT_MS,
});
if (head.trim().toLowerCase().startsWith(source.ref.toLowerCase())) {
return;
}
}
await this.withProgress("pull", sourceStr, `Refreshing ${sourceStr}...`, async () => {
await this.updateGit(source, "temporary");
});

View file

@ -14,7 +14,7 @@ export type GitSource = {
path: string;
/** Git ref (branch, tag, commit) if specified */
ref?: string;
/** True if ref was specified (package won't be auto-updated) */
/** True if ref was specified. `pi update` reconciles the checkout to the ref but does not move the ref. */
pinned: boolean;
};

View file

@ -2941,6 +2941,39 @@ export default function(api) { api.registerTool({ name: "test", description: "te
});
// https://github.com/earendil-works/pi/issues/11375
it("should refresh temporary git sources with a branch ref", async () => {
const managerWithInternals = packageManager as unknown as PackageManagerInternals;
const source = "git:github.com/example/repo@main";
const parsed = managerWithInternals.parseSource(source);
if (parsed.type !== "git") throw new Error("Expected git source");
const installedPath = managerWithInternals.getGitInstallPath(parsed, "temporary");
mkdirSync(join(installedPath, "extensions"), { recursive: true });
writeFileSync(join(installedPath, "extensions", "index.ts"), "export default function() {};");
const updateGitSpy = vi.spyOn(packageManager as any, "updateGit").mockResolvedValue(undefined);
const result = await packageManager.resolveExtensionSources([source], { temporary: true });
expect(updateGitSpy).toHaveBeenCalledTimes(1);
expect(updateGitSpy.mock.calls[0][0]).toMatchObject({ ref: "main" });
expect(result.extensions.some((r) => pathEndsWith(r.path, "extensions/index.ts") && r.enabled)).toBe(true);
});
it("should not refresh temporary git sources pinned to the checked-out commit", async () => {
const managerWithInternals = packageManager as unknown as PackageManagerInternals;
const source = "git:github.com/example/repo@abc1234";
const parsed = managerWithInternals.parseSource(source);
if (parsed.type !== "git") throw new Error("Expected git source");
mkdirSync(managerWithInternals.getGitInstallPath(parsed, "temporary"), { recursive: true });
vi.spyOn(packageManager as any, "runCommandCapture").mockResolvedValue(
"ABC1234def5678901234567890123456789012345\n",
);
const updateGitSpy = vi.spyOn(packageManager as any, "updateGit").mockResolvedValue(undefined);
await packageManager.resolveExtensionSources([source], { temporary: true });
expect(updateGitSpy).not.toHaveBeenCalled();
});
it("should refresh unpinned temporary npm sources when a newer version exists", async () => {
const managerWithInternals = packageManager as unknown as PackageManagerInternals;
const source = "npm:example";