gabriel / muse public
Closed #22
filed by gabriel human · 139 days ago

muse pull does not prune stale remote tracking refs after hub-side branch deletion

0 Anchors
— Blast radius
— Churn 30d
0 Proposals

Summary

When a merge proposal is merged on the hub with 'Source branch deleted', the tracking ref under .muse/remotes/<remote>/<branch> is not removed by the subsequent muse pull. The user must run muse fetch <remote> --prune as a separate explicit step.

Steps to reproduce

  1. Push a branch and create an MP
  2. Merge the MP on the hub (source branch auto-deleted)
  3. muse switch dev && muse pull local dev
  4. tree .muse/remotes — stale tracking ref still present

Expected behavior

muse pull local dev should behave as muse fetch local --prune && muse merge local/dev — pruning any tracking refs for branches the remote no longer reports. At minimum, the MP merge response (which confirms source branch deletion) should trigger a prune of that specific ref.

Actual behavior

Stale tracking refs accumulate silently. The only way to clear them is an explicit muse fetch <remote> --prune.

Context

This surfaced during a remote branch cleanup session. After merging MP #8 into local dev, .muse/remotes/local/fix/proposal-closed-state-badge persisted until manually pruned.

◎ Activity2
●
gabriel opened this issue 139 days ago
○
gabriel 139 days ago

Proposed solution: fetch.prune config option

Mirror git's fetch.prune behavior exactly.

Config (set once, applies to all fetches in that repo):

muse config set fetch.prune true

Behavior when set:

  • muse fetch <remote> — prunes stale tracking refs automatically
  • muse pull <remote> <branch> — prunes as part of the fetch step

Behavior when unset (default): current behavior — no pruning, explicit muse fetch <remote> --prune required.

Global variant (applies across all repos on the machine):

muse config set --global fetch.prune true

This is the exact pattern git uses (git config --global fetch.prune true). Agents and humans who set it once never have to think about stale tracking refs again. The --prune flag on muse fetch stays as an explicit one-shot override regardless of config.

○
gabriel 2 days ago

Implemented and shipped — commit ec9f1f47 on dev/main (pushed to local/staging/production). Implements exactly the fetch.prune config option proposed in the comment above, plus fixes the actual root cause the ticket reported.

Root cause

muse fetch --prune already existed and worked correctly. The bug was that muse pull had its own, entirely independent fetch implementation that never called the pruning routine at all — so even running muse fetch <remote> --prune immediately before a muse pull didn't help, since the very next muse pull would leave stale refs behind again with no way to avoid it short of remembering to run fetch --prune as a constant separate habit.

What shipped

fetch.prune config (muse/cli/config.py), mirroring git config fetch.prune exactly:

muse config set fetch.prune true            # this repo only
muse config set --global fetch.prune true   # every repo on this machine

Repo-level setting wins over global when both are set; defaults to False (pruning stays opt-in, same as git). --prune/--no-prune remain available on both muse fetch and muse pull as one-shot overrides regardless of config.

muse config get/set --global — new, generic flag mirroring git config --global: reads/writes ~/.muse/config.toml instead of the repo config, works outside a repo too. (This didn't exist before at all — needed to make the ticket's own proposed global variant possible.)

muse pull now actually prunes. Calls the same _prune_stale_refs() routine muse fetch --prune already used, right after discovering the remote's live branches — before resolving the branch being pulled, so a hub-deleted branch's stale ref is cleared even when pulling a completely different, still-existing branch. This is the exact repro from the ticket: MP merged with "source branch deleted", then muse pull local dev (a different branch) now correctly clears .muse/remotes/local/<deleted-branch>.

New pruned: list[str] field on muse pull --json's output (matching muse fetch's existing schema), populated at every status outcome. Pruning respects --dry-run (previews only, writes nothing).

Tests

New tests/test_pull_fetch_prune_config.py (13 tests): get_fetch_prune precedence (repo wins, global fallback, default false), muse config --global read/write/isolation from repo config, muse fetch honoring the config with --no-prune override, and muse pull actually pruning — explicit --prune, config-driven (no flag needed), default-off, and --dry-run preview-only.

Confirmed genuine RED by shelving the entire implementation and re-running: all 13 failed (ImportError for the new config function, "Unknown config namespace 'fetch'", argparse errors for the new flags, missing pruned JSON key) — restored, all green. One test-authoring mistake caught along the way (not a product bug): three pull tests initially included the "stale" branch inside the mocked remote's own live branch list, which made it not stale at all — fixed to correctly omit it.

Regression: 456 tests across the fetch, pull, and config test suites — all green, no regressions.

✓
closed this issue 2 days ago