diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 4b359cc434..7e45a93e89 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -1,6 +1,8 @@ - id: tmt-lint name: tmt lint - entry: bash -c "git ls-files --error-unmatch $(python3 -c 'import tmt; print(tmt.Tree(logger=tmt.Logger.create(), path=\".\").root)')/.fmf/version && tmt lint --failed-only --source $@" PAD + # Use a simple wrapper instead of simply `tmt lint` in order to reorder + # some arguments that need to be passed to `tmt` + entry: python -m tmt._pre_commit --pre-check lint --failed-only --source files: '(?:.*\.fmf|.*/\.fmf/version)$' verbose: false pass_filenames: true @@ -9,7 +11,7 @@ - id: tmt-tests-lint name: tmt tests lint - entry: bash -c "git ls-files --error-unmatch $(python3 -c 'import tmt; print(tmt.Tree(logger=tmt.Logger.create(), path=\".\").root)')/.fmf/version && tmt tests lint --failed-only --source $@" PAD + entry: python -m tmt._pre_commit --pre-check tests lint --failed-only --source files: '(?:.*\.fmf|.*/\.fmf/version)$' verbose: false pass_filenames: true @@ -18,7 +20,7 @@ - id: tmt-plans-lint name: tmt plans lint - entry: bash -c "git ls-files --error-unmatch $(python3 -c 'import tmt; print(tmt.Tree(logger=tmt.Logger.create(), path=\".\").root)')/.fmf/version && tmt plans lint --failed-only --source $@" PAD + entry: python -m tmt._pre_commit --pre-check plans lint --failed-only --source files: '(?:.*\.fmf|.*/\.fmf/version)$' verbose: false pass_filenames: true @@ -27,7 +29,7 @@ - id: tmt-stories-lint name: tmt stories lint - entry: bash -c "git ls-files --error-unmatch $(python3 -c 'import tmt; print(tmt.Tree(logger=tmt.Logger.create(), path=\".\").root)')/.fmf/version && tmt stories lint --failed-only --source $@" PAD + entry: python -m tmt._pre_commit --pre-check stories lint --failed-only --source files: '(?:.*\.fmf|.*/\.fmf/version)$' verbose: false pass_filenames: true diff --git a/tmt/_pre_commit/__init__.py b/tmt/_pre_commit/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tmt/_pre_commit/__main__.py b/tmt/_pre_commit/__main__.py new file mode 100644 index 0000000000..d2b1aacd57 --- /dev/null +++ b/tmt/_pre_commit/__main__.py @@ -0,0 +1,67 @@ +import re +import sys + +from tmt.__main__ import run_cli + + +def _run_precommit() -> None: # pyright: ignore[reportUnusedFunction] (used by project.scripts) + """ + A simple wrapper that re-orders cli arguments before :py:func:`run_cli`. + + This utility is needed in order to move arguments like ``--root`` before + the ``lint`` keyword when run by ``pre-commit``. + + Only a limited number of arguments are reordered. + """ + + # Only re-ordering a few known/necessary cli arguments of `main` command + # Could be improved if the click commands can be introspected like with + # `attrs.fields` + # https://github.com/pallets/click/issues/2709 + + argv = sys.argv + # Get the last argument that starts with `-`. Other inputs after that are + # assumed to be filenames (which can be thousands of them) + for last_arg in reversed(range(len(argv))): + if argv[last_arg].startswith("-"): + break + else: + # If there were no args passed, then just `run_cli` + run_cli() + return + + # At this point we need to check and re-order the arguments if needed + for pattern, nargs in [ + (r"(--root$|-r)", 1), + (r"(--root=.*)", 0), + (r"(--verbose$|-v+)", 0), + (r"(--debug$|-d+)", 0), + (r"(--pre-check$)", 0), + ]: + if any(match := re.match(pattern, arg) for arg in argv[1:last_arg + 1]): + assert match + # Actual argument matched + actual_arg = match.group(1) + indx = argv.index(actual_arg) + # Example of reorder args: + # ["tmt", "tests", "lint", "--fix", "--root", "/some/path", "some_file"] + # - argv[0]: name of the program is always first + # ["tmt"] + # - argv[indx:indx+1+nargs]: args to move (pass to `tmt.cli.main`) + # ["--root", "/some/path"] + # - argv[1:indx]: all other keywords (`lint` keyword is in here) + # ["tests", "lint", "--fix"] + # - argv[indx+1+nargs:]: All remaining keywords + # ["some_file"] + # After reorder: + # ["tmt", "--root", "/some/path", "tests", "lint", "--fix", "some_file"] + argv = ( + argv[0:1] + argv[indx: indx + 1 + nargs] + argv[1:indx] + argv[indx + 1 + nargs:] + ) + # Finally move the reordered args to sys.argv and run the cli + sys.argv = argv + run_cli() + + +if __name__ == "__main__": + _run_precommit() diff --git a/tmt/cli.py b/tmt/cli.py index 885cb4eedf..076657be3c 100644 --- a/tmt/cli.py +++ b/tmt/cli.py @@ -307,6 +307,10 @@ def write_dl( '--force-color', is_flag=True, default=False, help='Forces tmt to use colors in the output and logging.' ) +@option( + '--pre-check', is_flag=True, default=False, hidden=True, + help='Run pre-checks on the git root. (Used by pre-commit wrapper).' + ) def main( click_contex: Context, root: str, @@ -314,6 +318,7 @@ def main( no_color: bool, force_color: bool, show_time: bool, + pre_check: bool, **kwargs: Any) -> None: """ Test Management Tool """ @@ -339,6 +344,18 @@ def main( # Save click context and fmf context for future use tmt.utils.Common.store_cli_invocation(click_contex) + # Run pre-checks + if pre_check: + git_command = tmt.utils.Command('git', 'rev-parse', '--show-toplevel') + git_root = git_command.run(cwd=None, logger=logger).stdout + if not git_root: + raise tmt.utils.RunError("git rev-parse did not produce a path", git_command, 0) + git_root = git_root.strip() + git_command = tmt.utils.Command( + 'git', 'ls-files', '--error-unmatch', f"{git_root}/{root}/.fmf/version" + ) + git_command.run(cwd=None, logger=logger) + # Initialize metadata tree (from given path or current directory) tree = tmt.Tree(logger=logger, path=Path(root))