cgcardona / muse public
fetch.py python
100 lines 3.4 KB
99c4ff79 feat: add remote sync commands (remote, clone, fetch, pull, push, ls-remote) Gabriel Cardona <cgcardona@gmail.com> 1d ago
1 """muse fetch — download commits, snapshots, and objects from a remote.
2
3 Fetches the latest state of a remote branch without touching the local branch
4 HEAD or working tree. After a successful fetch:
5
6 - All new commits, snapshots, and objects from the remote are stored locally.
7 - The remote tracking pointer ``.muse/remotes/<remote>/<branch>`` is updated.
8
9 Use ``muse pull`` to fetch *and* merge into the current branch, or run
10 ``muse merge`` after fetching to integrate on your own schedule.
11 """
12
13 from __future__ import annotations
14
15 import logging
16 import pathlib
17
18 import typer
19
20 from muse.cli.config import get_auth_token, get_remote, get_upstream, set_remote_head
21 from muse.core.errors import ExitCode
22 from muse.core.pack import apply_pack
23 from muse.core.repo import require_repo
24 from muse.core.store import get_all_commits
25 from muse.core.transport import HttpTransport, TransportError
26
27 logger = logging.getLogger(__name__)
28
29 app = typer.Typer()
30
31
32 def _current_branch(root: pathlib.Path) -> str:
33 """Return the current branch name from ``.muse/HEAD``."""
34 head_ref = (root / ".muse" / "HEAD").read_text().strip()
35 return head_ref.removeprefix("refs/heads/").strip()
36
37
38 @app.callback(invoke_without_command=True)
39 def fetch(
40 ctx: typer.Context,
41 remote: str = typer.Argument(
42 "origin", help="Remote name to fetch from (default: origin)."
43 ),
44 branch: str | None = typer.Option(
45 None, "--branch", "-b", help="Remote branch to fetch (default: tracked branch or current branch)."
46 ),
47 ) -> None:
48 """Download commits, snapshots, and objects from a remote.
49
50 Updates the remote tracking pointer but does NOT change the local branch
51 HEAD or working tree. Run ``muse pull`` to fetch and merge in one step.
52 """
53 root = require_repo()
54
55 url = get_remote(remote, root)
56 if url is None:
57 typer.echo(f"❌ Remote '{remote}' is not configured.")
58 typer.echo(f" Add it with: muse remote add {remote} <url>")
59 raise typer.Exit(code=ExitCode.USER_ERROR)
60
61 token = get_auth_token(root)
62 current_branch = _current_branch(root)
63 target_branch = branch or get_upstream(current_branch, root) or current_branch
64
65 transport = HttpTransport()
66
67 try:
68 info = transport.fetch_remote_info(url, token)
69 except TransportError as exc:
70 typer.echo(f"❌ Cannot reach remote '{remote}': {exc}")
71 raise typer.Exit(code=ExitCode.INTERNAL_ERROR)
72
73 remote_commit_id = info["branch_heads"].get(target_branch)
74 if remote_commit_id is None:
75 typer.echo(
76 f"❌ Branch '{target_branch}' does not exist on remote '{remote}'."
77 )
78 raise typer.Exit(code=ExitCode.USER_ERROR)
79
80 # Collect local commit IDs so the server can send only the delta.
81 local_commit_ids = [c.commit_id for c in get_all_commits(root)]
82
83 typer.echo(f"Fetching {remote}/{target_branch} …")
84
85 try:
86 bundle = transport.fetch_pack(
87 url, token, want=[remote_commit_id], have=local_commit_ids
88 )
89 except TransportError as exc:
90 typer.echo(f"❌ Fetch failed: {exc}")
91 raise typer.Exit(code=ExitCode.INTERNAL_ERROR)
92
93 new_objects = apply_pack(root, bundle)
94 set_remote_head(remote, target_branch, remote_commit_id, root)
95
96 commits_received = len(bundle.get("commits") or [])
97 typer.echo(
98 f"✅ Fetched {commits_received} commit(s), {new_objects} new object(s) "
99 f"from {remote}/{target_branch} ({remote_commit_id[:8]})"
100 )