From b783102338b912f0add2b4d0411d743c13180fda Mon Sep 17 00:00:00 2001 From: Ed_ Date: Sun, 1 Mar 2026 22:34:55 -0500 Subject: [PATCH] feat(git): git operations with allowlist gates and confirm on destructive ops --- src/rook/git.py | 16 ++++++++++++++++ tests/test_git.py | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 src/rook/git.py create mode 100644 tests/test_git.py diff --git a/src/rook/git.py b/src/rook/git.py new file mode 100644 index 0000000..739f62b --- /dev/null +++ b/src/rook/git.py @@ -0,0 +1,16 @@ +import subprocess +from rook.policy import check_allowlist, confirm_spawn + + +class PolicyError(Exception): + pass + + +def run_git(args: list[str], cwd: str = '.') -> str: + if check_allowlist('git', args[0]) is False: + if not confirm_spawn('git ' + args[0], ' '.join(args)): + raise PolicyError(f"git {args[0]} denied by policy") + result = subprocess.run(['git'] + args, cwd=cwd, capture_output=True, text=True) + if result.returncode != 0: + raise RuntimeError(result.stderr) + return result.stdout diff --git a/tests/test_git.py b/tests/test_git.py new file mode 100644 index 0000000..d781468 --- /dev/null +++ b/tests/test_git.py @@ -0,0 +1,40 @@ +import pytest +from unittest.mock import patch, MagicMock +from rook.git import run_git, PolicyError + + +def test_git_status_runs(): + with patch('rook.git.subprocess.run') as mock_run: + mock_run.return_value = MagicMock(stdout='On branch master', returncode=0) + result = run_git(['status']) + assert 'branch' in result + + +def test_git_commit_allowed(): + with patch('rook.git.subprocess.run') as mock_run: + mock_run.return_value = MagicMock(stdout='', returncode=0) + run_git(['commit', '-m', 'msg']) + called_args = mock_run.call_args[0][0] + assert called_args[:2] == ['git', 'commit'] + + +def test_git_push_requires_confirm_yes(): + with patch('rook.git.confirm_spawn', return_value=True) as mock_confirm, \ + patch('rook.git.subprocess.run') as mock_run: + mock_run.return_value = MagicMock(stdout='', returncode=0) + run_git(['push']) + mock_confirm.assert_called() + mock_run.assert_called() + + +def test_git_push_denied_raises_policy_error(): + with patch('rook.git.confirm_spawn', return_value=False): + with pytest.raises(PolicyError): + run_git(['push']) + + +def test_git_nonzero_raises_runtime_error(): + with patch('rook.git.subprocess.run') as mock_run: + mock_run.return_value = MagicMock(stdout='', stderr='fatal: error', returncode=128) + with pytest.raises(RuntimeError): + run_git(['status'])