diff --git a/src/smith/providers/azdo_code.py b/src/smith/providers/azdo_code.py index 4bbc25b..9017256 100644 --- a/src/smith/providers/azdo_code.py +++ b/src/smith/providers/azdo_code.py @@ -262,6 +262,18 @@ def _git_auth_subprocess(self: Any, args: list[str], *, cwd: str | None = None) env=self._git_noninteractive_env() if extra_configs else None, ) + def _git_auth_subprocess_output(self: Any, args: list[str], *, cwd: str | None = None) -> str: + extra_configs = self._git_http_auth_extra_configs() + result = subprocess.run( + self._prepare_git_command(args, extra_configs=extra_configs), + cwd=cwd, + check=True, + capture_output=True, + text=True, + env=self._git_noninteractive_env() if extra_configs else None, + ) + return result.stdout + def _git_subprocess_result( self: Any, args: list[str], @@ -334,11 +346,11 @@ def _local_checkout_refresh_marker(checkout_dir: str) -> str: return os.path.join(checkout_dir, ".git", "smith-last-fetch") def _reset_local_checkout(self: Any, checkout_dir: str) -> None: - self._git_subprocess(["git", "-C", checkout_dir, "reset", "--hard", "HEAD"]) - self._git_subprocess(["git", "-C", checkout_dir, "clean", "-fd"]) + self._git_auth_subprocess(["git", "-C", checkout_dir, "reset", "--hard", "HEAD"]) + self._git_auth_subprocess(["git", "-C", checkout_dir, "clean", "-fd"]) def _checkout_local_ref(self: Any, checkout_dir: str, ref: str) -> None: - self._git_subprocess( + self._git_auth_subprocess( ["git", "-C", checkout_dir, "checkout", "--force", "--detach", ref] ) @@ -360,10 +372,10 @@ def _apply_sparse_patterns( checkout_dir: str, patterns: list[str] | None, ) -> None: - _local_checkout.apply_sparse_patterns(self._git_subprocess, checkout_dir, patterns) + _local_checkout.apply_sparse_patterns(self._git_auth_subprocess, checkout_dir, patterns) def _remote_head_sha(self: Any, checkout_dir: str, branch: str) -> str | None: - return _local_checkout.remote_head_sha(self._git_subprocess_output, checkout_dir, branch) + return _local_checkout.remote_head_sha(self._git_auth_subprocess_output, checkout_dir, branch) def _local_head_sha(self: Any, checkout_dir: str) -> str | None: return _local_checkout.local_head_sha(self._git_subprocess_output, checkout_dir) diff --git a/src/smith/providers/gitlab_code.py b/src/smith/providers/gitlab_code.py index 4e04f94..5bde7f8 100644 --- a/src/smith/providers/gitlab_code.py +++ b/src/smith/providers/gitlab_code.py @@ -709,6 +709,18 @@ def _git_auth_subprocess(self: Any, args: list[str], *, cwd: str | None = None) env=self._git_noninteractive_env() if extra_configs else None, ) + def _git_auth_subprocess_output(self: Any, args: list[str], *, cwd: str | None = None) -> str: + extra_configs = self._git_http_auth_extra_configs() + result = subprocess.run( + self._prepare_git_command(args, extra_configs=extra_configs), + cwd=cwd, + check=True, + capture_output=True, + text=True, + env=self._git_noninteractive_env() if extra_configs else None, + ) + return result.stdout + def _git_subprocess_result( self: Any, args: list[str], @@ -784,11 +796,11 @@ def _local_checkout_refresh_marker(checkout_dir: str) -> str: return os.path.join(checkout_dir, ".git", "smith-last-fetch") def _reset_local_checkout(self: Any, checkout_dir: str) -> None: - self._git_subprocess(["git", "-C", checkout_dir, "reset", "--hard", "HEAD"]) - self._git_subprocess(["git", "-C", checkout_dir, "clean", "-fd"]) + self._git_auth_subprocess(["git", "-C", checkout_dir, "reset", "--hard", "HEAD"]) + self._git_auth_subprocess(["git", "-C", checkout_dir, "clean", "-fd"]) def _checkout_local_ref(self: Any, checkout_dir: str, ref: str) -> None: - self._git_subprocess(["git", "-C", checkout_dir, "checkout", "--force", "--detach", ref]) + self._git_auth_subprocess(["git", "-C", checkout_dir, "checkout", "--force", "--detach", ref]) def _local_checkout_has_expected_origin(self: Any, checkout_dir: str, remote_url: str) -> bool: try: @@ -806,10 +818,10 @@ def _apply_sparse_patterns( checkout_dir: str, patterns: list[str] | None, ) -> None: - _local_checkout.apply_sparse_patterns(self._git_subprocess, checkout_dir, patterns) + _local_checkout.apply_sparse_patterns(self._git_auth_subprocess, checkout_dir, patterns) def _remote_head_sha(self: Any, checkout_dir: str, branch: str) -> str | None: - return _local_checkout.remote_head_sha(self._git_subprocess_output, checkout_dir, branch) + return _local_checkout.remote_head_sha(self._git_auth_subprocess_output, checkout_dir, branch) def _local_head_sha(self: Any, checkout_dir: str) -> str | None: return _local_checkout.local_head_sha(self._git_subprocess_output, checkout_dir) diff --git a/tests/unit/test_azdo_provider_internals.py b/tests/unit/test_azdo_provider_internals.py index 060f672..a6b8cdb 100644 --- a/tests/unit/test_azdo_provider_internals.py +++ b/tests/unit/test_azdo_provider_internals.py @@ -127,6 +127,148 @@ def test_azdo_ls_remote_precheck_skips_fetch_when_head_matches( assert mark_calls == [checkout_dir] +def test_azdo_remote_head_sha_uses_token_auth_when_available(monkeypatch: Any) -> None: + provider = _provider() + checkout_dir = os.path.join("tmp", "checkout") + git_calls: list[dict[str, Any]] = [] + monkeypatch.setattr(provider, "_get_token", lambda force_refresh=False: "ado-token") + + def _fake_run(args: list[str], **kwargs: Any) -> Any: + git_calls.append({"args": args, "env": kwargs.get("env")}) + return SimpleNamespace(stdout="abc123\trefs/heads/main\n", stderr="", returncode=0) + + monkeypatch.setattr("smith.providers.azdo_code.subprocess.run", _fake_run) + + assert provider._remote_head_sha(checkout_dir, "main") == "abc123" + assert git_calls == [ + { + "args": [ + "git", + "-c", + f"core.hooksPath={os.devnull}", + "-c", + "credential.interactive=never", + "-c", + "http.extraHeader=Authorization: Bearer ado-token", + "-C", + checkout_dir, + "ls-remote", + "origin", + "main", + ], + "env": {**os.environ, "GIT_TERMINAL_PROMPT": "0"}, + } + ] + + +def test_azdo_apply_sparse_patterns_uses_token_auth_when_available(monkeypatch: Any, tmp_path: Any) -> None: + provider = _provider() + checkout_dir = tmp_path / "checkout" + (checkout_dir / ".git").mkdir(parents=True) + git_calls: list[dict[str, Any]] = [] + monkeypatch.setattr(provider, "_get_token", lambda force_refresh=False: "ado-token") + + def _fake_run(args: list[str], **kwargs: Any) -> Any: + git_calls.append({"args": args, "env": kwargs.get("env")}) + return SimpleNamespace(stdout="", stderr="", returncode=0) + + monkeypatch.setattr("smith.providers.azdo_code.subprocess.run", _fake_run) + + provider._apply_sparse_patterns(str(checkout_dir), ["/*", "**/*.yml"]) + + assert git_calls == [ + { + "args": [ + "git", + "-c", + f"core.hooksPath={os.devnull}", + "-c", + "credential.interactive=never", + "-c", + "http.extraHeader=Authorization: Bearer ado-token", + "-C", + str(checkout_dir), + "sparse-checkout", + "set", + "--no-cone", + "/*", + "**/*.yml", + ], + "env": {**os.environ, "GIT_TERMINAL_PROMPT": "0"}, + } + ] + + +def test_azdo_checkout_and_reset_use_token_auth_when_available(monkeypatch: Any) -> None: + provider = _provider() + checkout_dir = os.path.join("tmp", "checkout") + git_calls: list[dict[str, Any]] = [] + monkeypatch.setattr(provider, "_get_token", lambda force_refresh=False: "ado-token") + + def _fake_run(args: list[str], **kwargs: Any) -> Any: + git_calls.append({"args": args, "env": kwargs.get("env")}) + return SimpleNamespace(stdout="", stderr="", returncode=0) + + monkeypatch.setattr("smith.providers.azdo_code.subprocess.run", _fake_run) + + provider._checkout_local_ref(checkout_dir, "FETCH_HEAD") + provider._reset_local_checkout(checkout_dir) + + assert git_calls == [ + { + "args": [ + "git", + "-c", + f"core.hooksPath={os.devnull}", + "-c", + "credential.interactive=never", + "-c", + "http.extraHeader=Authorization: Bearer ado-token", + "-C", + checkout_dir, + "checkout", + "--force", + "--detach", + "FETCH_HEAD", + ], + "env": {**os.environ, "GIT_TERMINAL_PROMPT": "0"}, + }, + { + "args": [ + "git", + "-c", + f"core.hooksPath={os.devnull}", + "-c", + "credential.interactive=never", + "-c", + "http.extraHeader=Authorization: Bearer ado-token", + "-C", + checkout_dir, + "reset", + "--hard", + "HEAD", + ], + "env": {**os.environ, "GIT_TERMINAL_PROMPT": "0"}, + }, + { + "args": [ + "git", + "-c", + f"core.hooksPath={os.devnull}", + "-c", + "credential.interactive=never", + "-c", + "http.extraHeader=Authorization: Bearer ado-token", + "-C", + checkout_dir, + "clean", + "-fd", + ], + "env": {**os.environ, "GIT_TERMINAL_PROMPT": "0"}, + }, + ] + + def test_azdo_ripgrep_files_with_matches_uses_subprocess( monkeypatch: Any, tmp_path: Any ) -> None: diff --git a/tests/unit/test_gitlab_provider_internals.py b/tests/unit/test_gitlab_provider_internals.py index ead3670..afceb65 100644 --- a/tests/unit/test_gitlab_provider_internals.py +++ b/tests/unit/test_gitlab_provider_internals.py @@ -291,6 +291,154 @@ def _fake_run(args: list[str], **kwargs: Any) -> Any: ] +def test_gitlab_remote_head_sha_uses_token_auth_when_available(monkeypatch: Any) -> None: + provider = _provider() + checkout_dir = os.path.join("tmp", "checkout") + git_calls: list[dict[str, Any]] = [] + monkeypatch.setattr(provider, "_get_token", lambda force_refresh=False: "env-token") + + def _fake_run(args: list[str], **kwargs: Any) -> Any: + git_calls.append({"args": args, "env": kwargs.get("env")}) + return SimpleNamespace(stdout="abc123\trefs/heads/main\n", stderr="", returncode=0) + + monkeypatch.setattr("smith.providers.gitlab_code.subprocess.run", _fake_run) + + expected_basic = base64.b64encode(b"oauth2:env-token").decode("ascii") + + assert provider._remote_head_sha(checkout_dir, "main") == "abc123" + assert git_calls == [ + { + "args": [ + "git", + "-c", + f"core.hooksPath={os.devnull}", + "-c", + "credential.interactive=never", + "-c", + f"http.extraHeader=Authorization: Basic {expected_basic}", + "-C", + checkout_dir, + "ls-remote", + "origin", + "main", + ], + "env": {**os.environ, "GIT_TERMINAL_PROMPT": "0"}, + } + ] + + +def test_gitlab_apply_sparse_patterns_uses_token_auth_when_available(monkeypatch: Any, tmp_path: Any) -> None: + provider = _provider() + checkout_dir = tmp_path / "checkout" + (checkout_dir / ".git").mkdir(parents=True) + git_calls: list[dict[str, Any]] = [] + monkeypatch.setattr(provider, "_get_token", lambda force_refresh=False: "env-token") + + def _fake_run(args: list[str], **kwargs: Any) -> Any: + git_calls.append({"args": args, "env": kwargs.get("env")}) + return SimpleNamespace(stdout="", stderr="", returncode=0) + + monkeypatch.setattr("smith.providers.gitlab_code.subprocess.run", _fake_run) + + expected_basic = base64.b64encode(b"oauth2:env-token").decode("ascii") + + provider._apply_sparse_patterns(str(checkout_dir), ["/*", "**/*.yml"]) + + assert git_calls == [ + { + "args": [ + "git", + "-c", + f"core.hooksPath={os.devnull}", + "-c", + "credential.interactive=never", + "-c", + f"http.extraHeader=Authorization: Basic {expected_basic}", + "-C", + str(checkout_dir), + "sparse-checkout", + "set", + "--no-cone", + "/*", + "**/*.yml", + ], + "env": {**os.environ, "GIT_TERMINAL_PROMPT": "0"}, + } + ] + + +def test_gitlab_checkout_and_reset_use_token_auth_when_available(monkeypatch: Any) -> None: + provider = _provider() + checkout_dir = os.path.join("tmp", "checkout") + git_calls: list[dict[str, Any]] = [] + monkeypatch.setattr(provider, "_get_token", lambda force_refresh=False: "env-token") + + def _fake_run(args: list[str], **kwargs: Any) -> Any: + git_calls.append({"args": args, "env": kwargs.get("env")}) + return SimpleNamespace(stdout="", stderr="", returncode=0) + + monkeypatch.setattr("smith.providers.gitlab_code.subprocess.run", _fake_run) + + expected_basic = base64.b64encode(b"oauth2:env-token").decode("ascii") + + provider._checkout_local_ref(checkout_dir, "FETCH_HEAD") + provider._reset_local_checkout(checkout_dir) + + assert git_calls == [ + { + "args": [ + "git", + "-c", + f"core.hooksPath={os.devnull}", + "-c", + "credential.interactive=never", + "-c", + f"http.extraHeader=Authorization: Basic {expected_basic}", + "-C", + checkout_dir, + "checkout", + "--force", + "--detach", + "FETCH_HEAD", + ], + "env": {**os.environ, "GIT_TERMINAL_PROMPT": "0"}, + }, + { + "args": [ + "git", + "-c", + f"core.hooksPath={os.devnull}", + "-c", + "credential.interactive=never", + "-c", + f"http.extraHeader=Authorization: Basic {expected_basic}", + "-C", + checkout_dir, + "reset", + "--hard", + "HEAD", + ], + "env": {**os.environ, "GIT_TERMINAL_PROMPT": "0"}, + }, + { + "args": [ + "git", + "-c", + f"core.hooksPath={os.devnull}", + "-c", + "credential.interactive=never", + "-c", + f"http.extraHeader=Authorization: Basic {expected_basic}", + "-C", + checkout_dir, + "clean", + "-fd", + ], + "env": {**os.environ, "GIT_TERMINAL_PROMPT": "0"}, + }, + ] + + def test_gitlab_grep_no_clone_skips_local_checkout(monkeypatch: Any) -> None: provider = _provider() monkeypatch.setenv("GITLAB_GREP_USE_LOCAL_CACHE", "true")