Coverage for app / helpers / buildinfo.py: 95%
87 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-06 04:49 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-06 04:49 +0000
1"""
2This module handles the generation and management of a `build-info.json` file, which stores
3metadata about the current build and Git repository. It includes functionality to check,
4generate, read, remove, and write build metadata based on the repository's state.
5"""
6import logging
7import os
8import json
9from pathlib import Path
10from datetime import datetime, timezone
11from dulwich.repo import Repo
12from dulwich.errors import NotGitRepository
15PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
16BUILD_INFO_FILE = PROJECT_ROOT / "build-info.json"
19def check_and_generate_build_info():
20 """
21 Checks if the file `build-info.json` exists and regenerates it if necessary. The function
22 compares timestamps between the last build and the latest commit to determine if
23 regeneration is needed.
24 """
26 if os.path.isfile(BUILD_INFO_FILE):
27 logging.debug("%s exists. Checking timestamps...", BUILD_INFO_FILE)
29 try:
30 # Load the existing `build-info.json` file into memory
31 with open(BUILD_INFO_FILE, 'r', encoding='utf-8') as f:
32 existing_build_info = json.load(f)
34 existing_commit_date = existing_build_info.get("commit_date", "")
36 # Open the repository
37 repo = Repo(PROJECT_ROOT)
39 # Get the latest commit details
40 head_ref = repo.head()
41 latest_commit = repo[head_ref]
42 latest_commit_date = datetime.fromtimestamp(latest_commit.commit_time).isoformat()
44 # Compare timestamps
45 if existing_commit_date:
46 existing_timestamp = (datetime.fromisoformat(existing_commit_date)
47 .replace(tzinfo=timezone.utc))
48 latest_timestamp = (datetime.fromisoformat(latest_commit_date)
49 .replace(tzinfo=timezone.utc))
51 if latest_timestamp > existing_timestamp:
52 logging.debug(
53 "Latest commit is newer than the one in %s. "
54 "Regenerating the build-info.json file...", BUILD_INFO_FILE
55 )
56 _generate_build_info(repo)
57 else:
58 logging.debug("%s is up-to-date.", BUILD_INFO_FILE)
59 else:
60 logging.debug("Existing commit date is invalid. Regenerating...")
61 _generate_build_info(repo)
63 except NotGitRepository:
64 logging.warning("Git repository not found. Leaving existing build-info.json untouched.")
66 else:
67 logging.debug("%s does not exist. Generating a new one...", BUILD_INFO_FILE)
68 _generate_build_info()
71def read_build_info() -> dict:
72 """
73 Reads and parses the build information from a predefined file.
75 This function ensures that the predefined build information file
76 exists and is in valid JSON format. The file is read into a Python
77 dictionary, which is then validated and returned.
79 :return: A dictionary containing the build information read from the
80 specified file.
81 :rtype: dict
82 :raises AssertionError: If the build information file does not exist.
83 :raises AssertionError: If the parsed data is not a dictionary.
84 """
85 if not BUILD_INFO_FILE.exists(): 85 ↛ 86line 85 didn't jump to line 86 because the condition on line 85 was never true
86 raise AssertionError(f"Build information file does not exist: {BUILD_INFO_FILE}")
87 build_info = json.loads(BUILD_INFO_FILE.read_text())
88 if not isinstance(build_info, dict): 88 ↛ 89line 88 didn't jump to line 89 because the condition on line 88 was never true
89 raise AssertionError("Build information must be a dictionary")
90 return build_info
93def write_empty_build_info():
94 """
95 Writes an empty build information dictionary to a JSON file named `build-info.json`.
97 The function initializes a dictionary with keys for branch name, commit ID,
98 committer, comment, build date, and commit date. It assigns an empty string
99 to each of these keys. The dictionary is then serialized and written to a
100 JSON file with a filename specified by the constant `BUILD_INFO_FILE`.
102 :raises FileNotFoundError: Raised if the specified file path does not exist
103 or cannot be accessed for writing.
104 """
105 build_info = {
106 "branch": "",
107 "commit_id": "",
108 "committer": "",
109 "comment": "",
110 "build_date": "",
111 "commit_date": "",
112 }
113 # Write to the build-info.json file
114 with open(BUILD_INFO_FILE, 'w', encoding='utf-8') as f:
115 json.dump(build_info, f, indent=4)
118def remove_build_info():
119 """
120 Removes the build information file if it exists. If the file does not exist,
121 the operation gracefully handles the absence without raising an exception.
123 :raises FileNotFoundError: Raised internally but handled by the function
124 when the file does not exist.
125 """
126 # remove any existing file
127 try:
128 BUILD_INFO_FILE.unlink()
129 except FileNotFoundError:
130 pass
133def _generate_build_info(repo: Repo = None):
134 """
135 Generates a `build-info.json` file containing metadata from the Git repository if it
136 is available. If the repository is not found, default values are used instead. The
137 generated file includes details such as the branch name, commit ID, committer, comment,
138 build date, and commit date.
139 """
140 build_date = datetime.now(timezone.utc).isoformat() # Build date in UTC
142 try:
143 # Open the repository, if not passed in
144 if not repo:
145 repo = Repo(PROJECT_ROOT)
147 # Get the latest commit details
148 head_ref = repo.head()
150 commit = repo[head_ref]
151 commit_id = commit.id.decode()
152 committer = commit.committer.decode()
153 comment = commit.message.decode().strip()
154 commit_date = datetime.fromtimestamp(commit.commit_time, timezone.utc).isoformat()
155 branch = _get_commit_target_branch(repo)
157 build_info = {
158 "branch": branch,
159 "commit_id": commit_id,
160 "committer": committer,
161 "comment": comment,
162 "build_date": build_date,
163 "commit_date": commit_date,
164 }
166 except NotGitRepository:
167 # Fallback logic if the Git repository is not initialized or unavailable
168 logging.warning("Git repository not found. Using default values.")
169 build_info = {
170 "branch": "",
171 "commit_id": "",
172 "committer": "",
173 "comment": "",
174 "build_date": build_date,
175 "commit_date": "",
176 }
178 # Write to the build-info.json file
179 with open(BUILD_INFO_FILE, 'w', encoding='utf-8') as f:
180 json.dump(build_info, f, indent=4)
181 logging.debug("Generated %s successfully.", BUILD_INFO_FILE)
184def _get_commit_target_branch(repo: Repo) -> str:
185 """
186 Determines the branch that would receive the next commit in the current repository.
187 If the repository is in a detached `HEAD` state, attempts to identify the branch that
188 tracks the current commit, if any.
189 """
190 try:
191 # Read the HEAD reference
192 head_ref = repo.refs[b"HEAD"]
194 if not head_ref:
195 return "(none)" # No HEAD exists
197 # If HEAD points to a branch, extract the branch name
198 if head_ref.startswith(b"refs/heads/"):
199 return head_ref.decode("utf-8")[len("refs/heads/"):] # Current branch name
201 # If HEAD does not point to a branch, we are in a detached HEAD state.
202 # Check for the branch containing the current HEAD commit.
203 head_commit = head_ref
205 # Iterate through all refs to find the branch tracking the HEAD commit
206 for ref in repo.refs: # repo.refs supports iteration over keys
207 if ref.startswith(b"refs/heads/"): # Only check local branches
208 if repo.refs[ref] == head_commit: # Check if the commit matches
209 return ref.decode("utf-8")[len("refs/heads/"):] # Extract branch name
211 # No branch found tracking the current commit, or the repository is detached
212 # without any tracking information
213 return "(detached)"
214 except Exception as e: # pylint: disable=broad-except
215 logging.error("An unexpected error occurred: %s", e, exc_info=True)
216 return f"(error: {e})"
219__all__ = ["check_and_generate_build_info", "read_build_info", "write_empty_build_info",
220 "remove_build_info", "BUILD_INFO_FILE", "PROJECT_ROOT"]
222if __name__ == "__main__": 222 ↛ 223line 222 didn't jump to line 223 because the condition on line 222 was never true
223 check_and_generate_build_info()