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
« 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# --------------------------------------------------------------------------------------------------
9from __future__ import annotations
11import os
12from pathlib import Path
13from typing import TYPE_CHECKING
15from tsfpga.system_utils import file_is_in_directory
17if TYPE_CHECKING:
18 from collections.abc import Iterator
20 from git.objects.tree import Tree
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)"``.
28 Arguments:
29 directory: The directory where git commands will be run.
30 use_rst_annotation: Use reStructuredText literal annotation for the SHA value.
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}"
39 if git_local_changes_present(directory=directory):
40 result += " (local changes present)"
42 return result
45def get_git_sha(directory: Path) -> str:
46 """
47 Get a short git SHA.
49 Arguments:
50 directory: The directory where git commands will be run.
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
60 if "GIT_COMMIT" in os.environ:
61 return os.environ["GIT_COMMIT"][0:sha_length]
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
67 repo = Repo(directory, search_parent_directories=True)
68 return repo.head.commit.hexsha[0:sha_length]
71def git_local_changes_present(directory: Path) -> bool:
72 """
73 Check if the git repo has local changes.
75 Arguments:
76 directory: The directory where git commands will be run.
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
85 repo = Repo(directory, search_parent_directories=True)
87 return repo.is_dirty()
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
100 try:
101 Repo(directory, search_parent_directories=True)
102 except InvalidGitRepositoryError:
103 return False
105 return True
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.
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.
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
130 exclude_directories = (
131 []
132 if exclude_directories is None
133 else [exclude_directory.resolve() for exclude_directory in exclude_directories]
134 )
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)
142 repo = Repo(directory, search_parent_directories=True)
143 repo_root = Path(repo.working_dir).resolve()
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
149 if file_endings_avoid is not None and file_path.name.endswith(file_endings_avoid):
150 continue
152 if file_is_in_directory(file_path, exclude_directories):
153 continue
155 if file_is_in_directory(file_path, [directory]):
156 yield file_path