Coverage for tsfpga/git_utils.py: 96%

51 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-07 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 

9# Standard libraries 

10import os 

11from pathlib import Path 

12from typing import Any, Iterator, Optional, Union 

13 

14# First party libraries 

15from tsfpga.system_utils import file_is_in_directory 

16 

17 

18def get_git_commit(directory: Path) -> str: 

19 """ 

20 Get a string describing the current git commit. 

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

22 

23 Arguments: 

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

25 

26 Return: 

27 Git commit information. 

28 """ 

29 git_commit = get_git_sha(directory=directory) 

30 if git_local_changes_present(directory=directory): 

31 git_commit += " (local changes present)" 

32 

33 return git_commit 

34 

35 

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

37 """ 

38 Get a short git SHA. 

39 

40 Arguments: 

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

42 

43 Return: 

44 The SHA. 

45 """ 

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

47 # The linux kernel, one of the largest projects, needs 11. 

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

49 sha_length = 16 

50 

51 if "GIT_COMMIT" in os.environ: 

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

53 

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

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

56 # pylint: disable=import-outside-toplevel 

57 # Third party libraries 

58 from git.repo import Repo 

59 

60 repo = Repo(directory, search_parent_directories=True) 

61 git_sha = repo.head.commit.hexsha[0:sha_length] 

62 

63 return git_sha 

64 

65 

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

67 """ 

68 Check if the git repo has local changes. 

69 

70 Arguments: 

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

72 

73 Return: 

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

75 """ 

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

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

78 # pylint: disable=import-outside-toplevel 

79 # Third party libraries 

80 from git.repo import Repo 

81 

82 repo = Repo(directory, search_parent_directories=True) 

83 

84 return repo.is_dirty() 

85 

86 

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

88 """ 

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

90 """ 

91 try: 

92 # pylint: disable=import-outside-toplevel 

93 # Third party libraries 

94 from git import InvalidGitRepositoryError 

95 from git.repo import Repo 

96 except ImportError: 

97 return False 

98 

99 try: 

100 Repo(directory, search_parent_directories=True) 

101 except InvalidGitRepositoryError: 

102 return False 

103 

104 return True 

105 

106 

107def find_git_files( 

108 directory: Path, 

109 exclude_directories: Optional[list[Path]] = None, 

110 file_endings_include: Optional[Union[str, tuple[str]]] = None, 

111 file_endings_avoid: Optional[Union[str, tuple[str]]] = None, 

112) -> Iterator[Path]: 

113 """ 

114 Find files that are checked in to git. 

115 

116 Arguments: 

117 directory: Search in this directory. 

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

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

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

121 

122 Return: 

123 The files that are available in git. 

124 """ 

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

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

127 # pylint: disable=import-outside-toplevel 

128 # Third party libraries 

129 from git.repo import Repo 

130 

131 exclude_directories = ( 

132 [] 

133 if exclude_directories is None 

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

135 ) 

136 

137 def list_paths(root_tree: Any, path: Path) -> Iterator[Path]: 

138 for blob in root_tree.blobs: 

139 yield path / blob.name 

140 for tree in root_tree.trees: 

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

142 

143 repo = Repo(directory, search_parent_directories=True) 

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

145 

146 for file_path in list_paths(repo.tree(), repo_root): 

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

148 continue 

149 

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

151 continue 

152 

153 if file_is_in_directory(file_path, exclude_directories): 

154 continue 

155 

156 if file_is_in_directory(file_path, [directory]): 

157 yield file_path