From 712770739912c44154346fb1de71180080f07d13 Mon Sep 17 00:00:00 2001 From: terminalchai Date: Fri, 17 Apr 2026 00:34:39 +0530 Subject: [PATCH] fix: handle cross-drive ValueError in _adjust_args_and_chdir on Windows On Windows, os.path.relpath() raises ValueError when the path and the current working directory are on different drives (e.g. config on C: but the git repo is on D:). Introduce a small _try_relpath() helper that wraps os.path.relpath in a try/except ValueError and returns the original path unchanged when the drives differ. All four os.path.relpath calls in _adjust_args_and_chdir (config, files, commit_msg_filename, repo) are updated to use this helper, so every cross-drive combination is covered. Fixes #2530 --- pre_commit/main.py | 18 ++++++++++++++---- tests/main_test.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 0c3eefdaa..417b9b5e4 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -172,6 +172,16 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: ) +def _try_relpath(path: str) -> str: + """Return os.path.relpath(path), or path unchanged on Windows when path + and the current working directory are on different drives (which would + raise ValueError: path is on mount 'C:', start on mount 'D:').""" + try: + return os.path.relpath(path) + except ValueError: + return path + + def _adjust_args_and_chdir(args: argparse.Namespace) -> None: # `--config` was specified relative to the non-root working directory if os.path.exists(args.config): @@ -188,15 +198,15 @@ def _adjust_args_and_chdir(args: argparse.Namespace) -> None: toplevel = git.get_root() os.chdir(toplevel) - args.config = os.path.relpath(args.config) + args.config = _try_relpath(args.config) if args.command in {'run', 'try-repo'}: - args.files = [os.path.relpath(filename) for filename in args.files] + args.files = [_try_relpath(filename) for filename in args.files] if args.commit_msg_filename is not None: - args.commit_msg_filename = os.path.relpath( + args.commit_msg_filename = _try_relpath( args.commit_msg_filename, ) if args.command == 'try-repo' and os.path.exists(args.repo): - args.repo = os.path.relpath(args.repo) + args.repo = _try_relpath(args.repo) def main(argv: Sequence[str] | None = None) -> int: diff --git a/tests/main_test.py b/tests/main_test.py index fed085fc8..a77f81916 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -89,6 +89,35 @@ def test_adjust_args_try_repo_repo_relative(in_git_dir): assert args.repo == 'foo' +def test_try_relpath_returns_relpath_when_same_drive(): + # Normal case: same drive / same filesystem -- should behave like relpath + result = main._try_relpath(os.path.abspath('.')) + assert result == os.path.relpath(os.path.abspath('.')) + + +def test_try_relpath_returns_original_on_cross_drive_valueerror(): + # On Windows, os.path.relpath raises ValueError when paths are on + # different drives (#2530). _try_relpath must return the path unchanged. + abs_path = 'C:\\Users\\me\\.pre-commit-config.yaml' + with mock.patch('os.path.relpath', side_effect=ValueError('different mount')): + assert main._try_relpath(abs_path) == abs_path + + +@pytest.mark.skipif(os.name != 'nt', reason='windows cross-drive only') +def test_adjust_args_config_on_different_drive(in_git_dir): # pragma: posix no cover + # Simulate a config whose absolute path is on a drive other than the git + # root so that os.path.relpath would raise ValueError. + abs_config = 'C:\\Users\\me\\.pre-commit-config.yaml' + with mock.patch('pre_commit.main.os.path.relpath', side_effect=ValueError('x')): + args = _args(config=abs_config) + # abspath is called before chdir, so mock exists to avoid a real chdir + with mock.patch('os.path.exists', return_value=False): + with mock.patch('pre_commit.git.get_root', return_value=str(in_git_dir)): + main._adjust_args_and_chdir(args) + # config must be preserved (not explode with ValueError) + assert args.config == abs_config + + FNS = ( 'autoupdate', 'clean', 'gc', 'hook_impl', 'install', 'install_hooks', 'migrate_config', 'run', 'sample_config', 'uninstall',