diff --git a/anchor/cli.py b/anchor/cli.py index 4006548..009c41f 100644 --- a/anchor/cli.py +++ b/anchor/cli.py @@ -636,7 +636,29 @@ def copy_file(relative_path, label): else: click.secho(" [SKIP] Cryptographic keypair already exists.", fg="yellow") - # ── Summary ─────────────────────────────────────────────── + # —— Git commit option —— + create_commit = False + if os.path.exists(".git") and sys.stdin and hasattr(sys.stdin, "isatty") and sys.stdin.isatty(): + click.echo("") + create_commit = click.confirm( + "Anchor has finished initialization.\n\n" + "Would you like to create an initial governance commit?", + default=True + ) + + if create_commit: + try: + if os.path.exists(".gitignore"): + subprocess.check_call(["git", "add", ".gitignore"]) # anchor: ignore SEC-007 + if os.path.exists(".pre-commit-config.yaml"): + subprocess.check_call(["git", "add", ".pre-commit-config.yaml"]) # anchor: ignore SEC-007 + subprocess.check_call(["git", "add", ".anchor/"]) # anchor: ignore SEC-007 + subprocess.check_call(["git", "commit", "-m", "chore: initialize Anchor governance"]) # anchor: ignore SEC-007 + click.secho(f" [{CHECK}] Created initial governance commit", fg="green") + except Exception as e: + click.secho(f" WARNING: Failed to create initial governance commit: {e}", fg="yellow") + + # —— Summary —— click.echo("") click.secho(" " + "-" * 40, fg="bright_black") click.secho(f" {len(requested_domains)} domain(s) loaded", fg="white") @@ -645,7 +667,10 @@ def copy_file(relative_path, label): if requested_regulators: click.secho(f" {len(requested_regulators)} regulator(s) loaded", fg="white") click.secho(" .anchor/ created", fg="white") - click.secho(" .anchor/ committed to repository (logs and governance state are version-controlled)", fg="white") + if create_commit: + click.secho(" .anchor/ committed to repository (logs and governance state are version-controlled)", fg="white") + else: + click.secho(" .anchor/ created (version-control pending developer action)", fg="bright_black") click.secho(" .anchor/cache/ added to .gitignore", fg="bright_black") click.echo("") click.secho( diff --git a/tests/unit/test_init_git.py b/tests/unit/test_init_git.py new file mode 100644 index 0000000..3bae8b7 --- /dev/null +++ b/tests/unit/test_init_git.py @@ -0,0 +1,100 @@ +import os +import sys +from unittest.mock import patch, MagicMock +from anchor.cli import init + +def test_init_git_confirm_yes(): + # Mocking environment to simulate a Git repository and an interactive terminal + with patch("os.path.exists") as mock_exists, \ + patch("sys.stdin") as mock_stdin, \ + patch("subprocess.check_call") as mock_check_call, \ + patch("os.makedirs") as mock_makedirs, \ + patch("shutil.copy2") as mock_copy, \ + patch("yaml.safe_load") as mock_yaml_load, \ + patch("yaml.dump") as mock_yaml_dump, \ + patch("click.confirm") as mock_confirm: + + # Simulate .git directory exists + def exists_side_effect(path): + if path == ".git": + return True + if ".anchor" in str(path) or "constitution" in str(path) or "policy" in str(path): + return False + return True + + mock_exists.side_effect = exists_side_effect + mock_stdin.isatty.return_value = True + + # We mock click.confirm: + # 1. Enable Git governance checks? (True) + # 2. Install pre-commit? (True) + # 3. Create pre-commit config? (True) + # 4. Would you like to create an initial governance commit? (True) + mock_confirm.side_effect = [True, True, True, True] + + # Call the callback directly to avoid CliRunner overriding sys.stdin + init.callback( + domains='', + frameworks='', + regulators='', + sandbox=False, + all_items=False, + force=False, + no_sign=True, + policy_name='policy.anchor' + ) + + # Verify check_calls were called for git add and git commit + git_add_calls = [ + call for call in mock_check_call.call_args_list + if call[0][0][0] == "git" and call[0][0][1] == "add" + ] + git_commit_calls = [ + call for call in mock_check_call.call_args_list + if call[0][0][0] == "git" and call[0][0][1] == "commit" + ] + + assert len(git_add_calls) > 0 + assert len(git_commit_calls) == 1 + +def test_init_git_confirm_no(): + with patch("os.path.exists") as mock_exists, \ + patch("sys.stdin") as mock_stdin, \ + patch("subprocess.check_call") as mock_check_call, \ + patch("os.makedirs") as mock_makedirs, \ + patch("shutil.copy2") as mock_copy, \ + patch("yaml.safe_load") as mock_yaml_load, \ + patch("yaml.dump") as mock_yaml_dump, \ + patch("click.confirm") as mock_confirm: + + def exists_side_effect(path): + if path == ".git": + return True + if ".anchor" in str(path) or "constitution" in str(path) or "policy" in str(path): + return False + return True + + mock_exists.side_effect = exists_side_effect + mock_stdin.isatty.return_value = True + + # Enable git checks but decline initial commit + mock_confirm.side_effect = [True, True, True, False] + + init.callback( + domains='', + frameworks='', + regulators='', + sandbox=False, + all_items=False, + force=False, + no_sign=True, + policy_name='policy.anchor' + ) + + # Verify git commit was NOT called + git_commit_calls = [ + call for call in mock_check_call.call_args_list + if call[0][0][0] == "git" and call[0][0][1] == "commit" + ] + + assert len(git_commit_calls) == 0