Coverage for src/_griffe/git.py: 73.13%
49 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-08-15 16:47 +0200
« prev ^ index » next coverage.py v7.6.1, created at 2024-08-15 16:47 +0200
1# This module contains Git utilities, used by our [`load_git`][griffe.load_git] function,
2# which in turn is used to load the API for different snapshots of a Git repository
3# and find breaking changes between them.
5from __future__ import annotations
7import os
8import shutil
9import subprocess
10from contextlib import contextmanager
11from pathlib import Path
12from tempfile import TemporaryDirectory
13from typing import Iterator
15from _griffe.exceptions import GitError
17_WORKTREE_PREFIX = "griffe-worktree-"
20def assert_git_repo(path: str | Path) -> None:
21 """Assert that a directory is a Git repository.
23 Parameters:
24 path: Path to a directory.
26 Raises:
27 OSError: When the directory is not a Git repository.
28 """
29 if not shutil.which("git"): 29 ↛ 30line 29 didn't jump to line 30 because the condition on line 29 was never true
30 raise RuntimeError("Could not find git executable. Please install git.")
32 try:
33 subprocess.run(
34 ["git", "-C", str(path), "rev-parse", "--is-inside-work-tree"],
35 check=True,
36 stdout=subprocess.DEVNULL,
37 stderr=subprocess.DEVNULL,
38 )
39 except subprocess.CalledProcessError as err:
40 raise OSError(f"Not a git repository: {path}") from err
43def get_latest_tag(repo: str | Path) -> str:
44 """Get latest tag of a Git repository.
46 Parameters:
47 repo: The path to Git repository.
49 Returns:
50 The latest tag.
51 """
52 if isinstance(repo, str): 52 ↛ 53line 52 didn't jump to line 53 because the condition on line 52 was never true
53 repo = Path(repo)
54 if not repo.is_dir(): 54 ↛ 55line 54 didn't jump to line 55 because the condition on line 54 was never true
55 repo = repo.parent
56 process = subprocess.run(
57 ["git", "tag", "-l", "--sort=-committerdate"],
58 cwd=repo,
59 text=True,
60 stdout=subprocess.PIPE,
61 stderr=subprocess.STDOUT,
62 check=False,
63 )
64 output = process.stdout.strip()
65 if process.returncode != 0 or not output: 65 ↛ 67line 65 didn't jump to line 67 because the condition on line 65 was always true
66 raise GitError(f"Cannot list Git tags in {repo}: {output or 'no tags'}")
67 return output.split("\n", 1)[0]
70def get_repo_root(repo: str | Path) -> str:
71 """Get the root of a Git repository.
73 Parameters:
74 repo: The path to a Git repository.
76 Returns:
77 The root of the repository.
78 """
79 if isinstance(repo, str):
80 repo = Path(repo)
81 if not repo.is_dir():
82 repo = repo.parent
83 output = subprocess.check_output(
84 ["git", "rev-parse", "--show-toplevel"],
85 cwd=repo,
86 )
87 return output.decode().strip()
90@contextmanager
91def tmp_worktree(repo: str | Path = ".", ref: str = "HEAD") -> Iterator[Path]:
92 """Context manager that checks out the given reference in the given repository to a temporary worktree.
94 Parameters:
95 repo: Path to the repository (i.e. the directory *containing* the `.git` directory)
96 ref: A Git reference such as a commit, tag or branch.
98 Yields:
99 The path to the temporary worktree.
101 Raises:
102 OSError: If `repo` is not a valid `.git` repository
103 RuntimeError: If the `git` executable is unavailable, or if it cannot create a worktree
104 """
105 assert_git_repo(repo)
106 repo_name = Path(repo).resolve().name
107 with TemporaryDirectory(prefix=f"{_WORKTREE_PREFIX}{repo_name}-{ref}-") as tmp_dir:
108 branch = f"griffe_{ref}"
109 location = os.path.join(tmp_dir, branch)
110 process = subprocess.run(
111 ["git", "-C", repo, "worktree", "add", "-b", branch, location, ref],
112 capture_output=True,
113 check=False,
114 )
115 if process.returncode:
116 raise RuntimeError(f"Could not create git worktree: {process.stderr.decode()}")
118 try:
119 yield Path(location)
120 finally:
121 subprocess.run(["git", "-C", repo, "worktree", "remove", branch], stdout=subprocess.DEVNULL, check=False)
122 subprocess.run(["git", "-C", repo, "worktree", "prune"], stdout=subprocess.DEVNULL, check=False)
123 subprocess.run(["git", "-C", repo, "branch", "-D", branch], stdout=subprocess.DEVNULL, check=False)