blob: 415a0bb587da260d9b28708938d7a122dd526265 [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
46 use_dryrun_options = True
47 # Override base class property to use path filter options.
48 use_filter_options = True
49
50 @classmethod
51 def AddParser(cls, parser):
52 super().AddParser(parser)
53 parser.add_argument(
54 "--check",
55 dest="dryrun",
56 action="store_true",
57 help="Display files with errors & exit non-zero",
58 )
59 parser.add_argument(
60 "--diff",
61 action="store_true",
62 help="Display diff instead of fixed content",
63 )
64 parser.add_argument(
65 "--stdout",
66 dest="inplace",
67 action="store_false",
68 help="Write to stdout",
69 )
70 parser.add_argument(
Yi Chou648d9652023-07-13 06:20:17 +000071 "-i",
72 "--inplace",
Trent Aptedcdc39e32023-06-15 15:40:53 +100073 default=True,
74 action="store_true",
75 help="Fix files inplace (default)",
76 )
77 parser.add_argument(
78 "--commit",
79 type=str,
80 help=(
81 "Use files from git commit instead of on disk. If no files are"
82 " provided, the list will be obtained from git diff-tree."
83 ),
84 )
85 parser.add_argument(
86 "--head",
87 dest="commit",
88 action="store_const",
89 const="HEAD",
90 help="Alias for --commit HEAD.",
91 )
92 parser.add_argument(
93 "files",
94 nargs="*",
95 type=Path,
96 help=(
97 "Files to fix. Directories will be expanded, and if in a git"
98 " repository, the .gitignore will be respected."
99 ),
100 )
101
102 @classmethod
103 def ProcessOptions(
104 cls,
105 parser: commandline.ArgumentParser,
106 options: commandline.ArgumentNamespace,
107 ) -> None:
108 """Validate & post-process options before freezing."""
109 if options.commit and not options.files:
Trent Apted72468352023-07-11 16:15:57 +1000110 options.files = GetFilesFromCommit(options.commit)
111
112 if options.commit and options.inplace:
113 # If a commit is provided, bail when using inplace if any of the
114 # files have uncommitted changes. This is because the input to the
115 # analyzer will not consider any working state changes, so they will
116 # likely be lost. In future this may be supported by attempting to
117 # stash and rebase changes. See also b/290714959.
118 if HasUncommittedChanges(options.files):
119 parser.error("In-place may clobber uncommitted changes.")
Trent Aptedcdc39e32023-06-15 15:40:53 +1000120
121 if not options.files:
122 # Running with no arguments is allowed to make the repo upload hook
123 # simple, but print a warning so that if someone runs this manually
124 # they are aware that nothing was changed.
125 logging.warning("No files provided. Doing nothing.")