gabriel / muse public
feat BREAKING feat/54-reflog-remote-sharing #1 / 1
AI Agent gabriel · 7 hours ago · Oct 22, 2026 · Diff

feat: muse#54 item 4 — remote reflog sharing via push/pull

Reflogs were 100% local-only until now: a fresh clone started with zero ref-movement history. Implements remote sharing by mirroring the existing version-tags remote-sync precedent (muse#57 item 4) rather than embedding reflogs in the content-addressed mpack format, since reflog entries -- like version tags -- are per-ref metadata, not content-addressed objects.

Muse CLI: - `muse push --reflog` (or `push.reflog=true` config, new [push] key alongside the existing `tags`) sends this branch's local reflog entries via transport.push_reflog(), oldest-first, resolved before the "up to date" early return so entries sync even with no new commits (same reasoning as --tags). - `muse pull` always best-effort fetches via transport.fetch_reflog() and merges incoming entries into a NEW remote-tracking namespace, `.muse/logs/refs/remotes/<remote>/<branch>` (new path helpers reflog_remotes_dir/reflog_remote_path) -- never into the local branch's own reflog. This is what "namespaced by remote" means: mirrors git's own separation between a branch's reflog and its remote-tracking ref's reflog, so a remote's history can merge in without colliding with purely local activity. - muse/core/reflog.py: reflog_entries_to_wire() normalizes IDs to canonical sha256:<hex> form at the wire boundary; merge_remote_reflog_entries() does idempotent (exact-raw-line-comparison) append-only merging; read_remote_reflog() mirrors read_reflog()'s contract for the new namespace. - muse/core/transport.py: push_reflog()/fetch_reflog() on both the Protocol and HttpTransport, same POST/GET-with-msgpack-body shape as push_version_tags()/fetch_version_tags(). - `muse reflog --remote <name> --branch <branch>` reads the merged remote-tracking log back; --remote requires --branch (a remote's reflog is tracked per-branch) and is mutually exclusive with --tag/--all; remote names are checked for path-traversal characters before use in a filesystem path, same security model as --branch. - _PushJson gained always-present reflog_pushed/reflog_skipped fields (0/0 when --reflog isn't used) rather than a combinatorial subclass alongside the existing _PushWithTagsJson, since --tags and --reflog are independent flags that can combine four ways.

MuseHub server (~/ecosystem/musehub, shipped as a companion commit): - New `musehub_reflog_entries` table (migration 0080), keyed by a content-hash `entry_id` rather than a mutable (repo_id, branch) pointer like version tags -- re-pushing an already-stored entry is a true `ON CONFLICT DO NOTHING` no-op, since reflog entries are immutable history with nothing to conflict (unlike a tag, which can legitimately re-point to a different commit). - `musehub/services/musehub_reflog.py`: store_reflog_entries()/ list_reflog_entries(), sole owner of the table. - `wire_push_reflog`/`wire_get_reflog` routes on the existing bare-path wire.py router, using the new BranchQueryParam validator (branch isn't part of the URL path template here, unlike the existing Path-based BranchParam used elsewhere). The read route additionally calls _assert_readable (a gap the version-tags read route has today, deliberately not copied here since reflog entries can reveal private branch activity).

Covered by tests/test_reflog_remote_sharing.py (15 cases: wire normalization, core merge idempotency/isolation-per-remote/no-local- pollution, CLI --reflog flag + config flag + transport-not-called when disabled, pull merge + non-fatal fetch-failure handling, and `reflog --remote` read-back including validation errors) plus tests/test_musehub_reflog_service.py (6 cases, musehub repo: round-trip, dedup-skip, per-branch isolation, ordering, empty-batch, missing-new_id).

Confirmed genuine RED by reverting every touched muse-side file and re-running (ImportError: reflog_remote_path doesn't exist), then GREEN with the fix restored plus the full targeted regression suite (296 muse-side tests across push/pull/reflog/config areas, zero regressions; 80 musehub-side tests including migrations + ORM/schema-match checks). Live-verified end-to-end against the real local MuseHub hub (containers restarted, migration 0080 auto-applied, schema check passed): created a scratch repo, pushed two commits' reflog entries with --reflog (first attempt stored 2, idempotent re-push correctly skipped 2), then pulled from a fresh clone and confirmed the remote-tracking log (.muse/logs/refs/remotes/local/main) received the exact entries while the local branch's own reflog stayed untouched. Scratch repo deleted after verification.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

sha256:bbaa4948a0e72cfa68f5d62f76f176f0c11153b513826d6fc84df500a11db79c sha
+64 ~31 −1 symbols
9 changed · 1231 in snapshot files
sha256:5e3b93fb90efea099b98a936047d3cd3a2c3446207c4eec168374cb1374d9a92 snapshot
+64
symbols added
~31
symbols modified
−1
symbol removed
9
files changed
1231
files in snapshot
0
dead code introduced
Semantic Changes 96 symbols
~ tests/test_reflog_remote_sharing.py .py 47 symbols added
+ TestMergeRemoteReflogEntries class class TestMergeRemoteReflogEntries L109–178
+ test_does_not_touch_local_branch_reflog method method test_does_not_touch_local_branch_reflog L124–139
+ test_empty_entries_is_noop method method test_empty_entries_is_noop L176–178
+ test_idempotent_on_repeat_merge method method test_idempotent_on_repeat_merge L141–154
+ test_merges_new_entries_into_remote_namespace method method test_merges_new_entries_into_remote_namespace L110–122
+ test_separate_remotes_do_not_collide method method test_separate_remotes_do_not_collide L156–174
+ TestPullFetchesReflog class class TestPullFetchesReflog L243–310
+ test_pull_merges_reflog_entries method method test_pull_merges_reflog_entries L244–281
+ test_pull_reflog_fetch_failure_is_non_fatal method method test_pull_reflog_fetch_failure_is_non_fatal L283–310
+ TestPushReflogFlag class class TestPushReflogFlag L181–240
+ test_push_reflog_config_flag method method test_push_reflog_config_flag L222–240
+ test_push_with_reflog_calls_transport method method test_push_with_reflog_calls_transport L182–201
+ test_push_without_reflog_flag_does_not_call_transport method method test_push_without_reflog_flag_does_not_call_transport L203–220
+ TestReflogEntriesToWire class class TestReflogEntriesToWire L92–106
+ test_normalizes_ids_to_canonical_form method method test_normalizes_ids_to_canonical_form L93–106
+ TestReflogRemoteReadCli class class TestReflogRemoteReadCli L313–341
+ test_invalid_remote_name_rejected method method test_invalid_remote_name_rejected L338–341
+ test_reflog_remote_flag_reads_merged_entries method method test_reflog_remote_flag_reads_merged_entries L314–326
+ test_remote_and_tag_mutually_exclusive method method test_remote_and_tag_mutually_exclusive L333–336
+ test_remote_without_branch_errors method method test_remote_without_branch_errors L328–331
+ _commit_file function function _commit_file L61–67
+ _env function function _env L33–34
+ _fake_commit_id function function _fake_commit_id L70–71
+ _init_repo function function _init_repo L41–58
+ _mock_push_transport function function _mock_push_transport L74–89
+ _run function function _run L37–38
+ cli variable variable cli L29–29
+ CliRunner import import CliRunner L27–27
+ MagicMock import import MagicMock L14–14
+ NULL_COMMIT_ID import import NULL_COMMIT_ID L18–18
+ ReflogEntry import import ReflogEntry L20–20
+ annotations import import annotations L9–9
+ append_reflog import import append_reflog L20–20
+ datetime import import datetime L11–11
+ fake_id import import fake_id L18–18
+ json import import json L12–12
+ merge_remote_reflog_entries import import merge_remote_reflog_entries L20–20
+ muse_dir import import muse_dir L19–19
+ patch import import patch L14–14
+ pathlib import import pathlib L13–13
+ pytest import import pytest L16–16
+ read_remote_reflog import import read_remote_reflog L20–20
+ ref_path import import ref_path L19–19
+ reflog_branch_path import import reflog_branch_path L19–19
+ reflog_entries_to_wire import import reflog_entries_to_wire L20–20
+ reflog_remote_path import import reflog_remote_path L19–19
+ runner variable variable runner L30–30
~ docs/issues/reflog-followup.md .md 1 symbol added, 1 symbol removed, 2 symbols modified
− Remote reflog sharing section 4. Remote reflog sharing L63–72
+ Remote reflog sharing ✅ IMPLEMENTED section 4. Remote reflog sharing ✅ IMPLEMENTED L63–95
~ muse/cli/commands/pull.py .py 1 symbol added, 2 symbols modified
+ merge_remote_reflog_entries import import merge_remote_reflog_entries L64–64
~ run
~ muse/cli/commands/push.py .py 2 symbols added, 3 symbols modified
+ read_reflog import import read_reflog L79–79
+ reflog_entries_to_wire import import reflog_entries_to_wire L79–79
~ run
~ muse/cli/commands/reflog.py .py 1 symbol added, 10 symbols modified
~ muse/core/paths.py .py 2 symbols added
+ reflog_remote_path function function reflog_remote_path L254–256
+ reflog_remotes_dir function function reflog_remotes_dir L243–252
~ muse/core/reflog.py .py 6 symbols added, 7 symbols modified
+ _reflog_remote_path import import _reflog_remote_path L60–60
+ _reflog_remotes_dir import import _reflog_remotes_dir L60–60
+ long_id import import long_id L59–59
+ merge_remote_reflog_entries function function merge_remote_reflog_entries L370–442
+ read_remote_reflog function function read_remote_reflog L445–482
+ reflog_entries_to_wire function function reflog_entries_to_wire L349–367
~ muse/core/transport.py .py 4 symbols added, 2 symbols modified
+ fetch_reflog method method fetch_reflog L1298–1316
+ push_reflog method method push_reflog L1279–1296
+ fetch_reflog method method fetch_reflog L530–545
+ push_reflog method method push_reflog L508–528
Files Changed
+1 ~8
1231 in snapshot
← Older Oldest on feat/54-reflog-remote-sharing
All commits
Newer → Latest on feat/54-reflog-remote-sharing

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:bbaa4948a0e72cfa68f5d62f76f176f0c11153b513826d6fc84df500a11db79c --body "your comment"