Source code for spack.util.git

# Copyright Spack Project Developers. See COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
"""Single util module where Spack should get a git executable."""

import os
import re
import sys
from typing import List, Optional, overload

from spack.vendor.typing_extensions import Literal

import spack.llnl.util.lang
import spack.util.executable as exe

# regex for a commit version
COMMIT_VERSION = re.compile(r"^[a-f0-9]{40}$")


[docs] def is_git_commit_sha(string: str) -> bool: return len(string) == 40 and bool(COMMIT_VERSION.match(string))
@spack.llnl.util.lang.memoized def _find_git() -> Optional[str]: """Find the git executable in the system path.""" return exe.which_string("git", required=False) @overload def git(required: Literal[True]) -> exe.Executable: ... @overload def git(required: bool = ...) -> Optional[exe.Executable]: ...
[docs] def git(required: bool = False) -> Optional[exe.Executable]: """Get a git executable. Raises CommandNotFoundError if ``required`` and git is not found.""" git_path = _find_git() if not git_path: if required: raise exe.CommandNotFoundError("spack requires 'git'. Make sure it is in your path.") return None git = exe.Executable(git_path) # If we're running under pytest, add this to ignore the fix for CVE-2022-39253 in # git 2.38.1+. Do this in one place; we need git to do this in all parts of Spack. if git and "pytest" in sys.modules: git.add_default_arg("-c", "protocol.file.allow=always") return git
[docs] def init_git_repo( repository: str, remote: str = "origin", git_exe: Optional[exe.Executable] = None ): """Initialize a new Git repository and configure it with a remote.""" git_exe = git_exe or git(required=True) git_exe("init", "--quiet", output=str) git_exe("remote", "add", remote, repository) # versions of git prior to v2.24 may not have the manyFiles feature # so we should ignore errors here on older versions of git git_exe("config", "feature.manyFiles", "true", ignore_errors=True)
[docs] def pull_checkout_commit( commit: str, remote: Optional[str] = None, depth: Optional[int] = None, git_exe: Optional[exe.Executable] = None, ): """Checkout the specified commit (fetched if necessary).""" git_exe = git_exe or git(required=True) # Do not do any fetching if the commit is already present. try: git_exe("checkout", "--quiet", commit, error=os.devnull) return except exe.ProcessError: pass # First try to fetch the specific commit from a specific remote. This allows fixed depth, but # the server needs to support it. if remote is not None: try: flags = [] if depth is None else [f"--depth={depth}"] git_exe("fetch", "--quiet", "--progress", *flags, remote, commit, error=os.devnull) git_exe("checkout", "--quiet", commit) return except exe.ProcessError: pass # Fall back to fetching all while unshallowing, to guarantee we get the commit. The depth flag # is equivalent to --unshallow, and needed cause git can pedantically error with # "--unshallow on a complete repository does not make sense". remote_flag = "--all" if remote is None else remote git_exe("fetch", "--quiet", "--progress", "--depth=2147483647", remote_flag) git_exe("checkout", "--quiet", commit)
[docs] def pull_checkout_tag( tag: str, remote: str = "origin", depth: Optional[int] = None, git_exe: Optional[exe.Executable] = None, ): """Fetch tags with specified depth and checkout the given tag.""" git_exe = git_exe or git(required=True) fetch_args = ["--quiet", "--progress", "--tags"] if depth is not None: if depth <= 0: raise ValueError("depth must be a positive integer") fetch_args.append(f"--depth={depth}") git_exe("fetch", *fetch_args, remote) git_exe("checkout", tag)
[docs] def pull_checkout_branch( branch: str, remote: str = "origin", depth: Optional[int] = None, git_exe: Optional[exe.Executable] = None, ): """Fetch and checkout branch, then rebase with remote tracking branch.""" git_exe = git_exe or git(required=True) fetch_args = ["--quiet", "--progress"] if depth: if depth <= 0: raise ValueError("depth must be a positive integer") fetch_args.append(f"--depth={depth}") git_exe("fetch", *fetch_args, remote, f"{branch}:refs/remotes/{remote}/{branch}") git_exe("checkout", "--quiet", branch) try: git_exe("rebase", "--quiet", f"{remote}/{branch}") except exe.ProcessError: git_exe("rebase", "--abort", fail_on_error=False, error=str, output=str) raise
[docs] def get_modified_files( from_ref: str = "HEAD~1", to_ref: str = "HEAD", git_exe: Optional[exe.Executable] = None ) -> List[str]: """Get a list of files modified between ``from_ref`` and ``to_ref`` Args: from_ref (str): oldest git ref, defaults to ``HEAD~1`` to_ref (str): newer git ref, defaults to ``HEAD`` Returns: list of file paths """ git_exe = git_exe or git(required=True) stdout = git_exe("diff", "--name-only", from_ref, to_ref, output=str) return stdout.split()
[docs] def get_commit_sha(path: str, ref: str) -> Optional[str]: """Get a commit sha for an arbitrary ref using ls-remote""" # search for matching branch, annotated tag's commit, then lightweight tag ref_list = [f"refs/heads/{ref}", f"refs/tags/{ref}^{{}}", f"refs/tags/{ref}"] if os.path.isdir(path): # for the filesystem an unpacked mirror could be in a detached state from a depth 1 clone # only reference there will be HEAD ref_list.append("HEAD") for try_ref in ref_list: # this command enabled in git@1.7 so no version checking supplied (1.7 released in 2009) try: query = git(required=True)( "ls-remote", path, try_ref, output=str, error=os.devnull, extra_env={"GIT_TERMINAL_PROMPT": "0"}, ) if query: return query.strip().split()[0] except spack.util.executable.ProcessError: continue return None