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.
This commit is contained in:
Yuri D'Elia 2021-10-27 16:44:22 +02:00
parent 9b18686168
commit 037bc078dc

View file

@ -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: def _adjust_args_and_chdir(args: argparse.Namespace) -> None:
# `--config` was specified relative to the non-root working directory # `--config` was specified relative to the non-root working directory
if os.path.exists(args.config): 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'}: if args.command in {'run', 'try-repo'}:
args.files = [os.path.abspath(filename) for filename in args.files] args.files = [os.path.abspath(filename) for filename in args.files]
if args.command == 'try-repo' and os.path.exists(args.repo): 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() toplevel = git.get_root()
os.chdir(toplevel) os.chdir(toplevel)
args.config = os.path.relpath(args.config) args.config = os.path.relpath(args.config)
if args.command in {'run', 'try-repo'}: 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): if args.command == 'try-repo' and os.path.exists(args.repo):
args.repo = os.path.relpath(args.repo) args.repo = os.path.relpath(args.repo)