Coverage for lobster/tools/core/online_report/online_report.py: 66%
132 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-26 14:55 +0000
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-26 14:55 +0000
1#!/usr/bin/env python3
2#
3# lobster_online_report - Transform file references to github references
4# Copyright (C) 2023-2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU Affero General Public License as
8# published by the Free Software Foundation, either version 3 of the
9# License, or (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful, but
12# WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14# Affero General Public License for more details.
15#
16# You should have received a copy of the GNU Affero General Public
17# License along with this program. If not, see
18# <https://www.gnu.org/licenses/>.
20import re
21import sys
22import os
23import argparse
24import configparser
25import subprocess
27from lobster.report import Report
28from lobster.location import File_Reference, Github_Reference
29from lobster.version import get_version
32class Parse_Error(Exception):
33 pass
36def is_git_main_module(path):
37 return os.path.isdir(os.path.join(path, ".git"))
40def is_dir_in_git_submodule(directory):
41 """
42 Checks if a given directory is nested inside a Git submodule.
44 Args:
45 directory (str): The path to the directory to check.
47 Returns:
48 bool: True if the directory is inside a Git submodule,
49 False otherwise.
50 str: The path to the superproject of submodule.
51 """
52 try:
53 # Check if the directory is part of a Git submodule
54 result = subprocess.run(['git',
55 'rev-parse',
56 '--show-superproject-working-tree'],
57 cwd=directory,
58 stdout=subprocess.PIPE,
59 stderr=subprocess.PIPE,
60 universal_newlines=True,
61 check=True)
62 if result.returncode == 0 and result.stdout.strip(): 62 ↛ 63line 62 didn't jump to line 63 because the condition on line 62 was never true
63 return True, result.stdout.strip()
64 else:
65 return False, ''
66 except (subprocess.CalledProcessError, OSError):
67 return False, ''
70def is_dir_in_git_main_module(directory):
71 """
72 Checks if a given directory is nested inside a Git main module.
74 Args:
75 directory (str): The path to the directory to check.
77 Returns:
78 bool: True if the directory is inside a Git mainmodule,
79 False otherwise.
80 str: The path to the mainmodule.
81 """
82 try:
83 # Check if the directory is part of a Git main module
84 result = subprocess.run(['git', 'rev-parse', '--show-toplevel'],
85 cwd=directory,
86 stdout=subprocess.PIPE,
87 stderr=subprocess.PIPE,
88 universal_newlines=True,
89 check=True)
90 if result.returncode == 0 and result.stdout.strip(): 90 ↛ 93line 90 didn't jump to line 93 because the condition on line 90 was always true
91 return True, result.stdout.strip()
92 else:
93 return False, ''
94 except (subprocess.CalledProcessError, OSError):
95 return False, ''
98def find_repo_main_root(file_path):
99 """
100 Find the main root repository.
102 Args:
103 file_path (str): The path to the file to check.
105 Returns:
106 str: The path to the main root repository.
107 """
108 file_path = os.path.abspath(file_path)
110 is_submodule, submodule_superproject_path = \
111 is_dir_in_git_submodule(os.path.dirname(file_path))
113 if is_submodule: 113 ↛ 114line 113 didn't jump to line 114 because the condition on line 113 was never true
114 return submodule_superproject_path
116 is_mainmodule, mainmodule_path = \
117 is_dir_in_git_main_module(os.path.dirname(file_path))
119 return mainmodule_path if is_mainmodule else os.getcwd()
122def path_starts_with_subpath(path, subpath):
123 path = os.path.normcase(path)
124 subpath = os.path.normcase(subpath)
125 return path.startswith(subpath)
128def parse_git_root(cfg):
129 gh_root = cfg["url"]
130 if not gh_root.endswith(".git"): 130 ↛ 133line 130 didn't jump to line 133 because the condition on line 130 was always true
131 gh_root += ".git"
133 if gh_root.startswith("http"): 133 ↛ 136line 133 didn't jump to line 136 because the condition on line 133 was always true
134 gh_root = gh_root[:-4]
135 else:
136 match = re.match(r"^(.*)@(.*):(.*)\.git$", gh_root)
137 if match is None:
138 raise Parse_Error("could not understand git origin %s" % gh_root)
139 gh_root = "https://%s/%s" % (match.group(2),
140 match.group(3))
142 return gh_root
145def add_github_reference_to_items(gh_root, gh_submodule_roots, repo_root, report):
146 """Function to add GitHub reference to items of the report"""
147 git_hash_cache = {}
148 for item in report.items.values():
149 if isinstance(item.location, File_Reference): 149 ↛ 148line 149 didn't jump to line 148 because the condition on line 149 was always true
150 assert (os.path.isdir(item.location.filename) or
151 os.path.isfile(item.location.filename))
153 actual_path, actual_repo, commit = get_git_commit_hash_repo_and_path(
154 gh_root, gh_submodule_roots, item, repo_root, git_hash_cache)
155 loc = Github_Reference(
156 gh_root=actual_repo,
157 filename=actual_path,
158 line=item.location.line,
159 commit=commit)
160 item.location = loc
163def get_git_commit_hash_repo_and_path(gh_root, gh_submodule_roots,
164 item, repo_root, git_hash_cache):
165 """Function to get git commit hash for the item file which is part of either the
166 root repo or the submodules."""
167 rel_path_from_root = os.path.relpath(
168 os.path.realpath(item.location.filename),
169 os.path.realpath(repo_root),
170 )
171 # pylint: disable=possibly-used-before-assignment
172 actual_repo = gh_root
173 actual_path = rel_path_from_root
174 git_repo = repo_root
175 # pylint: disable=consider-using-dict-items
176 for prefix in gh_submodule_roots: 176 ↛ 177line 176 didn't jump to line 177 because the loop on line 176 never started
177 if path_starts_with_subpath(rel_path_from_root, prefix):
178 actual_repo = gh_submodule_roots[prefix]
179 actual_path = rel_path_from_root[len(prefix) + 1:]
180 git_repo = prefix
181 break
182 commit = git_hash_cache.get(git_repo, None)
183 if not commit:
184 git_repo_path = repo_root
185 if git_repo and git_repo != repo_root: 185 ↛ 186line 185 didn't jump to line 186 because the condition on line 185 was never true
186 git_repo_path = os.path.normpath(os.path.join(repo_root, git_repo))
187 commit = get_hash_for_git_commit(git_repo_path)
188 git_hash_cache[git_repo] = commit.strip()
190 return actual_path, actual_repo, commit
193def get_hash_for_git_commit(repo_root):
194 return subprocess.check_output(
195 ["git", "rev-parse", "HEAD"], cwd=repo_root
196 ).decode().strip()
199def get_summary(in_file: str, out_file: str):
200 if in_file == out_file:
201 return f"LOBSTER report {in_file} modified to use online references."
202 return f"LOBSTER report {out_file} created, using online references."
205ap = argparse.ArgumentParser()
208@get_version(ap)
209def main():
210 # lobster-trace: core_online_report_req.Dummy_Requirement
211 ap.add_argument("lobster_report",
212 nargs="?",
213 default="report.lobster")
214 ap.add_argument("--repo-root",
215 help="override git repository root",
216 default=None)
217 ap.add_argument("--out",
218 help="output file, by default overwrite input",
219 default=None)
220 options = ap.parse_args()
222 if not os.path.isfile(options.lobster_report): 222 ↛ 223line 222 didn't jump to line 223 because the condition on line 222 was never true
223 if options.lobster_report == "report.lobster":
224 ap.error("specify report file")
225 else:
226 ap.error("%s is not a file" % options.lobster_report)
228 if options.repo_root: 228 ↛ 229line 228 didn't jump to line 229 because the condition on line 228 was never true
229 repo_root = os.path.abspath(os.path.expanduser(options.repo_root))
230 if not is_git_main_module(repo_root):
231 ap.error("cannot find .git directory in %s" % options.repo_root)
232 else:
233 repo_root = find_repo_main_root(options.lobster_report)
234 while True:
235 if is_git_main_module(repo_root): 235 ↛ 237line 235 didn't jump to line 237 because the condition on line 235 was always true
236 break
237 new_root = os.path.dirname(repo_root)
238 if new_root == repo_root:
239 print("error: could not find .git directory")
240 return 1
241 repo_root = new_root
243 git_config = configparser.ConfigParser()
244 git_config.read(os.path.join(repo_root, ".git", "config"))
245 if 'remote "origin"' not in git_config.sections(): 245 ↛ 246line 245 didn't jump to line 246 because the condition on line 245 was never true
246 print("error: could not find remote \"origin\" in git config")
247 return 1
249 git_m_config = configparser.ConfigParser()
250 if os.path.isfile(os.path.join(repo_root, ".gitmodules")): 250 ↛ 251line 250 didn't jump to line 251 because the condition on line 250 was never true
251 git_m_config.read(os.path.join(repo_root, ".gitmodules"))
253 gh_root = None
254 gh_submodule_roots = {}
255 for item in git_config:
256 if item == 'remote "origin"':
257 gh_root = parse_git_root(git_config[item])
258 elif item.startswith('submodule "'): 258 ↛ 259line 258 didn't jump to line 259 because the condition on line 258 was never true
259 assert re.match('submodule "(.*?)"', item)
260 sm_dir = git_m_config[item]["path"]
261 gh_submodule_roots[sm_dir] = parse_git_root(git_config[item])
263 report = Report()
264 report.load_report(options.lobster_report)
265 if gh_root: 265 ↛ 268line 265 didn't jump to line 268 because the condition on line 265 was always true
266 add_github_reference_to_items(gh_root, gh_submodule_roots, repo_root, report)
268 out_file = options.out if options.out else options.lobster_report
269 report.write_report(out_file)
270 print(get_summary(options.lobster_report, out_file))
271 return 0
274if __name__ == "__main__":
275 sys.exit(main())