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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 🔭 Git Pulsar (v0.10.0)
# 🔭 Git Pulsar (v0.10.1)

[![Tests](https://github.com/jacksonfergusondev/git-pulsar/actions/workflows/ci.yml/badge.svg)](https://github.com/jacksonfergusondev/git-pulsar/actions)
[![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
Expand Down
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "git-pulsar"
version = "0.10.0"
version = "0.10.1"
description = "Automated, paranoid git backups for students and casual coding."
readme = "README.md"
authors = [
Expand All @@ -14,8 +14,8 @@ requires = ["hatchling"]
build-backend = "hatchling.build"

[project.scripts]
git-pulsar = "src.cli:main"
git-pulsar-daemon = "src.daemon:main"
git-pulsar = "git_pulsar.cli:main"
git-pulsar-daemon = "git_pulsar.daemon:main"

[dependency-groups]
dev = [
Expand Down Expand Up @@ -54,7 +54,7 @@ pythonpath = ["."]
omit = ["src/service.py"]

[tool.bumpversion]
current_version = "0.10.0"
current_version = "0.10.1"
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
serialize = ["{major}.{minor}.{patch}"]
search = "{current_version}"
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion src/system.py → src/git_pulsar/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import sys
from pathlib import Path

from constants import MACHINE_ID_FILE
from .constants import MACHINE_ID_FILE


class SystemStrategy:
Expand Down
16 changes: 8 additions & 8 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import pytest

from src import cli
from git_pulsar import cli


def test_setup_repo_initializes_git(tmp_path: Path, mocker: MagicMock) -> None:
Expand All @@ -15,7 +15,7 @@ def test_setup_repo_initializes_git(tmp_path: Path, mocker: MagicMock) -> None:
mock_run = mocker.patch("subprocess.run")

# 2. Mock GitRepo for subsequent operations
mock_git_cls = mocker.patch("src.cli.GitRepo")
mock_git_cls = mocker.patch("git_pulsar.cli.GitRepo")
mock_repo = mock_git_cls.return_value

fake_registry = tmp_path / ".registry"
Expand All @@ -35,7 +35,7 @@ def test_setup_repo_initializes_git(tmp_path: Path, mocker: MagicMock) -> None:

def test_main_triggers_bootstrap(mocker: MagicMock) -> None:
"""Ensure --env flag calls ops.bootstrap_env."""
mock_bootstrap = mocker.patch("src.cli.ops.bootstrap_env")
mock_bootstrap = mocker.patch("git_pulsar.cli.ops.bootstrap_env")

mocker.patch("sys.argv", ["git-pulsar", "--env"])
cli.main()
Expand All @@ -45,7 +45,7 @@ def test_main_triggers_bootstrap(mocker: MagicMock) -> None:

def test_main_default_behavior(mocker: MagicMock) -> None:
"""Ensure running without flags defaults to setup_repo."""
mock_setup = mocker.patch("src.cli.setup_repo")
mock_setup = mocker.patch("git_pulsar.cli.setup_repo")
mocker.patch("sys.argv", ["git-pulsar"])

cli.main()
Expand All @@ -55,7 +55,7 @@ def test_main_default_behavior(mocker: MagicMock) -> None:

def test_finalize_command(mocker: MagicMock) -> None:
"""Ensure 'finalize' command calls ops.finalize_work."""
mock_finalize = mocker.patch("src.cli.ops.finalize_work")
mock_finalize = mocker.patch("git_pulsar.cli.ops.finalize_work")
mocker.patch("sys.argv", ["git-pulsar", "finalize"])

cli.main()
Expand All @@ -65,7 +65,7 @@ def test_finalize_command(mocker: MagicMock) -> None:

def test_restore_command(mocker: MagicMock) -> None:
"""Ensure 'restore' command calls ops.restore_file."""
mock_restore = mocker.patch("src.cli.ops.restore_file")
mock_restore = mocker.patch("git_pulsar.cli.ops.restore_file")
mocker.patch("sys.argv", ["git-pulsar", "restore", "file.py"])

cli.main()
Expand Down Expand Up @@ -98,7 +98,7 @@ def test_status_reports_pause_state(
os.chdir(tmp_path)

# Mock GitRepo
mock_cls = mocker.patch("src.cli.GitRepo")
mock_cls = mocker.patch("git_pulsar.cli.GitRepo")
mock_repo = mock_cls.return_value
mock_repo.get_last_commit_time.return_value = "15 minutes ago"
mock_repo.status_porcelain.return_value = []
Expand All @@ -119,7 +119,7 @@ def test_diff_shows_untracked_files(
(tmp_path / ".git").mkdir()
os.chdir(tmp_path)

mock_cls = mocker.patch("src.cli.GitRepo")
mock_cls = mocker.patch("git_pulsar.cli.GitRepo")
mock_repo = mock_cls.return_value
mock_repo.get_untracked_files.return_value = ["new_script.py"]

Expand Down
27 changes: 13 additions & 14 deletions tests/test_daemon.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from pathlib import Path
from unittest.mock import MagicMock

from src import daemon
from src.daemon import Config
from git_pulsar import daemon
from git_pulsar.daemon import Config


def test_run_backup_shadow_commit_flow(tmp_path: Path, mocker: MagicMock) -> None:
Expand All @@ -13,17 +13,17 @@ def test_run_backup_shadow_commit_flow(tmp_path: Path, mocker: MagicMock) -> Non
(tmp_path / ".git").mkdir()

# 1. Mock System & Identity
mocker.patch("src.daemon.SYSTEM.is_under_load", return_value=False)
mocker.patch("src.daemon.SYSTEM.get_battery", return_value=(100, True))
mocker.patch("src.daemon.get_machine_id", return_value="test-unit")
mocker.patch("git_pulsar.daemon.SYSTEM.is_under_load", return_value=False)
mocker.patch("git_pulsar.daemon.SYSTEM.get_battery", return_value=(100, True))
mocker.patch("git_pulsar.daemon.get_machine_id", return_value="test-unit")
mocker.patch("socket.gethostname", return_value="test-unit")

# 2. Mock Configuration (CRITICAL: Ensure tests don't read ~/.config)
# We provide a fresh default configuration
mocker.patch("src.daemon.CONFIG", Config())
mocker.patch("git_pulsar.daemon.CONFIG", Config())

# 3. Mock GitRepo
mock_cls = mocker.patch("src.daemon.GitRepo")
mock_cls = mocker.patch("git_pulsar.daemon.GitRepo")
repo = mock_cls.return_value
repo.path = tmp_path
repo.current_branch.return_value = "main"
Expand All @@ -34,8 +34,8 @@ def test_run_backup_shadow_commit_flow(tmp_path: Path, mocker: MagicMock) -> Non
repo.rev_parse.side_effect = lambda x: "parent_sha" if "HEAD" in x else None

# 4. Mock Network
mocker.patch("src.daemon.get_remote_host", return_value="github.com")
mocker.patch("src.daemon.is_remote_reachable", return_value=True)
mocker.patch("git_pulsar.daemon.get_remote_host", return_value="github.com")
mocker.patch("git_pulsar.daemon.is_remote_reachable", return_value=True)

# ACTION
daemon.run_backup(str(tmp_path))
Expand Down Expand Up @@ -71,13 +71,12 @@ def test_run_backup_skips_if_no_changes(tmp_path: Path, mocker: MagicMock) -> No
"""Optimization check: Don't commit if tree matches parent backup."""
(tmp_path / ".git").mkdir()

mocker.patch("src.daemon.SYSTEM.is_under_load", return_value=False)
mocker.patch("src.daemon.get_machine_id", return_value="test-unit")
mocker.patch("git_pulsar.daemon.SYSTEM.is_under_load", return_value=False)
mocker.patch("git_pulsar.daemon.get_machine_id", return_value="test-unit")

# Mock Config here as well
mocker.patch("src.daemon.CONFIG", Config())

mock_cls = mocker.patch("src.daemon.GitRepo")
mocker.patch("git_pulsar.daemon.CONFIG", Config())
mock_cls = mocker.patch("git_pulsar.daemon.GitRepo")
repo = mock_cls.return_value
repo.current_branch.return_value = "main"

Expand Down
18 changes: 9 additions & 9 deletions tests/test_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import pytest

from src import ops
from git_pulsar import ops


def test_bootstrap_env_enforces_macos(mocker: MagicMock) -> None:
Expand Down Expand Up @@ -52,7 +52,7 @@ def test_configure_identity_creates_file(tmp_path: Path, mocker: MagicMock) -> N
"""Should create machine_id file if missing."""
mocker.patch("builtins.input", return_value="my-laptop")
mock_id_file = tmp_path / "machine_id"
mocker.patch("src.ops.get_machine_id_file", return_value=mock_id_file)
mocker.patch("git_pulsar.ops.get_machine_id_file", return_value=mock_id_file)

ops.configure_identity()

Expand All @@ -63,7 +63,7 @@ def test_configure_identity_skips_existing(tmp_path: Path, mocker: MagicMock) ->
"""Should do nothing if file exists."""
mock_id_file = tmp_path / "machine_id"
mock_id_file.write_text("existing-id")
mocker.patch("src.ops.get_machine_id_file", return_value=mock_id_file)
mocker.patch("git_pulsar.ops.get_machine_id_file", return_value=mock_id_file)
mock_input = mocker.patch("builtins.input")

ops.configure_identity()
Expand All @@ -76,13 +76,13 @@ def test_configure_identity_skips_existing(tmp_path: Path, mocker: MagicMock) ->

def test_restore_clean(mocker: MagicMock) -> None:
"""Should checkout the file if working tree is clean."""
mock_cls = mocker.patch("src.ops.GitRepo")
mock_cls = mocker.patch("git_pulsar.ops.GitRepo")
mock_repo = mock_cls.return_value
mock_repo.status_porcelain.return_value = []

# Mock current branch and machine ID for ref construction
mock_repo.current_branch.return_value = "main"
mocker.patch("src.ops.get_machine_id", return_value="test-unit")
mocker.patch("git_pulsar.ops.get_machine_id", return_value="test-unit")

ops.restore_file("script.py")

Expand All @@ -95,7 +95,7 @@ def test_restore_dirty_fails(tmp_path: Path, mocker: MagicMock) -> None:
os.chdir(tmp_path)
(tmp_path / "script.py").touch()

mock_cls = mocker.patch("src.ops.GitRepo")
mock_cls = mocker.patch("git_pulsar.ops.GitRepo")
mock_repo = mock_cls.return_value
mock_repo.status_porcelain.return_value = ["M script.py"]
mock_repo.current_branch.return_value = "main"
Expand All @@ -106,8 +106,8 @@ def test_restore_dirty_fails(tmp_path: Path, mocker: MagicMock) -> None:

def test_sync_session_success(mocker: MagicMock) -> None:
"""Should find latest backup and checkout."""
mocker.patch("src.ops.GitRepo")
repo = mocker.patch("src.ops.GitRepo").return_value
mocker.patch("git_pulsar.ops.GitRepo")
repo = mocker.patch("git_pulsar.ops.GitRepo").return_value
repo.current_branch.return_value = "main"

# 1. Setup candidates
Expand Down Expand Up @@ -157,7 +157,7 @@ def mock_run(cmd: list[str], *args: Any, **kwargs: Any) -> str:

def test_finalize_octopus_merge(mocker: MagicMock) -> None:
"""Should squash merge multiple backup streams."""
repo = mocker.patch("src.ops.GitRepo").return_value
repo = mocker.patch("git_pulsar.ops.GitRepo").return_value
repo.status_porcelain.return_value = []
repo.current_branch.return_value = "main"

Expand Down
6 changes: 3 additions & 3 deletions tests/test_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from hypothesis import given
from hypothesis import strategies as st

from src import daemon
from git_pulsar import daemon

# Strategy: Generate a list of non-empty strings that don't contain ANY line breaks.
paths_strategy = st.lists(
Expand Down Expand Up @@ -40,8 +40,8 @@ def test_prune_registry_removes_only_target(
# We patch REGISTRY_FILE to point to our temp file
# We patch SYSTEM.notify to suppress desktop notifications
with (
patch("src.daemon.REGISTRY_FILE", registry_file),
patch("src.daemon.SYSTEM.notify"),
patch("git_pulsar.daemon.REGISTRY_FILE", registry_file),
patch("git_pulsar.daemon.SYSTEM.notify"),
):
# 3. Action
daemon.prune_registry(target)
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.