Coverage for tsfpga/git_utils.py: 93%

56 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-27 20:51 +0000

1# -------------------------------------------------------------------------------------------------- 

2# Copyright (c) Lukas Vik. All rights reserved. 

3# 

4# This file is part of the tsfpga project, a project platform for modern FPGA development. 

5# https://tsfpga.com 

6# https://github.com/tsfpga/tsfpga 

7# -------------------------------------------------------------------------------------------------- 

8 

9from __future__ import annotations 

10 

11import os 

12from pathlib import Path 

13from typing import TYPE_CHECKING 

14 

15from tsfpga.system_utils import file_is_in_directory 

16 

17if TYPE_CHECKING: 

18 from collections.abc import Iterator 

19 

20 from git.objects.tree import Tree 

21 

22 

23def get_git_commit(directory: Path, use_rst_annotation: bool = False) -> str: 

24 """ 

25 Get a string describing the current git commit. 

26 E.g. ``"abcdef0123"`` or ``"12345678 (local changes present)"``. 

27 

28 Arguments: 

29 directory: The directory where git commands will be run. 

30 use_rst_annotation: Use reStructuredText literal annotation for the SHA value. 

31 

32 Return: 

33 Git commit information. 

34 """ 

35 annotation = "``" if use_rst_annotation else "" 

36 git_sha = get_git_sha(directory=directory) 

37 result = f"{annotation}{git_sha}{annotation}" 

38 

39 if git_local_changes_present(directory=directory): 

40 result += " (local changes present)" 

41 

42 return result 

43 

44 

45def get_git_sha(directory: Path) -> str: 

46 """ 

47 Get a short git SHA. 

48 

49 Arguments: 

50 directory: The directory where git commands will be run. 

51 

52 Return: 

53 The SHA. 

54 """ 

55 # Generally, eight to ten characters are more than enough to be unique within a project. 

56 # The linux kernel, one of the largest projects, needs 12. 

57 # https://git-scm.com/book/en/v2/Git-Tools-Revision-Selection#Short-SHA-1 

58 sha_length = 12 

59 

60 if "GIT_COMMIT" in os.environ: 

61 return os.environ["GIT_COMMIT"][0:sha_length] 

62 

63 # Import fails if "git" executable is not available, hence it can not be on top level. 

64 # This function should only be called if git is available. 

65 from git.repo import Repo # noqa: PLC0415 

66 

67 repo = Repo(directory, search_parent_directories=True) 

68 return repo.head.commit.hexsha[0:sha_length] 

69 

70 

71def git_local_changes_present(directory: Path) -> bool: 

72 """ 

73 Check if the git repo has local changes. 

74 

75 Arguments: 

76 directory: The directory where git commands will be run. 

77 

78 Return: 

79 ``True`` if the repo contains changes that have been made after the last commit. 

80 """ 

81 # Import fails if "git" executable is not available, hence it can not be on top level. 

82 # This function should only be called if git is available. 

83 from git.repo import Repo # noqa: PLC0415 

84 

85 repo = Repo(directory, search_parent_directories=True) 

86 

87 return repo.is_dirty() 

88 

89 

90def git_commands_are_available(directory: Path) -> bool: 

91 """ 

92 True if "git" command executable is available, and ``directory`` is in a valid git repo. 

93 """ 

94 try: 

95 from git import InvalidGitRepositoryError # noqa: PLC0415 

96 from git.repo import Repo # noqa: PLC0415 

97 except ImportError: 

98 return False 

99 

100 try: 

101 Repo(directory, search_parent_directories=True) 

102 except InvalidGitRepositoryError: 

103 return False 

104 

105 return True 

106 

107 

108def find_git_files( 

109 directory: Path, 

110 exclude_directories: list[Path] | None = None, 

111 file_endings_include: str | tuple[str] | None = None, 

112 file_endings_avoid: str | tuple[str] | None = None, 

113) -> Iterator[Path]: 

114 """ 

115 Find files that are checked in to git. 

116 

117 Arguments: 

118 directory: Search in this directory. 

119 exclude_directories: Files in these directories will not be included. 

120 file_endings_include: Only files with these endings will be included. 

121 file_endings_avoid: Files with these endings will not be included. 

122 

123 Return: 

124 The files that are available in git. 

125 """ 

126 # Import fails if "git" executable is not available, hence it can not be on top level. 

127 # This function should only be called if git is available. 

128 from git.repo import Repo # noqa: PLC0415 

129 

130 exclude_directories = ( 

131 [] 

132 if exclude_directories is None 

133 else [exclude_directory.resolve() for exclude_directory in exclude_directories] 

134 ) 

135 

136 def list_paths(root_tree: Tree, path: Path) -> Iterator[Path]: 

137 for blob in root_tree.blobs: 

138 yield path / blob.name 

139 for tree in root_tree.trees: 

140 yield from list_paths(tree, path / tree.name) 

141 

142 repo = Repo(directory, search_parent_directories=True) 

143 repo_root = Path(repo.working_dir).resolve() 

144 

145 for file_path in list_paths(root_tree=repo.tree(), path=repo_root): 

146 if file_endings_include is not None and not file_path.name.endswith(file_endings_include): 

147 continue 

148 

149 if file_endings_avoid is not None and file_path.name.endswith(file_endings_avoid): 

150 continue 

151 

152 if file_is_in_directory(file_path, exclude_directories): 

153 continue 

154 

155 if file_is_in_directory(file_path, [directory]): 

156 yield file_path