Coverage for tsfpga/git_utils.py: 96%
51 statements
« prev ^ index » next coverage.py v7.6.10, created at 2024-12-29 20:52 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2024-12-29 20:52 +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# --------------------------------------------------------------------------------------------------
9# Standard libraries
10import os
11from pathlib import Path
12from typing import Any, Iterator, Optional, Union
14# First party libraries
15from tsfpga.system_utils import file_is_in_directory
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)"``.
23 Arguments:
24 directory: The directory where git commands will be run.
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)"
33 return git_commit
36def get_git_sha(directory: Path) -> str:
37 """
38 Get a short git SHA.
40 Arguments:
41 directory: The directory where git commands will be run.
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 12.
48 # https://git-scm.com/book/en/v2/Git-Tools-Revision-Selection#Short-SHA-1
49 sha_length = 12
51 if "GIT_COMMIT" in os.environ:
52 return os.environ["GIT_COMMIT"][0:sha_length]
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
60 repo = Repo(directory, search_parent_directories=True)
61 git_sha = repo.head.commit.hexsha[0:sha_length]
63 return git_sha
66def git_local_changes_present(directory: Path) -> bool:
67 """
68 Check if the git repo has local changes.
70 Arguments:
71 directory: The directory where git commands will be run.
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
82 repo = Repo(directory, search_parent_directories=True)
84 return repo.is_dirty()
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
99 try:
100 Repo(directory, search_parent_directories=True)
101 except InvalidGitRepositoryError:
102 return False
104 return True
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.
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.
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
131 exclude_directories = (
132 []
133 if exclude_directories is None
134 else [exclude_directory.resolve() for exclude_directory in exclude_directories]
135 )
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)
143 repo = Repo(directory, search_parent_directories=True)
144 repo_root = Path(repo.working_dir).resolve()
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
150 if file_endings_avoid is not None and file_path.name.endswith(file_endings_avoid):
151 continue
153 if file_is_in_directory(file_path, exclude_directories):
154 continue
156 if file_is_in_directory(file_path, [directory]):
157 yield file_path