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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed
- Fixed target directory lock issues on Windows inside `gitgo init` on scaffolding failures by returning to the original directory before cleanup.

### Changed
- Expanded and cleaned test suite coverage (now at 93% total coverage) across commands and utility modules with clean, uncommented test cases.

---

## [1.10.1] - 2026-07-17
Expand Down
1 change: 1 addition & 0 deletions src/pygitgo/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ def init_operation(args, standalone=False):
info(f"Next steps:\n cd {target_dir}\n gitgo repo\n gitgo link <url>")

except Exception as e:
os.chdir(orig_cwd)
if os.path.exists(target_dir) and not os.listdir(target_dir):
try:
os.rmdir(target_dir)
Expand Down
1 change: 0 additions & 1 deletion src/pygitgo/utils/banner.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ def _format_sync():
return f"{YELLOW}{ahead} ahead, {behind} behind (diverged){RESET}"

def show_banner():
# Import here to avoid circular dependencies
from pygitgo.main import get_version

ensure_inside_git_repository()
Expand Down
169 changes: 169 additions & 0 deletions tests/test_banner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
from pygitgo.utils.banner import _safe, _format_sync, show_banner
import pytest


def test_safe_success():
def mock_fn():
return "success"
assert _safe(mock_fn, default="default") == "success"

def test_safe_exception():
def mock_fn():
raise ValueError("error")
assert _safe(mock_fn, default="default") == "default"

def test_format_sync_exception(mocker):
mocker.patch("pygitgo.utils.banner.run_command", side_effect=Exception("error"))
assert _format_sync() is None

def test_format_sync_invalid_output(mocker):
mocker.patch("pygitgo.utils.banner.run_command", return_value="only_one_part")
assert _format_sync() is None

mocker.patch("pygitgo.utils.banner.run_command", return_value="three parts here")
assert _format_sync() is None

@pytest.mark.parametrize("ahead,behind,expected", [
(0, 0, "up to date"),
(2, 0, "2 ahead"),
(0, 3, "3 behind"),
(2, 3, "2 ahead, 3 behind (diverged)"),
])
def test_format_sync_valid_cases(mocker, ahead, behind, expected):
mocker.patch("pygitgo.utils.banner.run_command", return_value=f"{ahead}\t{behind}")
res = _format_sync()
assert expected in res

def test_show_banner_clean_status(mocker, capsys):
mocker.patch("pygitgo.main.get_version", return_value="1.10.1")
mocker.patch("pygitgo.utils.banner.ensure_inside_git_repository")
mocker.patch("pygitgo.utils.banner.get_user", return_value=("Huerte", "huerte@example.com"))
mocker.patch("pygitgo.utils.banner.get_current_branch", return_value="main")
mocker.patch("pygitgo.utils.banner.check_for_updates", return_value=None)

def mock_run(args, *a, **k):
cmd_str = " ".join(args) if isinstance(args, list) else str(args)
if "remote.origin.url" in cmd_str:
return "https://github.com/Huerte/GitGo.git"
elif "status" in cmd_str:
return ""
elif "rev-list" in cmd_str:
return "0\t0"
return ""

mocker.patch("pygitgo.utils.banner.run_command", side_effect=mock_run)

mock_commits = [{"hash": "abcdef0", "message": "Initial commit", "date": "2026-07-17", "author": "Huerte"}]
mocker.patch("pygitgo.utils.banner.get_recent_commits", return_value=mock_commits)

import os
mocker.patch("shutil.get_terminal_size", return_value=os.terminal_size((80, 20)))

show_banner()

captured = capsys.readouterr().out
assert "GitGo 1.10.1" in captured
assert "Your Fast Git Companion" in captured
assert "Identity" in captured
assert "Huerte <huerte@example.com>" in captured
assert "Remote" in captured
assert "https://github.com/Huerte/GitGo.git" in captured
assert "Branch" in captured
assert "main" in captured
assert "Sync" in captured
assert "up to date" in captured
assert "Status" in captured
assert "clean" in captured
assert "Latest" in captured
assert "[abcdef0] Initial commit" in captured

def test_show_banner_dirty_status(mocker, capsys):
mocker.patch("pygitgo.main.get_version", return_value="1.10.1")
mocker.patch("pygitgo.utils.banner.ensure_inside_git_repository")
mocker.patch("pygitgo.utils.banner.get_user", return_value=(None, None))
mocker.patch("pygitgo.utils.banner.get_current_branch", side_effect=Exception("no branch"))
mocker.patch("pygitgo.utils.banner.check_for_updates", return_value="Update available: 1.10.2")

