Skip to content

Fetch tags with a forced refspec #464

Closed

Description

@blooop

Work

Fetch tags with a forced refspec, so the bare's tags are prunable and a moved tag stops wedging the fetch.

One word, measured on #453, and it fixes a live production bug as well as a straggler. In Git::fetch_all (rust/devlaunch-core/src/clients/git.rs:612-626), replace --tags with an explicit +refs/tags/*:refs/tags/*.

--tags is a non-forced refs/tags/*:refs/tags/*, and git fetch --prune prunes per refspec. Two consequences, both measured:

  • Tags are never pruned. refs/tags is monotone in every tag the remote has ever advertised, and those refs pin objects. A 20,971,520 B blob reachable only from an upstream-deleted tag survived git gc --prune=now at 0 B reclaimed; with the forced refspec, the tag went and 20,975,616 B came back, taking the probe repository from 21,094,400 B to 118,784 B.
  • A tag force-moved upstream wedges the fetch permanently. ! [rejected] moving -> moving (would clobber existing tag), exit 1, repeatably, which RepoManager::fetch_repo maps to FetchRepoError::Refused. So freshness fetches stay broken for that repository until a human deletes the local tag by hand. With the forced refspec it is t [tag update], exit 0.

A test per effect: a tag deleted upstream is pruned locally, a tag force-moved upstream updates instead of failing, and the fetch's argv is pinned so the refspec cannot silently regress to --tags.

One principle-1 call to make explicitly rather than by implication, and to state in the resolution: pruning a tag deletes a ref. The reading that licenses it is that a tag in the bare is a copy of an upstream ref holding no unique work, which is the same argument that already licenses pruning heads. If that reading is accepted, say so; if not, this becomes a flagged path rather than automatic.

Also in scope because #453 found it in the same place: --prune removes refs/heads/main when main disappears upstream, leaving the bare's HEAD symref dangling, which Git::symbolic_ref reads for default-branch detection. Either handle it or record it as a known consequence with the symptom named.

Activity

  1. self-assigned this
    on Aug 25, 2026
  2. blooop commented on Aug 25, 2026

    @blooop
    OwnerAuthor

    breadcrumb: claimed. #471 is merged (e55239a), so main now carries the widened unpushed probe. Note the interaction: that probe asks --all --not --remotes, which spans refs/tags, and #453 measured that the bare keeps tags upstream has deleted. Forcing the tag refspec changes what a bare holds, so check whether it moves the false positive #471 took deliberately.

  3. blooop commented on Aug 25, 2026

    @blooop
    OwnerAuthor

    breadcrumb: seams, and red first. The behavioural pair is tested at the repository-manager fetch, against a real remote on a local path, because that is the level the wedge is reported at (Refused) rather than merely caused at; the refspec itself is pinned as argv at the git client, where every other verb already is. Red quotes git: ! [rejected] release -> release (would clobber existing tag), and the head pushed alongside it did not arrive either, which is worth saying out loud because a rejected tag fails the whole fetch, not just the tag.

    Principle 1, stated rather than implied: a tag in the bare is a copy of an upstream ref and holds no work that exists nowhere else, so pruning it is licensed by the same reading that already licenses pruning heads. Not a flagged path.

  4. blooop commented on Aug 25, 2026

    @blooop
    OwnerAuthor

    breadcrumb: built, PR #476. Git::fetch_all now asks for both refspecs spelled out and both forced, so a moved tag is t [tag update] instead of a permanently Refused fetch, and a retracted tag is pruned like a head. Three argv pins moved with it, and the pin now carries its reason so --tags cannot come back quietly.

    The #471 interaction, measured rather than reasoned about: this narrows the false positive and cannot widen it. Built the case both ways. Under the old refspec, a tag on a branch the remote later deleted survives in the bare, and the probe in a clone cut from that bare reports a phantom unpushed commit, in that clone and in every later one. Under the forced refspec the tag is pruned and the probe is silent. What survives is only the half docs/cleanup.md already describes, a tag the remote still carries which no remote branch reaches, so the page reads correctly before and after. Widening is unreachable from here: forcing only moves a local tag onto what upstream advertises and pruning only removes what it no longer advertises, so no commit becomes clone-only that was not already. #471's trade is deliberately untouched; it is decided on a class this change does not reach, and reversing it is #475's.

    The HEAD-dangling edge #453 found in the same place is recorded, not handled, with the symptom named where the refspec is: the symref still answers a branch name, so rev-parse is what fails and the failure surfaces later as a launch aimed at a branch the cache no longer has. Handling it means deciding what a bare should point HEAD at when upstream has no default branch, which is a decision rather than a fix.

  5. blooop commented on Aug 25, 2026

    @blooop
    OwnerAuthor

    breadcrumb: gate passed. PR #476, head 13072989041e0a1a7aa3dd100317c253ae2a803a, 15 checks green and MERGEABLE. Handing to a fresh reviewer who did not write it, with the tag-prune deletion (a ref removed automatically, unflagged) as the thing to press hardest on.

  6. blooop commented on Aug 25, 2026

    @blooop
    OwnerAuthor

    breadcrumb: reviewed, Approve, nothing blocking. Two-axis report on #476. Principle 1 re-measured from scratch: one production caller pointing at the bare alone, no tag verb anywhere in devlaunch, and an existing clone keeps the overwritten tag's objects through a bare gc --prune=now. #471's false positive narrows as claimed, with one correction for the record: "cannot widen" should read "narrows in every reachable non-wedged state" (a tag force-moved onto a commit upstream then orphans widens it, but only relative to a permanently failing fetch, and in the keep-the-clone direction). Two comment-only findings: repo_manager.rs:3551's "principle 1" resolves to nothing in-tree, and git.rs:566's rev-parse HEAD exits 0 (it is --verify that fails).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions