"""muse#54 item 4 -- remote reflog sharing. Covers: - core: reflog_entries_to_wire() / merge_remote_reflog_entries() / read_remote_reflog() - CLI: `muse push --reflog` sends local entries, `muse pull` fetches and merges them into the remote-tracking namespace, `muse reflog --remote` reads them back. """ from __future__ import annotations import datetime import json import pathlib from unittest.mock import MagicMock, patch import pytest from muse.core.types import fake_id, NULL_COMMIT_ID from muse.core.paths import muse_dir, ref_path, reflog_branch_path, reflog_remote_path from muse.core.reflog import ( ReflogEntry, append_reflog, merge_remote_reflog_entries, read_remote_reflog, reflog_entries_to_wire, ) from tests.cli_test_helper import CliRunner cli = None runner = CliRunner() def _env(root: pathlib.Path) -> dict: return {"MUSE_REPO_ROOT": str(root)} def _run(root: pathlib.Path, *args: str): return runner.invoke(cli, list(args), env=_env(root)) def _init_repo(tmp_path: pathlib.Path) -> tuple[pathlib.Path, str]: dot_muse = muse_dir(tmp_path) dot_muse.mkdir() repo_id = fake_id("repo") (dot_muse / "repo.json").write_text(json.dumps({ "repo_id": repo_id, "domain": "code", "default_branch": "main", "created_at": "2025-01-01T00:00:00+00:00", }), encoding="utf-8") (dot_muse / "HEAD").write_text("ref: refs/heads/main\n", encoding="utf-8") (dot_muse / "refs" / "heads").mkdir(parents=True) (dot_muse / "snapshots").mkdir() (dot_muse / "commits").mkdir() (dot_muse / "objects").mkdir() (dot_muse / "config.toml").write_text( '[remotes.local]\nurl = "https://localhost:1337/gabriel/myrepo"\n', encoding="utf-8", ) return tmp_path, repo_id def _commit_file(root: pathlib.Path, content: str, message: str) -> str: (root / "f.txt").write_text(content) add = _run(root, "code", "add", "f.txt") assert add.exit_code == 0, add.output commit = _run(root, "commit", "-m", message, "--json") assert commit.exit_code == 0, commit.output return json.loads(commit.output)["commit_id"] def _fake_commit_id(n: int) -> str: return f"{n:064x}" def _mock_push_transport(local_head: str) -> MagicMock: from muse.core.mpack import RemoteInfo transport = MagicMock() transport.fetch_remote_info.return_value = RemoteInfo( domain="code", default_branch="main", branch_heads={}, ) transport.push_mpack_presign.return_value = { "upload_url": "https://minio.example.com/put?sig=x", "mpack_key": "sha256:" + "a" * 64, } transport.push_mpack_put.return_value = None transport.push_mpack_unpack.return_value = { "job_id": "j", "head": local_head, "branch": "main", "blobs_in_mpack": 0, "commits_in_mpack": 1, } return transport class TestReflogEntriesToWire: def test_normalizes_ids_to_canonical_form(self) -> None: entry = ReflogEntry( old_id=NULL_COMMIT_ID, new_id="a" * 64, author="gabriel", timestamp=datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc), operation="commit: c1", ) wire = reflog_entries_to_wire([entry]) assert len(wire) == 1 assert wire[0]["old_id"] == f"sha256:{NULL_COMMIT_ID}" assert wire[0]["new_id"] == "sha256:" + "a" * 64 assert wire[0]["operation"] == "commit: c1" assert wire[0]["author"] == "gabriel" class TestMergeRemoteReflogEntries: def test_merges_new_entries_into_remote_namespace(self, tmp_path: pathlib.Path) -> None: root, repo_id = _init_repo(tmp_path) wire_entries = [{ "old_id": NULL_COMMIT_ID, "new_id": _fake_commit_id(1), "author": "gabriel", "timestamp": "2026-01-01T00:00:00+00:00", "operation": "commit: c1", }] merged = merge_remote_reflog_entries(root, "local", "main", wire_entries) assert merged == 1 entries = read_remote_reflog(root, "local", "main") assert len(entries) == 1 assert entries[0].new_id == _fake_commit_id(1) def test_does_not_touch_local_branch_reflog(self, tmp_path: pathlib.Path) -> None: """Merging remote entries must never write into the local branch's own reflog -- only its remote-tracking namespace.""" root, repo_id = _init_repo(tmp_path) local_log = reflog_branch_path(root, "main") assert not local_log.exists() merge_remote_reflog_entries(root, "local", "main", [{ "old_id": NULL_COMMIT_ID, "new_id": _fake_commit_id(1), "author": "gabriel", "timestamp": "2026-01-01T00:00:00+00:00", "operation": "commit: c1", }]) assert not local_log.exists() remote_log = reflog_remote_path(root, "local", "main") assert remote_log.exists() def test_idempotent_on_repeat_merge(self, tmp_path: pathlib.Path) -> None: root, repo_id = _init_repo(tmp_path) wire_entries = [{ "old_id": NULL_COMMIT_ID, "new_id": _fake_commit_id(1), "author": "gabriel", "timestamp": "2026-01-01T00:00:00+00:00", "operation": "commit: c1", }] first = merge_remote_reflog_entries(root, "local", "main", wire_entries) second = merge_remote_reflog_entries(root, "local", "main", wire_entries) assert first == 1 assert second == 0 entries = read_remote_reflog(root, "local", "main") assert len(entries) == 1 def test_separate_remotes_do_not_collide(self, tmp_path: pathlib.Path) -> None: root, repo_id = _init_repo(tmp_path) merge_remote_reflog_entries(root, "local", "main", [{ "old_id": NULL_COMMIT_ID, "new_id": _fake_commit_id(1), "author": "alice", "timestamp": "2026-01-01T00:00:00+00:00", "operation": "commit: c1", }]) merge_remote_reflog_entries(root, "staging", "main", [{ "old_id": NULL_COMMIT_ID, "new_id": _fake_commit_id(2), "author": "bob", "timestamp": "2026-01-01T00:00:00+00:00", "operation": "commit: c2", }]) local_entries = read_remote_reflog(root, "local", "main") staging_entries = read_remote_reflog(root, "staging", "main") assert len(local_entries) == 1 assert len(staging_entries) == 1 assert local_entries[0].new_id == _fake_commit_id(1) assert staging_entries[0].new_id == _fake_commit_id(2) def test_empty_entries_is_noop(self, tmp_path: pathlib.Path) -> None: root, repo_id = _init_repo(tmp_path) assert merge_remote_reflog_entries(root, "local", "main", []) == 0 class TestPushReflogFlag: def test_push_with_reflog_calls_transport( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> None: root, repo_id = _init_repo(tmp_path) commit_id = _commit_file(root, "v1\n", "c1") monkeypatch.setenv("MUSE_REPO_ROOT", str(root)) monkeypatch.chdir(root) transport = _mock_push_transport(commit_id) transport.push_reflog.return_value = {"stored": 1, "skipped": 0} with patch("muse.cli.commands.push.make_transport", return_value=transport): result = _run(root, "push", "local", "main", "--reflog", "--json") assert result.exit_code == 0, result.output data = json.loads(result.output) assert data["reflog_pushed"] == 1 transport.push_reflog.assert_called_once() call_args = transport.push_reflog.call_args assert call_args[0][2] == "main" # branch positional arg def test_push_without_reflog_flag_does_not_call_transport( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> None: root, repo_id = _init_repo(tmp_path) commit_id = _commit_file(root, "v1\n", "c1") monkeypatch.setenv("MUSE_REPO_ROOT", str(root)) monkeypatch.chdir(root) transport = _mock_push_transport(commit_id) with patch("muse.cli.commands.push.make_transport", return_value=transport): result = _run(root, "push", "local", "main", "--json") assert result.exit_code == 0, result.output data = json.loads(result.output) assert data["reflog_pushed"] == 0 assert data["reflog_skipped"] == 0 transport.push_reflog.assert_not_called() def test_push_reflog_config_flag( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> None: root, repo_id = _init_repo(tmp_path) commit_id = _commit_file(root, "v1\n", "c1") monkeypatch.setenv("MUSE_REPO_ROOT", str(root)) monkeypatch.chdir(root) config_result = _run(root, "config", "set", "push.reflog", "true", "--json") assert config_result.exit_code == 0, config_result.output transport = _mock_push_transport(commit_id) transport.push_reflog.return_value = {"stored": 1, "skipped": 0} with patch("muse.cli.commands.push.make_transport", return_value=transport): result = _run(root, "push", "local", "main", "--json") assert result.exit_code == 0, result.output transport.push_reflog.assert_called_once() class TestPullFetchesReflog: def test_pull_merges_reflog_entries( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> None: root, repo_id = _init_repo(tmp_path) commit_id = _commit_file(root, "v1\n", "c1") monkeypatch.setenv("MUSE_REPO_ROOT", str(root)) monkeypatch.chdir(root) from muse.core.mpack import RemoteInfo transport = MagicMock() transport.fetch_remote_info.return_value = RemoteInfo( domain="code", default_branch="main", branch_heads={"main": commit_id}, ) transport.fetch_mpack.return_value = { "repo_id": repo_id, "domain": "code", "default_branch": "main", "branch_heads": {"main": commit_id}, "commits": [], "snapshots": [], "blobs": [], "blobs_received": 0, "shallow_commits": [], } transport.fetch_version_tags.return_value = {"tags": []} transport.fetch_reflog.return_value = {"entries": [ { "old_id": NULL_COMMIT_ID, "new_id": commit_id, "author": "gabriel", "timestamp": "2026-01-01T00:00:00+00:00", "operation": "commit: c1", }, ]} with patch("muse.cli.commands.pull.make_transport", return_value=transport): result = _run(root, "pull", "local", "main", "--json") assert result.exit_code == 0, result.output transport.fetch_reflog.assert_called_once() entries = read_remote_reflog(root, "local", "main") assert len(entries) == 1 assert entries[0].new_id == commit_id.split(":", 1)[1] def test_pull_reflog_fetch_failure_is_non_fatal( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> None: root, repo_id = _init_repo(tmp_path) commit_id = _commit_file(root, "v1\n", "c1") monkeypatch.setenv("MUSE_REPO_ROOT", str(root)) monkeypatch.chdir(root) from muse.core.mpack import RemoteInfo from muse.core.transport import TransportError transport = MagicMock() transport.fetch_remote_info.return_value = RemoteInfo( domain="code", default_branch="main", branch_heads={"main": commit_id}, ) transport.fetch_mpack.return_value = { "repo_id": repo_id, "domain": "code", "default_branch": "main", "branch_heads": {"main": commit_id}, "commits": [], "snapshots": [], "blobs": [], "blobs_received": 0, "shallow_commits": [], } transport.fetch_version_tags.return_value = {"tags": []} transport.fetch_reflog.side_effect = TransportError("boom", 500) with patch("muse.cli.commands.pull.make_transport", return_value=transport): result = _run(root, "pull", "local", "main", "--json") assert result.exit_code == 0, result.output class TestReflogRemoteReadCli: def test_reflog_remote_flag_reads_merged_entries(self, tmp_path: pathlib.Path) -> None: root, repo_id = _init_repo(tmp_path) merge_remote_reflog_entries(root, "local", "main", [{ "old_id": NULL_COMMIT_ID, "new_id": _fake_commit_id(1), "author": "gabriel", "timestamp": "2026-01-01T00:00:00+00:00", "operation": "commit: c1", }]) result = _run(root, "reflog", "--remote", "local", "--branch", "main", "--json") assert result.exit_code == 0, result.output data = json.loads(result.output) assert data["ref"] == "refs/remotes/local/main" assert data["total"] == 1 def test_remote_without_branch_errors(self, tmp_path: pathlib.Path) -> None: root, repo_id = _init_repo(tmp_path) result = _run(root, "reflog", "--remote", "local", "--json") assert result.exit_code == 1 def test_remote_and_tag_mutually_exclusive(self, tmp_path: pathlib.Path) -> None: root, repo_id = _init_repo(tmp_path) result = _run(root, "reflog", "--remote", "local", "--branch", "main", "--tag", "v1.0.0", "--json") assert result.exit_code == 1 def test_invalid_remote_name_rejected(self, tmp_path: pathlib.Path) -> None: root, repo_id = _init_repo(tmp_path) result = _run(root, "reflog", "--remote", "../escape", "--branch", "main", "--json") assert result.exit_code == 1