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

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 shutil 

9import subprocess 

10from contextlib import contextmanager 

11from pathlib import Path 

12from tempfile import TemporaryDirectory 

13from typing import Iterator 

14 

15from _griffe.exceptions import GitError 

16 

17_WORKTREE_PREFIX = "griffe-worktree-" 

18 

19 

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

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

22 

23 Parameters: 

24 path: Path to a directory. 

25 

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.") 

31 

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 

41 

42 

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

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

45 

46 Parameters: 

47 repo: The path to Git repository. 

48 

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] 

68 

69 

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

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

72 

73 Parameters: 

74 repo: The path to a Git repository. 

75 

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() 

88 

89 

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. 

93 

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. 

97 

98 Yields: 

99 The path to the temporary worktree. 

100 

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()}") 

117 

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)