Coverage for src/_griffe/git.py: 73.91%

55 statements  

« prev     ^ index     » next       coverage.py v7.6.2, created at 2024-10-12 01:34 +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. 

4 

5from __future__ import annotations 

6 

7import os 

8import re 

9import shutil 

10import subprocess 

11import unicodedata 

12from contextlib import contextmanager 

13from pathlib import Path 

14from tempfile import TemporaryDirectory 

15from typing import TYPE_CHECKING 

16 

17from _griffe.exceptions import GitError 

18 

19if TYPE_CHECKING: 

20 from collections.abc import Iterator 

21 

22_WORKTREE_PREFIX = "griffe-worktree-" 

23 

24 

25def _normalize(value: str) -> str: 

26 value = unicodedata.normalize("NFKC", value) 

27 value = re.sub(r"[^\w]+", "-", value) 

28 return re.sub(r"[-\s]+", "-", value).strip("-") 

29 

30 

31def assert_git_repo(path: str | Path) -> None: 

32 """Assert that a directory is a Git repository. 

33 

34 Parameters: 

35 path: Path to a directory. 

36 

37 Raises: 

38 OSError: When the directory is not a Git repository. 

39 """ 

40 if not shutil.which("git"): 40 ↛ 41line 40 didn't jump to line 41 because the condition on line 40 was never true

41 raise RuntimeError("Could not find git executable. Please install git.") 

42 

43 try: 

44 subprocess.run( 

45 ["git", "-C", str(path), "rev-parse", "--is-inside-work-tree"], 

46 check=True, 

47 stdout=subprocess.DEVNULL, 

48 stderr=subprocess.DEVNULL, 

49 ) 

50 except subprocess.CalledProcessError as err: 

51 raise OSError(f"Not a git repository: {path}") from err 

52 

53 

54def get_latest_tag(repo: str | Path) -> str: 

55 """Get latest tag of a Git repository. 

56 

57 Parameters: 

58 repo: The path to Git repository. 

59 

60 Returns: 

61 The latest tag. 

62 """ 

63 if isinstance(repo, str): 63 ↛ 64line 63 didn't jump to line 64 because the condition on line 63 was never true

64 repo = Path(repo) 

65 if not repo.is_dir(): 65 ↛ 66line 65 didn't jump to line 66 because the condition on line 65 was never true

66 repo = repo.parent 

67 process = subprocess.run( 

68 ["git", "tag", "-l", "--sort=-committerdate"], 

69 cwd=repo, 

70 text=True, 

71 stdout=subprocess.PIPE, 

72 stderr=subprocess.STDOUT, 

73 check=False, 

74 ) 

75 output = process.stdout.strip() 

76 if process.returncode != 0 or not output: 76 ↛ 78line 76 didn't jump to line 78 because the condition on line 76 was always true

77 raise GitError(f"Cannot list Git tags in {repo}: {output or 'no tags'}") 

78 return output.split("\n", 1)[0] 

79 

80 

81def get_repo_root(repo: str | Path) -> str: 

82 """Get the root of a Git repository. 

83 

84 Parameters: 

85 repo: The path to a Git repository. 

86 

87 Returns: 

88 The root of the repository. 

89 """ 

90 if isinstance(repo, str): 

91 repo = Path(repo) 

92 if not repo.is_dir(): 

93 repo = repo.parent 

94 output = subprocess.check_output( 

95 ["git", "rev-parse", "--show-toplevel"], 

96 cwd=repo, 

97 ) 

98 return output.decode().strip() 

99 

100 

101@contextmanager 

102def tmp_worktree(repo: str | Path = ".", ref: str = "HEAD") -> Iterator[Path]: 

103 """Context manager that checks out the given reference in the given repository to a temporary worktree. 

104 

105 Parameters: 

106 repo: Path to the repository (i.e. the directory *containing* the `.git` directory) 

107 ref: A Git reference such as a commit, tag or branch. 

108 

109 Yields: 

110 The path to the temporary worktree. 

111 

112 Raises: 

113 OSError: If `repo` is not a valid `.git` repository 

114 RuntimeError: If the `git` executable is unavailable, or if it cannot create a worktree 

115 """ 

116 assert_git_repo(repo) 

117 repo_name = Path(repo).resolve().name 

118 normref = _normalize(ref) 

119 with TemporaryDirectory(prefix=f"{_WORKTREE_PREFIX}{repo_name}-{normref}-") as tmp_dir: 

120 location = os.path.join(tmp_dir, normref) # noqa: PTH118 

121 process = subprocess.run( 

122 ["git", "-C", repo, "worktree", "add", "-b", normref, location, ref], 

123 capture_output=True, 

124 check=False, 

125 ) 

126 if process.returncode: 

127 raise RuntimeError(f"Could not create git worktree: {process.stderr.decode()}") 

128 

129 try: 

130 yield Path(location) 

131 finally: 

132 subprocess.run(["git", "-C", repo, "worktree", "remove", normref], stdout=subprocess.DEVNULL, check=False) 

133 subprocess.run(["git", "-C", repo, "worktree", "prune"], stdout=subprocess.DEVNULL, check=False) 

134 subprocess.run(["git", "-C", repo, "branch", "-D", normref], stdout=subprocess.DEVNULL, check=False)