blob: 55361a8999f6505f97dcdec13908a32002563b33 [file] [log] [blame]
Trent Aptedcdc39e32023-06-15 15:40:53 +10001# Copyright 2023 The ChromiumOS Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Shared helpers for cros analyzer commands (fix, lint, format)."""
6
7from abc import ABC
8import logging
Trent Apted72468352023-07-11 16:15:57 +10009import os
Trent Aptedcdc39e32023-06-15 15:40:53 +100010from pathlib import Path
11from typing import List
12
13from chromite.cli import command
14from chromite.lib import commandline
15from chromite.lib import git
16
17
Trent Apted72468352023-07-11 16:15:57 +100018def GetFilesFromCommit(commit: str) -> List[str]:
19 """Returns files changed in the provided git `commit` as absolute paths."""
20 repo_root_path = git.FindGitTopLevel(None)
21 files_in_repo = git.RunGit(
22 repo_root_path,
Trent Aptedcdc39e32023-06-15 15:40:53 +100023 ["diff-tree", "--no-commit-id", "--name-only", "-r", commit],
Trent Apted72468352023-07-11 16:15:57 +100024 ).stdout.splitlines()
25 return [os.path.join(repo_root_path, p) for p in files_in_repo]
26
27
28def HasUncommittedChanges(files: List[str]) -> bool:
29 """Returns whether there are uncommitted changes on any of the `files`.
30
31 `files` can be absolute or relative to the current working directory. If a
32 file is passed that is outside the git repository corresponding to the
33 current working directory, an exception will be thrown.
34 """
35 working_status = git.RunGit(
36 None, ["status", "--porcelain=v1", *files]
37 ).stdout.splitlines()
38 if working_status:
39 logging.warning("%s", "\n".join(working_status))
40 return bool(working_status)
Trent Aptedcdc39e32023-06-15 15:40:53 +100041
42
43class AnalyzerCommand(ABC, command.CliCommand):
44 """Shared argument parsing for cros analyzers (fix, lint, format)."""
45
Trent Apted4d4f1bd2023-06-26 12:24:54 +100046 # Additional aliases to offer for the "--inplace" option.
47 inplace_option_aliases = []
48
Trent Apteded02b392023-06-26 12:47:11 +100049 # Whether to include options that only make sense for analyzers that can
50 # modify the files being checked.
51 can_modify_files = False
52
53 # CliCommand overrides.
Trent Aptedcdc39e32023-06-15 15:40:53 +100054 use_filter_options = True
55
56 @classmethod
57 def AddParser(cls, parser):
58 super().AddParser(parser)
Trent Apteded02b392023-06-26 12:47:11 +100059 if cls.can_modify_files:
60 parser.add_argument(
61 "--check",
62 dest="dryrun",
63 action="store_true",
64 help="Display files with errors & exit non-zero",
65 )
66 parser.add_argument(
67 "--diff",
68 action="store_true",
69 help="Display diff instead of fixed content",
70 )
71 parser.add_argument(
72 "--stdout",
73 dest="inplace",
74 action="store_false",
75 help="Write to stdout",
76 )
77 parser.add_argument(
78 *(["-i", "--inplace"] + cls.inplace_option_aliases),
79 default=True,
80 action="store_true",
81 dest="inplace",
82 help="Fix files inplace (default)",
83 )
84
Trent Aptedcdc39e32023-06-15 15:40:53 +100085 parser.add_argument(
Mike Frysinger483c3172023-08-02 14:37:35 -040086 "-j",
87 "--jobs",
88 type=int,
89 default=None,
90 help="Number of files to process in parallel.",
91 )
92
93 parser.add_argument(
Trent Aptedcdc39e32023-06-15 15:40:53 +100094 "--commit",
95 type=str,
96 help=(
97 "Use files from git commit instead of on disk. If no files are"
98 " provided, the list will be obtained from git diff-tree."
99 ),
100 )
101 parser.add_argument(
102 "--head",
103 dest="commit",
104 action="store_const",
105 const="HEAD",
106 help="Alias for --commit HEAD.",
107 )
108 parser.add_argument(
109 "files",
110 nargs="*",
111 type=Path,
112 help=(
113 "Files to fix. Directories will be expanded, and if in a git"
114 " repository, the .gitignore will be respected."
115 ),
116 )
117
118 @classmethod
119 def ProcessOptions(
120 cls,
121 parser: commandline.ArgumentParser,
122 options: commandline.ArgumentNamespace,
123 ) -> None:
124 """Validate & post-process options before freezing."""
Trent Apted488c8932023-07-27 13:44:16 +1000125 if cls.use_dryrun_options and options.dryrun and options.inplace:
126 # A dry-run should never alter files in-place.
127 logging.warning("Ignoring inplace option for dry-run.")
128 options.inplace = False
Trent Apted4d4f1bd2023-06-26 12:24:54 +1000129
130 # Whether a committed change is being analyzed. Note "pre-submit" is a
131 # special commit passed by `pre-upload.py --pre-submit` asking to check
132 # changes only staged for a commit, but not yet committed.
133 is_committed = options.commit and options.commit != "pre-submit"
134
135 if is_committed and not options.files:
Trent Apted72468352023-07-11 16:15:57 +1000136 options.files = GetFilesFromCommit(options.commit)
137
Trent Apteded02b392023-06-26 12:47:11 +1000138 if cls.can_modify_files and is_committed and options.inplace:
Trent Apted72468352023-07-11 16:15:57 +1000139 # If a commit is provided, bail when using inplace if any of the
140 # files have uncommitted changes. This is because the input to the
141 # analyzer will not consider any working state changes, so they will
142 # likely be lost. In future this may be supported by attempting to
143 # stash and rebase changes. See also b/290714959.
144 if HasUncommittedChanges(options.files):
145 parser.error("In-place may clobber uncommitted changes.")
Trent Aptedcdc39e32023-06-15 15:40:53 +1000146
147 if not options.files:
148 # Running with no arguments is allowed to make the repo upload hook
149 # simple, but print a warning so that if someone runs this manually
150 # they are aware that nothing was changed.
151 logging.warning("No files provided. Doing nothing.")