diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 3b0d3d96..e1475392 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - Fixed RPC `get_session_stats` to expose `contextUsage`, so headless clients can read actual current context-window usage instead of deriving it from token totals ([#2550](https://github.com/badlogic/pi-mono/issues/2550)) +- Fixed `pi update` for git packages to fetch only the tracked target branch with `--no-tags`, reducing unrelated branch and tag noise while preserving force-push-safe updates ([#2548](https://github.com/badlogic/pi-mono/issues/2548)) ## [0.62.0] - 2026-03-23 diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index b897f099..7e449e22 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -1279,20 +1279,66 @@ export class DefaultPackageManager implements PackageManager { return match[1]; } - private async getLocalGitUpdateTarget(installedPath: string): Promise<{ ref: string; head: string }> { + private async getLocalGitUpdateTarget( + installedPath: string, + ): Promise<{ ref: string; head: string; fetchArgs: string[] }> { try { + const upstream = await this.runCommandCapture("git", ["rev-parse", "--abbrev-ref", "@{upstream}"], { + cwd: installedPath, + timeoutMs: NETWORK_TIMEOUT_MS, + }); + const trimmedUpstream = upstream.trim(); + if (!trimmedUpstream.startsWith("origin/")) { + throw new Error(`Unsupported upstream remote: ${trimmedUpstream}`); + } + const branch = trimmedUpstream.slice("origin/".length); + if (!branch) { + throw new Error("Missing upstream branch name"); + } const head = await this.runCommandCapture("git", ["rev-parse", "@{upstream}"], { cwd: installedPath, timeoutMs: NETWORK_TIMEOUT_MS, }); - return { ref: "@{upstream}", head }; + return { + ref: "@{upstream}", + head, + fetchArgs: [ + "fetch", + "--prune", + "--no-tags", + "origin", + `+refs/heads/${branch}:refs/remotes/origin/${branch}`, + ], + }; } catch { await this.runCommand("git", ["remote", "set-head", "origin", "-a"], { cwd: installedPath }).catch(() => {}); const head = await this.runCommandCapture("git", ["rev-parse", "origin/HEAD"], { cwd: installedPath, timeoutMs: NETWORK_TIMEOUT_MS, }); - return { ref: "origin/HEAD", head }; + const originHeadRef = await this.runCommandCapture("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], { + cwd: installedPath, + timeoutMs: NETWORK_TIMEOUT_MS, + }).catch(() => ""); + const branch = originHeadRef.trim().replace(/^refs\/remotes\/origin\//, ""); + if (branch) { + return { + ref: "origin/HEAD", + head, + fetchArgs: [ + "fetch", + "--prune", + "--no-tags", + "origin", + `+refs/heads/${branch}:refs/remotes/origin/${branch}`, + ], + }; + } + return { + ref: "origin/HEAD", + head, + fetchArgs: ["fetch", "--prune", "--no-tags", "origin", "+HEAD:refs/remotes/origin/HEAD"], + }; } } @@ -1478,15 +1524,20 @@ export class DefaultPackageManager implements PackageManager { return; } - // Fetch latest from remote (handles force-push by getting new history) - await this.runCommand("git", ["fetch", "--prune", "origin"], { cwd: targetDir }); + const target = await this.getLocalGitUpdateTarget(targetDir); + + // Fetch only the ref we will reset to, avoiding unrelated branch/tag noise. + await this.runCommand("git", target.fetchArgs, { cwd: targetDir }); const localHead = await this.runCommandCapture("git", ["rev-parse", "HEAD"], { cwd: targetDir, timeoutMs: NETWORK_TIMEOUT_MS, }); - const target = await this.getLocalGitUpdateTarget(targetDir); - if (localHead.trim() === target.head.trim()) { + const refreshedTargetHead = await this.runCommandCapture("git", ["rev-parse", target.ref], { + cwd: targetDir, + timeoutMs: NETWORK_TIMEOUT_MS, + }); + if (localHead.trim() === refreshedTargetHead.trim()) { return; } diff --git a/packages/coding-agent/test/git-update.test.ts b/packages/coding-agent/test/git-update.test.ts index 2f59f9ee..392cebf0 100644 --- a/packages/coding-agent/test/git-update.test.ts +++ b/packages/coding-agent/test/git-update.test.ts @@ -139,7 +139,10 @@ describe("DefaultPackageManager git update", () => { await packageManager.update(); - expect(executedCommands).toContain("git fetch --prune origin"); + expect(executedCommands).toContain( + "git fetch --prune --no-tags origin +refs/heads/main:refs/remotes/origin/main", + ); + expect(executedCommands).not.toContain("git fetch --prune origin"); expect(executedCommands).not.toContain("git reset --hard @{upstream}"); expect(executedCommands).not.toContain("git reset --hard origin/HEAD"); expect(executedCommands).not.toContain("git clean -fdx"); @@ -183,8 +186,26 @@ describe("DefaultPackageManager git update", () => { const detachedCommit = getCurrentCommit(installedDir); git(["checkout", detachedCommit], installedDir); + const executedCommands: string[] = []; + const managerWithInternals = packageManager as unknown as { + runCommand: (command: string, args: string[], options?: { cwd?: string }) => Promise; + }; + 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 --prune --no-tags origin +refs/heads/main:refs/remotes/origin/main", + ); expect(getCurrentCommit(installedDir)).toBe(latestCommit); expect(getFileContent(installedDir, "extension.ts")).toBe("// v3"); }); @@ -323,12 +344,17 @@ describe("DefaultPackageManager git update", () => { if (args[0] === "rev-parse" && args[1] === "@{upstream}") { return "remote-head"; } + if (args[0] === "rev-parse" && args[1] === "--abbrev-ref") { + return "origin/main"; + } return ""; }; await packageManager.resolveExtensionSources([gitSource], { temporary: true }); - expect(executedCommands).toContain("git fetch --prune origin"); + expect(executedCommands).toContain( + "git fetch --prune --no-tags origin +refs/heads/main:refs/remotes/origin/main", + ); expect(getFileContent(cachedDir, "pi-extensions/session-breakdown.ts")).toBe("// fresh"); });