From 72c858275e18a819770d1385aa9ba55839419593 Mon Sep 17 00:00:00 2001 From: Jackson Ferguson Date: Sat, 31 Jan 2026 21:58:18 -0800 Subject: [PATCH 1/6] feat(daemon): use rich spinner for interactive push feedback --- src/git_pulsar/daemon.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/git_pulsar/daemon.py b/src/git_pulsar/daemon.py index d5c8ca1..33ad876 100644 --- a/src/git_pulsar/daemon.py +++ b/src/git_pulsar/daemon.py @@ -15,6 +15,8 @@ from types import FrameType from typing import Iterator +from rich.console import Console + from . import ops from .constants import ( APP_NAME, @@ -303,12 +305,26 @@ def _attempt_push(repo: GitRepo, refspec: str, interactive: bool) -> None: try: env = os.environ.copy() env["GIT_SSH_COMMAND"] = "ssh -o BatchMode=yes" + cmd = ["push", remote_name, refspec] + + if interactive: + console = Console() + with console.status( + f"[bold blue]Pushing {repo.path.name}...[/bold blue]", spinner="dots" + ): + # capture=True suppresses the "Enumerating objects..." wall of text + repo._run(cmd, capture=True, env=env) + console.print(f"[bold green]✔ {repo.path.name}: Pushed.[/bold green]") + else: + # Background mode: log to file/stderr + repo._run(cmd, capture=True, env=env) + logger.info(f"SUCCESS {repo.path.name}: Pushed.") - # Push specific refspec - repo._run(["push", remote_name, refspec], capture=False, env=env) - logger.info(f"SUCCESS {repo.path.name}: Pushed.") except Exception as e: - logger.error(f"PUSH ERROR {repo.path.name}: {e}") + if interactive: + Console().print(f"[bold red]✘ PUSH ERROR {repo.path.name}: {e}[/bold red]") + else: + logger.error(f"PUSH ERROR {repo.path.name}: {e}") def run_backup(original_path_str: str, interactive: bool = False) -> None: From 71b37f62cc9ff7579c797f6d8a80d68827b9d552 Mon Sep 17 00:00:00 2001 From: Jackson Ferguson Date: Sat, 31 Jan 2026 21:59:23 -0800 Subject: [PATCH 2/6] refactor(ops): initialize rich console for styled output --- src/git_pulsar/ops.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/git_pulsar/ops.py b/src/git_pulsar/ops.py index 297e61b..c05ead1 100644 --- a/src/git_pulsar/ops.py +++ b/src/git_pulsar/ops.py @@ -7,10 +7,14 @@ import time from pathlib import Path +from rich.console import Console + from .constants import BACKUP_NAMESPACE from .git_wrapper import GitRepo from .system import get_machine_id, get_machine_id_file +console = Console() + def get_backup_ref(branch: str) -> str: """Constructs the namespaced ref for the current machine/branch.""" From 0cc50ecdaa03d959ebf5ea5fb7b3e0b72c8c2a06 Mon Sep 17 00:00:00 2001 From: Jackson Ferguson Date: Sat, 31 Jan 2026 22:03:03 -0800 Subject: [PATCH 3/6] feat(ops): add spinner and panel to sync_session command --- src/git_pulsar/ops.py | 44 +++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/src/git_pulsar/ops.py b/src/git_pulsar/ops.py index c05ead1..871404a 100644 --- a/src/git_pulsar/ops.py +++ b/src/git_pulsar/ops.py @@ -8,6 +8,7 @@ from pathlib import Path from rich.console import Console +from rich.panel import Panel from .constants import BACKUP_NAMESPACE from .git_wrapper import GitRepo @@ -177,20 +178,25 @@ def sync_session() -> None: repo = GitRepo(Path.cwd()) current_branch = repo.current_branch() - print(f"📡 Scanning for latest session on '{current_branch}'...") - # 1. Fetch everything (all machines) - try: - repo._run( - [ - "fetch", - "origin", - f"refs/heads/{BACKUP_NAMESPACE}/*:refs/heads/{BACKUP_NAMESPACE}/*", - ], - capture=False, - ) - except Exception: - print("⚠️ Fetch warning: network might be down.") + with console.status( + f"[bold blue]Scanning for session on '{current_branch}'...[/bold blue]", + spinner="dots", + ): + try: + repo._run( + [ + "fetch", + "origin", + f"refs/heads/{BACKUP_NAMESPACE}/*:refs/heads/{BACKUP_NAMESPACE}/*", + ], + capture=True, # <--- Changed to True to keep spinner clean + ) + except Exception: + console.print( + "[yellow]⚠️ Fetch warning: network might be down " + "(checking local cache).[/yellow]" + ) # 2. Find candidates # Pattern: refs/heads/{namespace}/{machine}/{branch} @@ -224,9 +230,15 @@ def sync_session() -> None: machine_name = latest_ref.split("/")[-2] human_time = repo._run(["log", "-1", "--format=%cr", latest_ref]) - print("\n🎯 Found latest session:") - print(f" • Source: {machine_name}") - print(f" • Time: {human_time}") + # Replace plain print with a Panel + console.print( + Panel( + f"[bold]Source:[/bold] {machine_name}\n[bold]Time:[/bold] {human_time}", + title="🎯 Latest Session Found", + border_style="green", + expand=False, + ) + ) # Check if this IS our current state (approx) local_tree = repo.write_tree() # Current worktree state From 9db6c0223eda717443d33ff53c35cdb38647450e Mon Sep 17 00:00:00 2001 From: Jackson Ferguson Date: Sat, 31 Jan 2026 22:05:22 -0800 Subject: [PATCH 4/6] feat(ops): silence fetch and merge output in finalize_work --- src/git_pulsar/ops.py | 50 ++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/src/git_pulsar/ops.py b/src/git_pulsar/ops.py index 871404a..ca92104 100644 --- a/src/git_pulsar/ops.py +++ b/src/git_pulsar/ops.py @@ -281,21 +281,22 @@ def finalize_work() -> None: working_branch = repo.current_branch() try: - # 2. Sync with Remote (Anti-Race + Backup Aggregation) - print("-> Syncing with origin...") - try: - # Fetch main AND all pulsar backups to ensure we see 'library' work - repo._run(["fetch", "origin", "main"], capture=False) - repo._run( - [ - "fetch", - "origin", - f"refs/heads/{BACKUP_NAMESPACE}/*:refs/heads/{BACKUP_NAMESPACE}/*", - ], - capture=False, - ) - except Exception as e: - print(f"⚠️ Fetch warning: {e}") + # 2. Sync with Remote + with console.status( + "[bold blue]Syncing with origin...[/bold blue]", spinner="dots" + ): + try: + repo._run(["fetch", "origin", "main"], capture=True) + repo._run( + [ + "fetch", + "origin", + f"refs/heads/{BACKUP_NAMESPACE}/*:refs/heads/{BACKUP_NAMESPACE}/*", + ], + capture=True, + ) + except Exception as e: + console.print(f"[yellow]⚠️ Fetch warning: {e}[/yellow]") # 3. Identify Backup Candidates # Find ALL refs that match: refs/heads/{namespace}/*/current_branch @@ -318,13 +319,18 @@ def finalize_work() -> None: repo.checkout(target) # 5. Octopus Squash - print("-> Collapsing backup streams...") - try: - repo.merge_squash(*candidates) - except RuntimeError: - print("⚠️ Merge conflicts detected. Please resolve them, then commit.") - # We exit here to let the user resolve conflicts manually - sys.exit(0) + with console.status( + f"[bold blue]Collapsing {len(candidates)} backup streams...[/bold blue]", + spinner="dots", + ): + try: + repo.merge_squash(*candidates) + except RuntimeError: + console.print( + "[bold red]⚠️ Merge conflicts detected. Please resolve " + "them, then commit.[/bold red]" + ) + sys.exit(0) # 5. Commit (Interactive) print("-> Committing (opens editor)...") From a6a41c745ae511f786a8bf0e4ecbf06c5752c47d Mon Sep 17 00:00:00 2001 From: Jackson Ferguson Date: Sat, 31 Jan 2026 22:06:37 -0800 Subject: [PATCH 5/6] feat(ops): add spinner for git gc and prune operations --- src/git_pulsar/ops.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/git_pulsar/ops.py b/src/git_pulsar/ops.py index ca92104..53758f3 100644 --- a/src/git_pulsar/ops.py +++ b/src/git_pulsar/ops.py @@ -372,10 +372,14 @@ def prune_backups(days: int, repo_path: Path | None = None) -> None: continue if deleted_count == 0: - print("✨ No stale backups found.") + console.print("[dim]✨ No stale backups found.[/dim]") else: - print(f"💀 Dropped {deleted_count} stale refs. Running git gc...") - repo._run(["gc", "--auto"], capture=False) + console.print(f"[bold red]💀 Dropped {deleted_count} stale refs.[/bold red]") + with console.status( + "[bold blue]Running garbage collection (git gc)...[/bold blue]", + spinner="dots", + ): + repo._run(["gc", "--auto"], capture=True) def add_ignore(pattern: str) -> None: From f829819f9236b346ea4d77f5d10e15bdd1d6d672 Mon Sep 17 00:00:00 2001 From: Jackson Ferguson Date: Sat, 31 Jan 2026 22:12:57 -0800 Subject: [PATCH 6/6] test(ops): update assertions to match captured git output --- tests/test_ops.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_ops.py b/tests/test_ops.py index 39f970f..3c0d97d 100644 --- a/tests/test_ops.py +++ b/tests/test_ops.py @@ -143,7 +143,7 @@ def mock_run(cmd: list[str], *args: Any, **kwargs: Any) -> str: "origin", f"refs/heads/{BACKUP_NAMESPACE}/*:refs/heads/{BACKUP_NAMESPACE}/*", ], - capture=False, + capture=True, ) # Verify we checked out the Desktop ref (newer) @@ -178,7 +178,7 @@ def test_finalize_octopus_merge(mocker: MagicMock) -> None: "origin", f"refs/heads/{BACKUP_NAMESPACE}/*:refs/heads/{BACKUP_NAMESPACE}/*", ], - capture=False, + capture=True, ) # 2. Verify Octopus Merge