| # Copyright 2023 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Shared helpers for cros analyzer commands (fix, lint, format).""" |
| |
| import logging |
| import os |
| from pathlib import Path |
| from typing import List |
| |
| from chromite.cli import command |
| from chromite.lib import commandline |
| from chromite.lib import git |
| |
| |
| def GetFilesFromCommit(commit: str) -> List[str]: |
| """Returns files changed in the provided git `commit` as absolute paths.""" |
| repo_root_path = git.FindGitTopLevel(None) |
| files_in_repo = git.RunGit( |
| repo_root_path, |
| ["diff-tree", "--no-commit-id", "--name-only", "-r", commit], |
| ).stdout.splitlines() |
| return [os.path.join(repo_root_path, p) for p in files_in_repo] |
| |
| |
| def HasUncommittedChanges(files: List[str]) -> bool: |
| """Returns whether there are uncommitted changes on any of the `files`. |
| |
| `files` can be absolute or relative to the current working directory. If a |
| file is passed that is outside the git repository corresponding to the |
| current working directory, an exception will be thrown. |
| """ |
| working_status = git.RunGit( |
| None, ["status", "--porcelain=v1", *files] |
| ).stdout.splitlines() |
| if working_status: |
| logging.warning("%s", "\n".join(working_status)) |
| return bool(working_status) |
| |
| |
| class AnalyzerCommand(command.CliCommand): |
| """Shared argument parsing for cros analyzers (fix, lint, format).""" |
| |
| # Additional aliases to offer for the "--inplace" option. |
| inplace_option_aliases = [] |
| |
| # Whether to include options that only make sense for analyzers that can |
| # modify the files being checked. |
| can_modify_files = False |
| |
| # CliCommand overrides. |
| use_filter_options = True |
| |
| @classmethod |
| def AddParser(cls, parser) -> None: |
| super().AddParser(parser) |
| if cls.can_modify_files: |
| parser.add_argument( |
| "--check", |
| dest="dryrun", |
| action="store_true", |
| help="Display files with errors & exit non-zero", |
| ) |
| parser.add_argument( |
| "--diff", |
| action="store_true", |
| help="Display diff instead of fixed content", |
| ) |
| parser.add_argument( |
| *(["-i", "--inplace"] + cls.inplace_option_aliases), |
| dest="inplace", |
| default=None, |
| action="store_true", |
| help="Fix files inplace (default)", |
| ) |
| # NB: This must come after --inplace due to dest= being the same, |
| # and so --inplace's default= is used. |
| parser.add_argument( |
| "--stdout", |
| dest="inplace", |
| action="store_false", |
| help="Write to stdout", |
| ) |
| |
| parser.add_argument( |
| "-j", |
| "--jobs", |
| type=int, |
| default=None, |
| help="Number of files to process in parallel.", |
| ) |
| |
| parser.add_argument( |
| "--commit", |
| type=str, |
| help=( |
| "Use files from git commit instead of on disk. If no files are" |
| " provided, the list will be obtained from git diff-tree." |
| ), |
| ) |
| parser.add_argument( |
| "--head", |
| "--HEAD", |
| dest="commit", |
| action="store_const", |
| const="HEAD", |
| help="Alias for --commit HEAD.", |
| ) |
| parser.add_argument( |
| "files", |
| nargs="*", |
| type=Path, |
| help=( |
| "Files to fix. Directories will be expanded, and if in a git" |
| " repository, the .gitignore will be respected." |
| ), |
| ) |
| |
| @classmethod |
| def ProcessOptions( |
| cls, |
| parser: commandline.ArgumentParser, |
| options: commandline.ArgumentNamespace, |
| ) -> None: |
| """Validate & post-process options before freezing.""" |
| if cls.can_modify_files: |
| if cls.use_dryrun_options and options.dryrun: |
| if options.inplace: |
| # A dry-run should never alter files in-place. |
| logging.warning("Ignoring inplace option for dry-run.") |
| options.inplace = False |
| if options.inplace is None: |
| options.inplace = True |
| |
| # Whether a committed change is being analyzed. Note "pre-submit" is a |
| # special commit passed by `pre-upload.py --pre-submit` asking to check |
| # changes only staged for a commit, but not yet committed. |
| is_committed = options.commit and options.commit != "pre-submit" |
| |
| if is_committed and not options.files: |
| options.files = GetFilesFromCommit(options.commit) |
| |
| if cls.can_modify_files and is_committed and options.inplace: |
| # If a commit is provided, bail when using inplace if any of the |
| # files have uncommitted changes. This is because the input to the |
| # analyzer will not consider any working state changes, so they will |
| # likely be lost. In future this may be supported by attempting to |
| # stash and rebase changes. See also b/290714959. |
| if HasUncommittedChanges(options.files): |
| parser.error("In-place may clobber uncommitted changes.") |
| |
| if not options.files: |
| # Running with no arguments is allowed to make the repo upload hook |
| # simple, but print a warning so that if someone runs this manually |
| # they are aware that nothing was changed. |
| logging.warning("No files provided. Doing nothing.") |