diff --git a/pre_commit/main.py b/pre_commit/main.py index d46c0d5b7..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,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 = _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 5194e9ea8..209a44740 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -37,6 +37,76 @@ 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_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():