blob: 652522ea8f6769cc899bb0a020e7026b606aabcf [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(
Mike Frysinger8f9ed8d2023-08-31 10:26:38 -040072 *(["-i", "--inplace"] + cls.inplace_option_aliases),
73 dest="inplace",
74 default=None,
75 action="store_true",
76 help="Fix files inplace (default)",
77 )
78 # NB: This must come after --inplace due to dest= being the same,
79 # and so --inplace's default= is used.
80 parser.add_argument(
Trent Apteded02b392023-06-26 12:47:11 +100081 "--stdout",
82 dest="inplace",
83 action="store_false",
84 help="Write to stdout",
85 )
Trent Apteded02b392023-06-26 12:47:11 +100086
Trent Aptedcdc39e32023-06-15 15:40:53 +100087 parser.add_argument(
Mike Frysinger483c3172023-08-02 14:37:35 -040088 "-j",
89 "--jobs",
90 type=int,
91 default=None,
92 help="Number of files to process in parallel.",
93 )
94
95 parser.add_argument(
Trent Aptedcdc39e32023-06-15 15:40:53 +100096 "--commit",
97 type=str,
98 help=(
99 "Use files from git commit instead of on disk. If no files are"
100 " provided, the list will be obtained from git diff-tree."
101 ),
102 )
103 parser.add_argument(
104 "--head",
Mike Frysinger995444e2023-08-09 14:56:48 -0400105 "--HEAD",
Trent Aptedcdc39e32023-06-15 15:40:53 +1000106 dest="commit",
107 action="store_const",
108 const="HEAD",
109 help="Alias for --commit HEAD.",
110 )
111 parser.add_argument(
112 "files",
113 nargs="*",
114 type=Path,
115 help=(
116 "Files to fix. Directories will be expanded, and if in a git"
117 " repository, the .gitignore will be respected."
118 ),
119 )
120
121 @classmethod
122 def ProcessOptions(
123 cls,
124 parser: commandline.ArgumentParser,
125 options: commandline.ArgumentNamespace,
126 ) -> None:
127 """Validate & post-process options before freezing."""
Mike Frysinger8f9ed8d2023-08-31 10:26:38 -0400128 if cls.can_modify_files:
129 if cls.use_dryrun_options and options.dryrun:
130 if options.inplace:
131 # A dry-run should never alter files in-place.
132 logging.warning("Ignoring inplace option for dry-run.")
133 options.inplace = False
134 if options.inplace is None:
135 options.inplace = True
Trent Apted4d4f1bd2023-06-26 12:24:54 +1000136
137 # Whether a committed change is being analyzed. Note "pre-submit" is a
138 # special commit passed by `pre-upload.py --pre-submit` asking to check
139 # changes only staged for a commit, but not yet committed.
140 is_committed = options.commit and options.commit != "pre-submit"
141
142 if is_committed and not options.files:
Trent Apted72468352023-07-11 16:15:57 +1000143 options.files = GetFilesFromCommit(options.commit)
144
Trent Apteded02b392023-06-26 12:47:11 +1000145 if cls.can_modify_files and is_committed and options.inplace:
Trent Apted72468352023-07-11 16:15:57 +1000146 # If a commit is provided, bail when using inplace if any of the
147 # files have uncommitted changes. This is because the input to the
148 # analyzer will not consider any working state changes, so they will
149 # likely be lost. In future this may be supported by attempting to
150 # stash and rebase changes. See also b/290714959.
151 if HasUncommittedChanges(options.files):
152 parser.error("In-place may clobber uncommitted changes.")
Trent Aptedcdc39e32023-06-15 15:40:53 +1000153
154 if not options.files:
155 # Running with no arguments is allowed to make the repo upload hook
156 # simple, but print a warning so that if someone runs this manually
157 # they are aware that nothing was changed.
158 logging.warning("No files provided. Doing nothing.")