From 236a8c7c14cae4ab27b7b4ab04acb16a5e06a4cc Mon Sep 17 00:00:00 2001 From: dataflow-solutions-sk Date: Mon, 10 Aug 2026 19:31:27 +0200 Subject: [PATCH 1/2] main: handle cross-drive ValueError from os.path.relpath on config --- pre_commit/main.py | 8 +++++++- tests/main_test.py | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index d46c0d5b7..1eb3ac87e 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -188,7 +188,13 @@ def _adjust_args_and_chdir(args: argparse.Namespace) -> None: toplevel = git.get_root() os.chdir(toplevel) - args.config = os.path.relpath(args.config) + try: + args.config = os.path.relpath(args.config) + except ValueError: + # on windows, relpath raises ValueError when `args.config` and the + # current working directory are on different drives / mounts -- + # fall back to the (already absolute) path in that case + pass if args.command in {'run', 'try-repo'}: args.files = [os.path.relpath(filename) for filename in args.files] if args.commit_msg_filename is not None: diff --git a/tests/main_test.py b/tests/main_test.py index 5194e9ea8..83c47293a 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -37,6 +37,29 @@ def test_adjust_args_and_chdir_noop(in_git_dir): assert args.files == ['f1', 'f2'] +def test_adjust_args_and_chdir_cross_drive_config(in_git_dir): + # on windows, `os.path.relpath` raises `ValueError` when the two + # paths are on different drives/mounts (e.g. config on `C:`, repo on + # `D:`) -- simulate that here so the test doesn't require an actual + # second drive to exist + in_git_dir.join(C.CONFIG_FILE).ensure() + args = _args(command='run', files=[]) + abs_config = os.path.abspath(args.config) + + real_relpath = os.path.relpath + + def relpath(path, *args, **kwargs): + if path == abs_config: + raise ValueError("path is on mount 'C:', start on mount 'D:'") + return real_relpath(path, *args, **kwargs) + + with mock.patch.object(os.path, 'relpath', side_effect=relpath): + main._adjust_args_and_chdir(args) + + assert os.getcwd() == in_git_dir + assert args.config == abs_config + + def test_adjust_args_and_chdir_relative_things(in_git_dir): in_git_dir.join('foo/cfg.yaml').ensure() with in_git_dir.join('foo').as_cwd(): From 86e23799fa447980ff14bb5a0f5613d2c3e654f8 Mon Sep 17 00:00:00 2001 From: dataflow-solutions-sk Date: Mon, 10 Aug 2026 19:33:22 +0200 Subject: [PATCH 2/2] main: guard files/commit_msg_filename/repo relpath against cross-drive ValueError --- pre_commit/main.py | 24 +++++++++++++---------- tests/main_test.py | 47 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 1eb3ac87e..eca38b25f 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -172,6 +172,16 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: ) +def _relpath_or_abspath(path: str) -> str: + # on windows, `os.path.relpath` raises `ValueError` when `path` and the + # current working directory are on different drives / mounts -- fall + # back to the (already absolute) path in that case + try: + return os.path.relpath(path) + except ValueError: + return os.path.abspath(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,21 +198,15 @@ def _adjust_args_and_chdir(args: argparse.Namespace) -> None: toplevel = git.get_root() os.chdir(toplevel) - try: - args.config = os.path.relpath(args.config) - except ValueError: - # on windows, relpath raises ValueError when `args.config` and the - # current working directory are on different drives / mounts -- - # fall back to the (already absolute) path in that case - pass + args.config = _relpath_or_abspath(args.config) if args.command in {'run', 'try-repo'}: - args.files = [os.path.relpath(filename) for filename in args.files] + args.files = [_relpath_or_abspath(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 = _relpath_or_abspath( args.commit_msg_filename, ) if args.command == 'try-repo' and os.path.exists(args.repo): - args.repo = os.path.relpath(args.repo) + args.repo = _relpath_or_abspath(args.repo) def main(argv: Sequence[str] | None = None) -> int: diff --git a/tests/main_test.py b/tests/main_test.py index 83c47293a..209a44740 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -60,6 +60,53 @@ def relpath(path, *args, **kwargs): assert args.config == abs_config +def test_adjust_args_and_chdir_cross_drive_files(in_git_dir): + # `args.files` and `args.commit_msg_filename` go through the same + # `os.path.relpath` call and share the same cross-drive failure mode + # as `args.config` -- make sure they fall back to the absolute path + # too (e.g. `pre-commit run --files `) + f1 = in_git_dir.join('f1').ensure() + args = _args( + command='run', + files=[str(f1)], + commit_msg_filename=str(f1), + ) + abs_f1 = os.path.abspath(str(f1)) + + real_relpath = os.path.relpath + + def relpath(path, *args, **kwargs): + if path == abs_f1: + raise ValueError("path is on mount 'C:', start on mount 'D:'") + return real_relpath(path, *args, **kwargs) + + with mock.patch.object(os.path, 'relpath', side_effect=relpath): + main._adjust_args_and_chdir(args) + + assert args.files == [abs_f1] + assert args.commit_msg_filename == abs_f1 + + +def test_adjust_args_try_repo_cross_drive_repo(in_git_dir): + # `args.repo` shares the same cross-drive failure mode too (e.g. + # `pre-commit try-repo` with a repo on another drive) + with in_git_dir.join('foo').ensure_dir().as_cwd(): + args = _args(command='try-repo', repo='../foo', files=[]) + abs_repo = os.path.abspath(args.repo) + + real_relpath = os.path.relpath + + def relpath(path, *args, **kwargs): + if path == abs_repo: + raise ValueError("path is on mount 'C:', start on mount 'D:'") + return real_relpath(path, *args, **kwargs) + + with mock.patch.object(os.path, 'relpath', side_effect=relpath): + main._adjust_args_and_chdir(args) + + assert args.repo == abs_repo + + def test_adjust_args_and_chdir_relative_things(in_git_dir): in_git_dir.join('foo/cfg.yaml').ensure() with in_git_dir.join('foo').as_cwd():