From 037bc078dcc26ea5f8069b0a16a1fd49d1e03b5b Mon Sep 17 00:00:00 2001 From: Yuri D'Elia Date: Wed, 27 Oct 2021 16:44:22 +0200 Subject: [PATCH] Improve path canonicalization The git toplevel is a path with all symlinks leading to the repository root resolved. When transforming the supplied files to paths relative to the repository's root we _also_ need to resolve all symlinks up to the repository's root in order to correctly handle different symlinked paths that eventually lead to the same repository. Such paths are currently accepted by git, but are not matched correctly in pre-commit, as they incorrectly show as out-of-tree. To relativize paths correctly we iteratively try to replace the trailing components of the absolute path until the repository root is matched. The root prefix is then substituted for the real path, yelding a path with the subtree path untouched. This allows to correctly supply a symlink within the working tree to git itself. For paths which are not directly given as an argument to git, using os.path.realpath() instead is sufficient. This allows pre-commit to see the subtree path in the same way as currently accepted by git. --- pre_commit/main.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index f1e8d03d..c18c1a6a 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -156,21 +156,36 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: ) +def _canon_subpath(path: str, toplevel: str) -> str: + tail = '' + while len(path): + if os.path.samefile(path, toplevel): + return os.path.join(toplevel, tail) + path, base = os.path.split(path) + tail = os.path.join(base, tail) + return tail + + +def _canon_relpath(path: str, toplevel: str) -> str: + return os.path.relpath(_canon_subpath(path, toplevel)) + + 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): - args.config = os.path.abspath(args.config) + args.config = os.path.realpath(args.config) if args.command in {'run', 'try-repo'}: args.files = [os.path.abspath(filename) for filename in args.files] if args.command == 'try-repo' and os.path.exists(args.repo): - args.repo = os.path.abspath(args.repo) + args.repo = os.path.realpath(args.repo) toplevel = git.get_root() os.chdir(toplevel) args.config = os.path.relpath(args.config) if args.command in {'run', 'try-repo'}: - args.files = [os.path.relpath(filename) for filename in args.files] + args.files = [_canon_relpath(filename, toplevel) + for filename in args.files] if args.command == 'try-repo' and os.path.exists(args.repo): args.repo = os.path.relpath(args.repo)