From ec8de2876a1893b11da316e3b85130163427ccb4 Mon Sep 17 00:00:00 2001 From: paipeline Date: Sun, 22 Feb 2026 07:36:25 +0100 Subject: [PATCH 1/2] Fix: Properly propagate git authentication errors in fetch/import operations Fixes #10992 When git authentication fails during DVC fetch or import operations, authentication errors were being silently converted to generic SCM errors or not propagated at all, causing DVC to report 'Everything is up to date' instead of the actual authentication failure. Changes: - Enhanced _pull() function to catch AuthError and convert to GitAuthError - Updated clone() function to preserve authentication error details - Added proper error chaining with 'from exc' for better debugging This ensures that users get clear feedback when their PAT tokens or credentials are incorrect, instead of misleading success messages. --- dvc/repo/open_repo.py | 15 +++++++-- dvc/scm.py | 4 ++- test_auth_error_fix.py | 75 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 test_auth_error_fix.py diff --git a/dvc/repo/open_repo.py b/dvc/repo/open_repo.py index 25ddf877a7..f4c0e34dec 100644 --- a/dvc/repo/open_repo.py +++ b/dvc/repo/open_repo.py @@ -212,11 +212,22 @@ def _clone_default_branch(url, rev): def _pull(git: "Git", unshallow: bool = False): + from scmrepo.exceptions import AuthError + from dvc.repo.experiments.utils import fetch_all_exps + from dvc.scm import GitAuthError - git.fetch(unshallow=unshallow) + try: + git.fetch(unshallow=unshallow) + except AuthError as exc: + raise GitAuthError(str(exc)) from exc + _merge_upstream(git) - fetch_all_exps(git, "origin") + + try: + fetch_all_exps(git, "origin") + except AuthError as exc: + raise GitAuthError(str(exc)) from exc def _merge_upstream(git: "Git"): diff --git a/dvc/scm.py b/dvc/scm.py index c451a501eb..13e180f672 100644 --- a/dvc/scm.py +++ b/dvc/scm.py @@ -144,7 +144,7 @@ def update_git(self, event: "GitProgressEvent") -> None: def clone(url: str, to_path: str, **kwargs): - from scmrepo.exceptions import CloneError as InternalCloneError + from scmrepo.exceptions import AuthError, CloneError as InternalCloneError from dvc.repo.experiments.utils import fetch_all_exps @@ -154,6 +154,8 @@ def clone(url: str, to_path: str, **kwargs): if "shallow_branch" not in kwargs: fetch_all_exps(git, url, progress=pbar.update_git) return git + except AuthError as exc: + raise GitAuthError(str(exc)) from exc except InternalCloneError as exc: raise CloneError("SCM error") from exc diff --git a/test_auth_error_fix.py b/test_auth_error_fix.py new file mode 100644 index 0000000000..0e77bd1d5a --- /dev/null +++ b/test_auth_error_fix.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +""" +Test that git authentication errors are properly reported in DVC fetch/import operations. +""" + +import tempfile +import unittest.mock +from unittest.mock import MagicMock, patch + +import pytest +from scmrepo.exceptions import AuthError + +from dvc.repo.open_repo import _pull, clone +from dvc.scm import GitAuthError + + +def test_pull_auth_error_propagation(): + """Test that _pull properly converts AuthError to GitAuthError.""" + mock_git = MagicMock() + mock_git.fetch.side_effect = AuthError("Authentication failed") + + with pytest.raises(GitAuthError) as exc_info: + _pull(mock_git) + + assert "Authentication failed" in str(exc_info.value) + assert "See https://dvc.org/doc/user-guide/troubleshooting#git-auth" in str(exc_info.value) + + +def test_pull_fetch_all_exps_auth_error(): + """Test that _pull handles AuthError from fetch_all_exps.""" + mock_git = MagicMock() + mock_git.fetch.return_value = None # fetch succeeds + + with patch("dvc.repo.open_repo.fetch_all_exps") as mock_fetch_all_exps: + mock_fetch_all_exps.side_effect = AuthError("Authentication failed for experiments") + + with pytest.raises(GitAuthError) as exc_info: + _pull(mock_git) + + assert "Authentication failed for experiments" in str(exc_info.value) + + +def test_clone_auth_error_propagation(): + """Test that clone properly converts AuthError to GitAuthError.""" + with patch("dvc.scm.Git.clone") as mock_git_clone: + mock_git_clone.side_effect = AuthError("Bad PAT token") + + with pytest.raises(GitAuthError) as exc_info: + clone("https://github.com/test/repo.git", "/tmp/test") + + assert "Bad PAT token" in str(exc_info.value) + assert "See https://dvc.org/doc/user-guide/troubleshooting#git-auth" in str(exc_info.value) + + +def test_clone_fetch_all_exps_auth_error(): + """Test that clone handles AuthError from fetch_all_exps.""" + mock_git = MagicMock() + + with patch("dvc.scm.Git.clone", return_value=mock_git): + with patch("dvc.repo.experiments.utils.fetch_all_exps") as mock_fetch_all_exps: + mock_fetch_all_exps.side_effect = AuthError("Experiments fetch auth failed") + + with pytest.raises(GitAuthError) as exc_info: + clone("https://github.com/test/repo.git", "/tmp/test") + + assert "Experiments fetch auth failed" in str(exc_info.value) + + +if __name__ == "__main__": + # Run basic tests + test_pull_auth_error_propagation() + test_pull_fetch_all_exps_auth_error() + test_clone_auth_error_propagation() + test_clone_fetch_all_exps_auth_error() + print("✅ All tests passed!") \ No newline at end of file From 29c386477a0869d3cf961fba6074ecc5fae1a285 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 06:36:54 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- dvc/repo/open_repo.py | 4 ++-- dvc/scm.py | 3 ++- test_auth_error_fix.py | 38 +++++++++++++++++++++----------------- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/dvc/repo/open_repo.py b/dvc/repo/open_repo.py index f4c0e34dec..8ea58b6fb5 100644 --- a/dvc/repo/open_repo.py +++ b/dvc/repo/open_repo.py @@ -221,9 +221,9 @@ def _pull(git: "Git", unshallow: bool = False): git.fetch(unshallow=unshallow) except AuthError as exc: raise GitAuthError(str(exc)) from exc - + _merge_upstream(git) - + try: fetch_all_exps(git, "origin") except AuthError as exc: diff --git a/dvc/scm.py b/dvc/scm.py index 13e180f672..f3e5a86e90 100644 --- a/dvc/scm.py +++ b/dvc/scm.py @@ -144,7 +144,8 @@ def update_git(self, event: "GitProgressEvent") -> None: def clone(url: str, to_path: str, **kwargs): - from scmrepo.exceptions import AuthError, CloneError as InternalCloneError + from scmrepo.exceptions import AuthError + from scmrepo.exceptions import CloneError as InternalCloneError from dvc.repo.experiments.utils import fetch_all_exps diff --git a/test_auth_error_fix.py b/test_auth_error_fix.py index 0e77bd1d5a..ce90f85125 100644 --- a/test_auth_error_fix.py +++ b/test_auth_error_fix.py @@ -3,8 +3,6 @@ Test that git authentication errors are properly reported in DVC fetch/import operations. """ -import tempfile -import unittest.mock from unittest.mock import MagicMock, patch import pytest @@ -18,25 +16,29 @@ def test_pull_auth_error_propagation(): """Test that _pull properly converts AuthError to GitAuthError.""" mock_git = MagicMock() mock_git.fetch.side_effect = AuthError("Authentication failed") - + with pytest.raises(GitAuthError) as exc_info: _pull(mock_git) - + assert "Authentication failed" in str(exc_info.value) - assert "See https://dvc.org/doc/user-guide/troubleshooting#git-auth" in str(exc_info.value) + assert "See https://dvc.org/doc/user-guide/troubleshooting#git-auth" in str( + exc_info.value + ) def test_pull_fetch_all_exps_auth_error(): """Test that _pull handles AuthError from fetch_all_exps.""" mock_git = MagicMock() mock_git.fetch.return_value = None # fetch succeeds - + with patch("dvc.repo.open_repo.fetch_all_exps") as mock_fetch_all_exps: - mock_fetch_all_exps.side_effect = AuthError("Authentication failed for experiments") - + mock_fetch_all_exps.side_effect = AuthError( + "Authentication failed for experiments" + ) + with pytest.raises(GitAuthError) as exc_info: _pull(mock_git) - + assert "Authentication failed for experiments" in str(exc_info.value) @@ -44,32 +46,34 @@ def test_clone_auth_error_propagation(): """Test that clone properly converts AuthError to GitAuthError.""" with patch("dvc.scm.Git.clone") as mock_git_clone: mock_git_clone.side_effect = AuthError("Bad PAT token") - + with pytest.raises(GitAuthError) as exc_info: clone("https://github.com/test/repo.git", "/tmp/test") - + assert "Bad PAT token" in str(exc_info.value) - assert "See https://dvc.org/doc/user-guide/troubleshooting#git-auth" in str(exc_info.value) + assert "See https://dvc.org/doc/user-guide/troubleshooting#git-auth" in str( + exc_info.value + ) def test_clone_fetch_all_exps_auth_error(): """Test that clone handles AuthError from fetch_all_exps.""" mock_git = MagicMock() - + with patch("dvc.scm.Git.clone", return_value=mock_git): with patch("dvc.repo.experiments.utils.fetch_all_exps") as mock_fetch_all_exps: mock_fetch_all_exps.side_effect = AuthError("Experiments fetch auth failed") - + with pytest.raises(GitAuthError) as exc_info: clone("https://github.com/test/repo.git", "/tmp/test") - + assert "Experiments fetch auth failed" in str(exc_info.value) if __name__ == "__main__": # Run basic tests test_pull_auth_error_propagation() - test_pull_fetch_all_exps_auth_error() + test_pull_fetch_all_exps_auth_error() test_clone_auth_error_propagation() test_clone_fetch_all_exps_auth_error() - print("✅ All tests passed!") \ No newline at end of file + print("✅ All tests passed!")