def mock_run(args, *a, **k):
cmd_str = " ".join(args) if isinstance(args, list) else str(args)
if "remote.origin.url" in cmd_str:
raise Exception("no remote")
elif "status" in cmd_str:
return "M src/main.py\n?? test.py\n"
elif "rev-list" in cmd_str:
return "2\t3"
return ""

mocker.patch("pygitgo.utils.banner.run_command", side_effect=mock_run)
mocker.patch("pygitgo.utils.banner.get_recent_commits", return_value=[])
import os
mocker.patch("shutil.get_terminal_size", return_value=os.terminal_size((40, 20)))

show_banner()

captured = capsys.readouterr().out
assert "Identity" in captured
assert "Not set <Not set>" in captured
assert "Remote" in captured
assert "not set" in captured
assert "Branch" in captured
assert "unknown" in captured
assert "Sync" in captured
assert "2 ahead, 3 behind (diverged)" in captured
assert "Status" in captured
assert "1 modified, 1 untracked" in captured
assert "Latest" in captured
assert "no commits yet" in captured
assert "Update available: 1.10.2" in captured

def test_show_banner_dirty_only_modified(mocker, capsys):
mocker.patch("pygitgo.main.get_version", return_value="1.10.1")
mocker.patch("pygitgo.utils.banner.ensure_inside_git_repository")
mocker.patch("pygitgo.utils.banner.get_user", return_value=("user", "email"))
mocker.patch("pygitgo.utils.banner.get_current_branch", return_value="main")
mocker.patch("pygitgo.utils.banner.check_for_updates", return_value=None)

def mock_run(args, *a, **k):
cmd_str = " ".join(args) if isinstance(args, list) else str(args)
if "remote.origin.url" in cmd_str:
return "https://github.com/Huerte/GitGo.git"
elif "status" in cmd_str:
return "M src/main.py\n"
elif "rev-list" in cmd_str:
return "0\t0"
return ""

mocker.patch("pygitgo.utils.banner.run_command", side_effect=mock_run)
mocker.patch("pygitgo.utils.banner.get_recent_commits", return_value=[])

show_banner()

captured = capsys.readouterr().out
assert "1 modified" in captured
assert "untracked" not in captured

def test_show_banner_dirty_only_untracked(mocker, capsys):
mocker.patch("pygitgo.main.get_version", return_value="1.10.1")
mocker.patch("pygitgo.utils.banner.ensure_inside_git_repository")
mocker.patch("pygitgo.utils.banner.get_user", return_value=("user", "email"))
mocker.patch("pygitgo.utils.banner.get_current_branch", return_value="main")
mocker.patch("pygitgo.utils.banner.check_for_updates", return_value=None)

def mock_run(args, *a, **k):
cmd_str = " ".join(args) if isinstance(args, list) else str(args)
if "remote.origin.url" in cmd_str:
return "https://github.com/Huerte/GitGo.git"
elif "status" in cmd_str:
return "?? untracked.py\n"
elif "rev-list" in cmd_str:
return "0\t0"
return ""

mocker.patch("pygitgo.utils.banner.run_command", side_effect=mock_run)
mocker.patch("pygitgo.utils.banner.get_recent_commits", return_value=[])

show_banner()

captured = capsys.readouterr().out
assert "1 untracked" in captured
assert "modified" not in captured
140 changes: 140 additions & 0 deletions tests/test_colors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
from pygitgo.utils.colors import _supports_color
import pygitgo.utils.colors
import importlib
import pytest
import sys
import os

@pytest.fixture(autouse=True)
def restore_colors_after_tests():
yield
importlib.reload(pygitgo.utils.colors)

def test_supports_color_no_isatty(mocker):
class DummyStdout:
pass
mocker.patch("sys.stdout", DummyStdout())
assert _supports_color() is False

def test_supports_color_isatty_false(mocker):
mock_stdout = mocker.MagicMock()
mock_stdout.isatty.return_value = False
mocker.patch("sys.stdout", mock_stdout)
assert _supports_color() is False

def test_supports_color_term_dumb(mocker):
mock_stdout = mocker.MagicMock()
mock_stdout.isatty.return_value = True
mocker.patch("sys.stdout", mock_stdout)
mocker.patch.dict(os.environ, {"TERM": "dumb"})
assert _supports_color() is False

def test_supports_color_no_color_env(mocker):
mock_stdout = mocker.MagicMock()
mock_stdout.isatty.return_value = True
mocker.patch("sys.stdout", mock_stdout)
mocker.patch.dict(os.environ, {"NO_COLOR": "1"})
assert _supports_color() is False

def test_supports_color_non_windows_success(mocker):
mock_stdout = mocker.MagicMock()
mock_stdout.isatty.return_value = True
mocker.patch("sys.stdout", mock_stdout)
mocker.patch("sys.platform", "linux")
mocker.patch.dict(os.environ, {}, clear=True)
assert _supports_color() is True

