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