From 8f50afd88fb00ac18bda7ac6ec52a549374ef303 Mon Sep 17 00:00:00 2001 From: Greg Pstrucha <875316+gricha@users.noreply.github.com> Date: Mon, 20 Apr 2026 15:59:38 -0700 Subject: [PATCH] fix(git): Force cached repo fetches Force git fetches when refreshing cached skill repositories so\nretagged refs do not fail with would clobber existing tag errors.\nThis keeps dotagents aligned with the remote state for mutable refs\nlike major version tags while leaving SHA-pinned installs unchanged.\n\nAdd regression coverage for the fetch commands and the age-gate\nunshallow path so the cache update behavior stays explicit.\n\nCo-Authored-By: OpenAI Codex --- src/sources/git.test.ts | 43 +++++++++++++++++++++++++++++++++++++++-- src/sources/git.ts | 6 +++--- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/sources/git.test.ts b/src/sources/git.test.ts index 4489ce9..49cc91f 100644 --- a/src/sources/git.test.ts +++ b/src/sources/git.test.ts @@ -5,7 +5,7 @@ vi.mock("../utils/exec.js", () => ({ ExecError: Error, })); -import { clone, headCommitDate, findCommitOlderThan } from "./git.js"; +import { clone, fetchAndReset, fetchRef, headCommitDate, findCommitOlderThan } from "./git.js"; import { exec } from "../utils/exec.js"; const mockExec = vi.mocked(exec); @@ -59,6 +59,7 @@ describe("clone", () => { // Second call: fetch the specific SHA expect(mockExec).toHaveBeenNthCalledWith(2, "git", [ "fetch", + "--force", "--depth=1", "--", "origin", @@ -146,6 +147,44 @@ describe("clone", () => { }); }); +describe("fetchAndReset", () => { + it("force-fetches origin before resetting to FETCH_HEAD", async () => { + await fetchAndReset("/tmp/repo"); + + expect(mockExec).toHaveBeenNthCalledWith(1, "git", [ + "fetch", + "--force", + "--depth=1", + "--", + "origin", + ], { cwd: "/tmp/repo" }); + expect(mockExec).toHaveBeenNthCalledWith(2, "git", [ + "reset", + "--hard", + "FETCH_HEAD", + ], { cwd: "/tmp/repo" }); + }); +}); + +describe("fetchRef", () => { + it("force-fetches the requested ref before checkout", async () => { + await fetchRef("/tmp/repo", "v0"); + + expect(mockExec).toHaveBeenNthCalledWith(1, "git", [ + "fetch", + "--force", + "--depth=1", + "--", + "origin", + "v0", + ], { cwd: "/tmp/repo" }); + expect(mockExec).toHaveBeenNthCalledWith(2, "git", [ + "checkout", + "FETCH_HEAD", + ], { cwd: "/tmp/repo" }); + }); +}); + describe("headCommitDate", () => { it("returns the committer date of HEAD", async () => { mockExec.mockResolvedValueOnce({ stdout: "2026-03-15T10:30:00+00:00\n", stderr: "" }); @@ -175,7 +214,7 @@ describe("findCommitOlderThan", () => { expect(mockExec).toHaveBeenNthCalledWith( 1, "git", - ["fetch", "--unshallow", "--", "origin"], + ["fetch", "--force", "--unshallow", "--", "origin"], { cwd: "/tmp/repo" }, ); expect(mockExec).toHaveBeenNthCalledWith( diff --git a/src/sources/git.ts b/src/sources/git.ts index ebe16a3..681c96c 100644 --- a/src/sources/git.ts +++ b/src/sources/git.ts @@ -77,7 +77,7 @@ export async function clone( */ export async function fetchAndReset(repoDir: string): Promise { try { - await exec("git", ["fetch", "--depth=1", "--", "origin"], { cwd: repoDir }); + await exec("git", ["fetch", "--force", "--depth=1", "--", "origin"], { cwd: repoDir }); await exec("git", ["reset", "--hard", "FETCH_HEAD"], { cwd: repoDir }); } catch (err) { if (err instanceof ExecError) { @@ -92,7 +92,7 @@ export async function fetchAndReset(repoDir: string): Promise { */ export async function fetchRef(repoDir: string, ref: string): Promise { try { - await exec("git", ["fetch", "--depth=1", "--", "origin", ref], { + await exec("git", ["fetch", "--force", "--depth=1", "--", "origin", ref], { cwd: repoDir, }); await exec("git", ["checkout", "FETCH_HEAD"], { cwd: repoDir }); @@ -141,7 +141,7 @@ export async function findCommitOlderThan( // Unshallow to get full history — needed to find commits older than the cutoff. // Only called when HEAD is too new, so the extra fetch is acceptable. try { - await exec("git", ["fetch", "--unshallow", "--", "origin"], { cwd: repoDir }); + await exec("git", ["fetch", "--force", "--unshallow", "--", "origin"], { cwd: repoDir }); } catch (err) { if (!(err instanceof ExecError)) {throw err;} // --unshallow fails on a complete (non-shallow) repository — that's fine