def test_supports_color_win32_ctypes_exception(mocker):
mock_stdout = mocker.MagicMock()
mock_stdout.isatty.return_value = True
mocker.patch("sys.stdout", mock_stdout)
mocker.patch("sys.platform", "win32")
mocker.patch.dict(os.environ, {}, clear=True)
mocker.patch.dict(sys.modules, {"ctypes": None})
assert _supports_color() is False

def test_supports_color_win32_invalid_handle(mocker):
mock_stdout = mocker.MagicMock()
mock_stdout.isatty.return_value = True
mocker.patch("sys.stdout", mock_stdout)
mocker.patch("sys.platform", "win32")
mocker.patch.dict(os.environ, {}, clear=True)

mock_ctypes = mocker.MagicMock()
mock_ctypes.windll.kernel32.GetStdHandle.return_value = -1
mocker.patch.dict(sys.modules, {"ctypes": mock_ctypes})
assert _supports_color() is False

mock_ctypes.windll.kernel32.GetStdHandle.return_value = None
assert _supports_color() is False

def test_supports_color_win32_get_console_mode_fail(mocker):
mock_stdout = mocker.MagicMock()
mock_stdout.isatty.return_value = True
mocker.patch("sys.stdout", mock_stdout)
mocker.patch("sys.platform", "win32")
mocker.patch.dict(os.environ, {}, clear=True)

mock_ctypes = mocker.MagicMock()
mock_ctypes.windll.kernel32.GetStdHandle.return_value = 123
mock_ctypes.windll.kernel32.GetConsoleMode.return_value = False
mocker.patch.dict(sys.modules, {"ctypes": mock_ctypes})
assert _supports_color() is False

def test_supports_color_win32_set_console_mode_fail(mocker):
mock_stdout = mocker.MagicMock()
mock_stdout.isatty.return_value = True
mocker.patch("sys.stdout", mock_stdout)
mocker.patch("sys.platform", "win32")
mocker.patch.dict(os.environ, {}, clear=True)

mock_ctypes = mocker.MagicMock()
mock_ctypes.windll.kernel32.GetStdHandle.return_value = 123
mock_ctypes.windll.kernel32.GetConsoleMode.return_value = True
mock_ctypes.windll.kernel32.SetConsoleMode.return_value = False
mocker.patch.dict(sys.modules, {"ctypes": mock_ctypes})
assert _supports_color() is False

def test_supports_color_win32_success(mocker):
mock_stdout = mocker.MagicMock()
mock_stdout.isatty.return_value = True
mocker.patch("sys.stdout", mock_stdout)
mocker.patch("sys.platform", "win32")
mocker.patch.dict(os.environ, {}, clear=True)

mock_ctypes = mocker.MagicMock()
mock_ctypes.windll.kernel32.GetStdHandle.return_value = 123
mock_ctypes.windll.kernel32.GetConsoleMode.return_value = True
mock_ctypes.windll.kernel32.SetConsoleMode.return_value = True
mocker.patch.dict(sys.modules, {"ctypes": mock_ctypes})
assert _supports_color() is True

def test_color_constants_when_color_enabled(mocker):
mock_stdout = mocker.MagicMock()
mock_stdout.isatty.return_value = True
mocker.patch("sys.stdout", mock_stdout)
mocker.patch("sys.platform", "linux")
mocker.patch.dict(os.environ, {}, clear=True)

importlib.reload(pygitgo.utils.colors)
assert pygitgo.utils.colors.RED == "\033[31m"
assert pygitgo.utils.colors.GREEN == "\033[32m"
assert pygitgo.utils.colors.YELLOW == "\033[33m"
assert pygitgo.utils.colors.BLUE == "\033[34m"
assert pygitgo.utils.colors.CYAN == "\033[36m"
assert pygitgo.utils.colors.RESET == "\033[0m"

def test_color_constants_when_color_disabled(mocker):
mock_stdout = mocker.MagicMock()
mock_stdout.isatty.return_value = False
mocker.patch("sys.stdout", mock_stdout)
mocker.patch("sys.platform", "linux")
mocker.patch.dict(os.environ, {}, clear=True)

importlib.reload(pygitgo.utils.colors)
assert pygitgo.utils.colors.RED == ""
assert pygitgo.utils.colors.GREEN == ""
assert pygitgo.utils.colors.YELLOW == ""
assert pygitgo.utils.colors.BLUE == ""
assert pygitgo.utils.colors.CYAN == ""
assert pygitgo.utils.colors.RESET == ""
Loading
Loading