diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index 199597574..f45678ccc 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -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 { + private async refreshTemporaryGitSource(source: GitSource, sourceStr: string, installedPath: string): Promise { 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"); }); diff --git a/packages/coding-agent/src/utils/git.ts b/packages/coding-agent/src/utils/git.ts index 1314edea7..2b5a1369d 100644 --- a/packages/coding-agent/src/utils/git.ts +++ b/packages/coding-agent/src/utils/git.ts @@ -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; }; diff --git a/packages/coding-agent/test/package-manager.test.ts b/packages/coding-agent/test/package-manager.test.ts index 530bb48bf..bdeffba40 100644 --- a/packages/coding-agent/test/package-manager.test.ts +++ b/packages/coding-agent/test/package-manager.test.ts @@ -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";