xref: /aosp_15_r20/external/pigweed/pw_presubmit/py/pw_presubmit/git_repo.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1# Copyright 2020 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""Helpful commands for working with a Git repository."""
15
16from pathlib import Path
17import subprocess
18from typing import Collection, Pattern
19
20from pw_cli import git_repo
21from pw_cli.decorators import deprecated
22from pw_presubmit.tools import PresubmitToolRunner
23
24# Moved to pw_cli.git_repo.
25TRACKING_BRANCH_ALIAS = git_repo.TRACKING_BRANCH_ALIAS
26
27
28class LoggingGitRepo(git_repo.GitRepo):
29    """A version of GitRepo that defaults to using a PresubmitToolRunner."""
30
31    def __init__(self, repo_root: Path):
32        super().__init__(repo_root, PresubmitToolRunner())
33
34
35@deprecated('Extend GitRepo to expose needed functionality.')
36def git_stdout(
37    *args: Path | str, show_stderr=False, repo: Path | str = '.'
38) -> str:
39    """Runs a git command in the specified repo."""
40    tool_runner = PresubmitToolRunner()
41    return (
42        tool_runner(
43            'git',
44            ['-C', str(repo), *args],
45            stderr=None if show_stderr else subprocess.DEVNULL,
46            check=True,
47            pw_presubmit_ignore_dry_run=True,
48        )
49        .stdout.decode()
50        .strip()
51    )
52
53
54def find_git_repo(path_in_repo: Path) -> git_repo.GitRepo:
55    """Tries to find the root of the git repo that owns path_in_repo."""
56    return git_repo.find_git_repo(path_in_repo, PresubmitToolRunner())
57
58
59@deprecated('Use GitRepo().tracking_branch().')
60def tracking_branch(
61    repo_path: Path | None = None,
62    fallback: str | None = None,
63) -> str | None:
64    """Returns the tracking branch of the current branch.
65
66    Since most callers of this function can safely handle a return value of
67    None, suppress exceptions and return None if there is no tracking branch.
68
69    Args:
70      repo_path: repo path from which to run commands; defaults to Path.cwd()
71
72    Raises:
73      ValueError: if repo_path is not in a Git repository
74
75    Returns:
76      the remote tracking branch name or None if there is none
77    """
78    repo = repo_path if repo_path is not None else Path.cwd()
79    return find_git_repo(repo).tracking_branch(fallback)
80
81
82@deprecated('Use GitRepo().list_files().')
83def list_files(
84    commit: str | None = None,
85    pathspecs: Collection[Path | str] = (),
86    repo_path: Path | None = None,
87) -> list[Path]:
88    """Lists files with git ls-files or git diff --name-only.
89
90    Args:
91      commit: commit to use as a base for git diff
92      pathspecs: Git pathspecs to use in git ls-files or diff
93      repo_path: repo path from which to run commands; defaults to Path.cwd()
94
95    Returns:
96      A sorted list of absolute paths
97    """
98    repo = repo_path if repo_path is not None else Path.cwd()
99    return find_git_repo(repo).list_files(commit, pathspecs)
100
101
102@deprecated('Use GitRepo.has_uncommitted_changes().')
103def has_uncommitted_changes(repo: Path | None = None) -> bool:
104    """Returns True if the Git repo has uncommitted changes in it.
105
106    This does not check for untracked files.
107    """
108    return LoggingGitRepo(
109        repo if repo is not None else Path.cwd()
110    ).has_uncommitted_changes()
111
112
113def describe_files(
114    git_root: Path,  # pylint: disable=unused-argument
115    working_dir: Path,
116    commit: str | None,
117    pathspecs: Collection[Path | str],
118    exclude: Collection[Pattern],
119    project_root: Path | None = None,
120) -> str:
121    """Completes 'Doing something to ...' for a set of files in a Git repo."""
122    return git_repo.describe_git_pattern(
123        working_dir=working_dir,
124        commit=commit,
125        pathspecs=pathspecs,
126        exclude=exclude,
127        tool_runner=PresubmitToolRunner(),
128        project_root=project_root,
129    )
130
131
132@deprecated('Use find_git_repo().root().')
133def root(repo_path: Path | str = '.') -> Path:
134    """Returns the repository root as an absolute path.
135
136    Raises:
137      FileNotFoundError: the path does not exist
138      subprocess.CalledProcessError: the path is not in a Git repo
139    """
140    return find_git_repo(Path(repo_path)).root()
141
142
143def within_repo(repo_path: Path | str = '.') -> Path | None:
144    """Similar to root(repo_path), returns None if the path is not in a repo."""
145    try:
146        return root(repo_path)
147    except git_repo.GitError:
148        return None
149
150
151def is_repo(repo_path: Path | str = '.') -> bool:
152    """True if the path is tracked by a Git repo."""
153    return within_repo(repo_path) is not None
154
155
156def path(
157    repo_path: Path | str,
158    *additional_repo_paths: Path | str,
159    repo: Path | str = '.',
160) -> Path:
161    """Returns a path relative to a Git repository's root."""
162    return root(repo).joinpath(repo_path, *additional_repo_paths)
163
164
165@deprecated('Use GitRepo.commit_message().')
166def commit_message(commit: str = 'HEAD', repo: Path | str = '.') -> str:
167    """Returns the commit message of the revision."""
168    return LoggingGitRepo(Path(repo)).commit_message(commit)
169
170
171@deprecated('Use GitRepo.commit_author().')
172def commit_author(commit: str = 'HEAD', repo: Path | str = '.') -> str:
173    """Returns the commit author of the revision."""
174    return LoggingGitRepo(Path(repo)).commit_author(commit)
175
176
177@deprecated('Use GitRepo.commit_hash().')
178def commit_hash(
179    rev: str = 'HEAD', short: bool = True, repo: Path | str = '.'
180) -> str:
181    """Returns the commit hash of the revision."""
182    return LoggingGitRepo(Path(repo)).commit_hash(rev, short)
183
184
185@deprecated('Use GitRepo.list_submodules().')
186def discover_submodules(
187    superproject_dir: Path, excluded_paths: Collection[Pattern | str] = ()
188) -> list[Path]:
189    """Query git and return a list of submodules in the current project.
190
191    Args:
192        superproject_dir: Path object to directory under which we are looking
193                          for submodules. This will also be included in list
194                          returned unless excluded.
195        excluded_paths: Pattern or string that match submodules that should not
196                        be returned. All matches are done on posix style paths.
197
198    Returns:
199        List of "Path"s which were found but not excluded, this includes
200        superproject_dir unless excluded.
201    """
202    return LoggingGitRepo(superproject_dir).list_submodules(excluded_paths) + [
203        superproject_dir.resolve()
204    ]
205