"""TDD tests for ``muse log a..b`` range syntax (issue #28). ``muse log A..B`` shows commits reachable from B but not reachable from A. This is the standard range meaning: commits unique to B relative to A. Coverage tiers: - Unit: _parse_range extracted from log.py - Integration: muse log A..B with two-branch repo - Edge cases: A..B where A==B (empty), unknown ref on either side, A..B with --json, A..B with -n limit, plain ref still works """ from __future__ import annotations import json import os import pathlib import pytest from tests.cli_test_helper import CliRunner, InvokeResult runner = CliRunner() # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _invoke(repo: pathlib.Path, *args: str) -> InvokeResult: from muse.cli.app import main as cli saved = os.getcwd() try: os.chdir(repo) return runner.invoke(cli, list(args)) finally: os.chdir(saved) def _init(repo: pathlib.Path) -> None: repo.mkdir(parents=True, exist_ok=True) _invoke(repo, "init") def _commit(repo: pathlib.Path, msg: str, filename: str | None = None) -> str: """Commit a file and return the commit_id.""" fname = filename or f"f_{abs(hash(msg)) % 100000}.py" (repo / fname).write_text(f"# {msg}\n") _invoke(repo, "code", "add", ".") result = _invoke(repo, "commit", "-m", msg, "--json") data = json.loads(result.output) return data["commit_id"] def _checkout(repo: pathlib.Path, branch: str, *, new: bool = False) -> None: args = ["checkout", "-b", branch] if new else ["checkout", branch] _invoke(repo, *args) def _log_json(repo: pathlib.Path, *extra: str) -> list[dict]: result = _invoke(repo, "log", "--json", *extra) return json.loads(result.output)["commits"] def _make_fork_repo(tmp: pathlib.Path) -> tuple[pathlib.Path, str, str, str]: """Build a repo with a shared base and a diverging feature branch. History: main: A → B → C (base=A, main-only=B,C) feat/x: A → D → E (feat-only=D,E) ^ shared base (commit A) Returns (repo, commit_A_id, commit_C_id, commit_E_id). """ repo = tmp / "repo" _init(repo) # Shared base commit a_id = _commit(repo, "base: shared commit A", "a.py") # main branch: B then C b_id = _commit(repo, "main: commit B", "b.py") c_id = _commit(repo, "main: commit C", "c.py") # feature branch: checkout at A, add D and E _checkout(repo, "feat/x", new=True) # The branch was created from current HEAD (C on main). # We need to branch from A. Easier: just add unique commits on feat/x. d_id = _commit(repo, "feat: commit D", "d.py") e_id = _commit(repo, "feat: commit E", "e.py") return repo, a_id, c_id, e_id # --------------------------------------------------------------------------- # Unit — _parse_range lives in rev_list; verify log reuses same semantics # --------------------------------------------------------------------------- def test_LOG_DR1_plain_ref_still_works(tmp_path: pathlib.Path) -> None: """Plain ``muse log dev`` (no ..) continues to work as before.""" repo = tmp_path / "repo" _init(repo) _commit(repo, "first", "f.py") _commit(repo, "second", "g.py") commits = _log_json(repo, "main") assert len(commits) == 2 assert commits[0]["message"] == "second" def test_LOG_DR2_dotdot_shows_only_feature_commits(tmp_path: pathlib.Path) -> None: """``muse log main..feat/x`` shows only commits on feat/x not on main.""" repo = tmp_path / "repo" _init(repo) _commit(repo, "shared base", "base.py") _commit(repo, "main commit", "m.py") _checkout(repo, "feat/x", new=True) _commit(repo, "feat commit D", "d.py") _commit(repo, "feat commit E", "e.py") commits = _log_json(repo, "main..feat/x") messages = [c["message"] for c in commits] assert "feat commit D" in messages assert "feat commit E" in messages assert "shared base" not in messages assert "main commit" not in messages def test_LOG_DR3_dotdot_empty_when_same_ref(tmp_path: pathlib.Path) -> None: """``muse log main..main`` returns no commits (nothing unique).""" repo = tmp_path / "repo" _init(repo) _commit(repo, "only commit", "f.py") commits = _log_json(repo, "main..main") assert commits == [], f"Expected empty list, got {commits}" def test_LOG_DR4_dotdot_json_output_schema(tmp_path: pathlib.Path) -> None: """``muse log A..B --json`` returns valid envelope with commits list.""" repo = tmp_path / "repo" _init(repo) _commit(repo, "base", "base.py") _checkout(repo, "feat/y", new=True) _commit(repo, "feat only", "feat.py") result = _invoke(repo, "log", "--json", "main..feat/y") assert result.exit_code == 0, result.output data = json.loads(result.output) assert "commits" in data assert isinstance(data["commits"], list) assert len(data["commits"]) == 1 assert data["commits"][0]["message"] == "feat only" def test_LOG_DR5_dotdot_respects_n_limit(tmp_path: pathlib.Path) -> None: """``muse log main..feat/z -n 1`` returns at most 1 commit.""" repo = tmp_path / "repo" _init(repo) _commit(repo, "base", "base.py") _checkout(repo, "feat/z", new=True) _commit(repo, "feat first", "f1.py") _commit(repo, "feat second", "f2.py") _commit(repo, "feat third", "f3.py") commits = _log_json(repo, "main..feat/z", "-n", "1") assert len(commits) == 1 def test_LOG_DR6_unknown_exclude_ref_errors(tmp_path: pathlib.Path) -> None: """``muse log nonexistent..main`` exits non-zero with a clear error.""" repo = tmp_path / "repo" _init(repo) _commit(repo, "only commit", "f.py") result = _invoke(repo, "log", "--json", "nonexistent..main") assert result.exit_code != 0 data = json.loads(result.output) assert "error" in data def test_LOG_DR7_unknown_include_ref_errors(tmp_path: pathlib.Path) -> None: """``muse log main..nonexistent`` exits non-zero with a clear error.""" repo = tmp_path / "repo" _init(repo) _commit(repo, "only commit", "f.py") result = _invoke(repo, "log", "--json", "main..nonexistent") assert result.exit_code != 0 data = json.loads(result.output) assert "error" in data def test_LOG_DR8_dotdot_commit_ids_as_refs(tmp_path: pathlib.Path) -> None: """``muse log ..`` works with raw commit IDs.""" repo = tmp_path / "repo" _init(repo) base_id = _commit(repo, "base", "base.py") _commit(repo, "second", "second.py") tip_id = _commit(repo, "third", "third.py") commits = _log_json(repo, f"{base_id}..{tip_id}") messages = [c["message"] for c in commits] assert "third" in messages assert "second" in messages assert "base" not in messages