Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions src/git_pulsar/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
108 changes: 67 additions & 41 deletions src/git_pulsar/ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@
import time
from pathlib import Path

from rich.console import Console
from rich.panel import Panel

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."""
Expand Down Expand Up @@ -173,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}
Expand Down Expand Up @@ -220,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
Expand Down Expand Up @@ -265,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
Expand All @@ -302,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)...")
Expand Down Expand Up @@ -350,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:
Expand Down
4 changes: 2 additions & 2 deletions tests/test_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down