gabriel / muse public
feat BREAKING feat/22-fetch-prune-config #1 / 1
AI Agent gabriel · 1 day ago · Oct 22, 2026 · Diff

feat: fetch.prune config option; muse pull now actually prunes stale refs (muse#22)

Implements the user's own proposed solution on the ticket: a fetch.prune config switch (repo + global, repo wins), mirroring git's fetch.prune exactly. Also fixes the root cause the ticket actually reported: muse pull had its own independent fetch logic and never called the pruning routine muse fetch --prune already used — so even an explicit muse fetch --prune run immediately before a pull did nothing to help, since pull's next invocation would just leave stale refs behind again.

muse/cli/config.py: - New [fetch] config section (FetchConfig TypedDict, parse/serialize, settable namespace), prune: bool, same boolean pattern as [push]. - New get_fetch_prune(repo_root) — repo-level fetch.prune wins when set; falls back to ~/.muse/config.toml (global) when repo-level is unset; defaults to False (pruning stays opt-in, matching git's own default).

muse/cli/commands/config_cmd.py: - New --global flag on both `muse config get` and `muse config set`, mirroring `git config --global` exactly: reads/writes ~/.muse/config.toml instead of the repo config, works outside a repo too.

muse/cli/commands/fetch.py: - --prune/-p and new --no-prune are now mutually exclusive, default None (previously --prune defaulted to False with no way to express "use whatever the config says"). When neither flag is passed, prune is resolved from get_fetch_prune(root) rather than hardcoded False.

muse/cli/commands/pull.py: - New --prune/-p / --no-prune flags, same resolution as fetch.py. - Pruning actually happens now: calls fetch.py's _prune_stale_refs() right after fetch_remote_info() returns the remote's live branch_heads — 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 (the exact muse#22 repro: MP merged with "source branch deleted", then `muse pull local dev` left .muse/remotes/local/<deleted-branch> behind). - New `pruned: list[str]` field on _PullJson, populated at all 9 construction sites (every status: up_to_date, fast_forward, merged, conflict, fetched, dry_run). - Pruning itself respects --dry-run (previews via "Would prune", writes nothing) same as muse fetch --dry-run --prune already did.

docs/agent-guide.md: two new Gitism Glossary entries, framed like the existing refspec-syntax entry ("now works identically to git, not a gotcha") — fetch.prune config and muse config --global.

Tests: new tests/test_pull_fetch_prune_config.py (13 tests) — get_fetch_prune precedence (4), muse config --global read/write/isolation (3), muse fetch honouring fetch.prune config + --no-prune override (2), muse pull actually pruning: explicit --prune, config-driven, default-off, --dry-run preview-only (4).

Confirmed genuine RED by shelving the entire implementation (config.py, config_cmd.py, fetch.py, pull.py) and re-running: all 13 new tests failed (ImportError for get_fetch_prune, "Unknown config namespace 'fetch'", argparse exit 2 for the new flags, missing "pruned" JSON key) — restored, all 13 green.

One test-authoring bug caught and fixed along the way (not a product bug): three of the pull tests initially included the "stale" branch inside the mocked remote's own branch_heads dict, which made it not stale at all from _prune_stale_refs' perspective — fixed by correctly omitting it, matching the real-world scenario of a branch deleted on the remote.

Regression: 456 tests across test_pull_fetch_prune_config.py, test_cmd_fetch_hardening.py, test_fetch_envelope.py, test_fetch_mpack_command_wiring.py, test_transport_fetch_phase2.py, test_cmd_pull_hardening.py, test_pull_missing_snapshot_guard.py, test_coord_pull_null_records.py, test_cmd_config.py, test_config_cmd_envelope.py, test_cmd_config_hardening.py — all green, no regressions.

sha256:ec9f1f4725bce6f66bf08946ea05413572841d9ba43e02df2f3d1cf94e14eb68 sha
+47 ~26 symbols
6 changed · 1256 in snapshot files
sha256:a9c6533a5fda7af184c6ec37c40cbfa09f4c2a010949d8457648aaea72312ceb snapshot
+47
symbols added
~26
symbols modified
6
files changed
1256
files in snapshot
0
dead code introduced
Semantic Changes 73 symbols
~ tests/test_pull_fetch_prune_config.py .py 39 symbols added
+ REMOTE_ID variable variable REMOTE_ID L32–32
+ STALE_ID variable variable STALE_ID L33–33
+ TestConfigGlobalFlag class class TestConfigGlobalFlag L98–152
+ test_get_global_reads_from_home method method test_get_global_reads_from_home L119–135
+ test_repo_set_does_not_leak_to_global_get method method test_repo_set_does_not_leak_to_global_get L137–152
+ test_set_global_writes_to_home_not_repo method method test_set_global_writes_to_home_not_repo L99–117
+ TestFetchHonoursPruneConfig class class TestFetchHonoursPruneConfig L179–225
+ test_config_true_prunes_without_explicit_flag method method test_config_true_prunes_without_explicit_flag L180–201
+ test_no_prune_flag_overrides_config_true method method test_no_prune_flag_overrides_config_true L203–225
+ TestGetFetchPrunePrecedence class class TestGetFetchPrunePrecedence L51–90
+ test_default_false_when_unset method method test_default_false_when_unset L52–59
+ test_global_true_used_when_repo_unset method method test_global_true_used_when_repo_unset L71–79
+ test_repo_false_overrides_global_true method method test_repo_false_overrides_global_true L81–90
+ test_repo_true_wins method method test_repo_true_wins L61–69
+ TestPullPrunesStaleRefs class class TestPullPrunesStaleRefs L265–349
+ test_pull_does_not_prune_by_default method method test_pull_does_not_prune_by_default L309–325
+ test_pull_dry_run_prune_does_not_delete method method test_pull_dry_run_prune_does_not_delete L327–349
+ test_pull_prunes_when_config_true_no_explicit_flag method method test_pull_prunes_when_config_true_no_explicit_flag L290–307
+ test_pull_up_to_date_prunes_when_explicit_flag method method test_pull_up_to_date_prunes_when_explicit_flag L266–288
+ _env function function _env L36–37
+ _init_fetch_repo function function _init_fetch_repo L160–167
+ _make_remote_info function function _make_remote_info L170–176
+ _write_remote_ref function function _write_remote_ref L40–43
+ cli variable variable cli L29–29
+ CliRunner import import CliRunner L25–25
+ InvokeResult import import InvokeResult L25–25
+ MagicMock import import MagicMock L21–21
+ Manifest import import Manifest L26–26
+ annotations import import annotations L16–16
+ blob_id import import blob_id L26–26
+ datetime import import datetime L18–18
+ json import import json L19–19
+ muse_dir import import muse_dir L27–27
+ patch import import patch L21–21
+ pathlib import import pathlib L20–20
+ pytest import import pytest L23–23
+ remotes_dir import import remotes_dir L27–27
+ pull_repo function function pull_repo L234–262
+ runner variable variable runner L30–30
~ docs/agent-guide.md .md 2 symbols added, 2 symbols modified
+ prune config — now works identically to git (muse#22) section fetch.prune config — now works identically to git (muse#22) L1470–1492
+ muse config set/get --global — now works identically to git section muse config set/get --global — now works identically to git L1493–1503
~ muse/cli/commands/config_cmd.py .py 1 symbol added, 3 symbols modified
+ pathlib import import pathlib L89–89
~ muse/cli/commands/fetch.py .py 1 symbol added, 7 symbols modified
~ muse/cli/commands/pull.py .py 2 symbols added, 8 symbols modified
+ _prune_stale_refs import import _prune_stale_refs L56–56
+ get_fetch_prune import import get_fetch_prune L48–48
~ run
~ muse/cli/config.py .py 2 symbols added, 6 symbols modified
+ FetchConfig class class FetchConfig L123–132
+ get_fetch_prune function function get_fetch_prune L1477–1497
Files Changed
+1 ~5
1256 in snapshot
← Older Oldest on feat/22-fetch-prune-config
All commits
Newer → Latest on feat/22-fetch-prune-config

0 comments

No comments yet. Be the first to start the discussion.

To add a comment, use the Muse CLI: muse hub commit comment sha256:ec9f1f4725bce6f66bf08946ea05413572841d9ba43e02df2f3d1cf94e14eb68 --body "your comment"