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

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/>. 

19 

20import re 

21import sys 

22import os 

23import argparse 

24import configparser 

25import subprocess 

26 

27from lobster.report import Report 

28from lobster.location import File_Reference, Github_Reference 

29from lobster.version import get_version 

30 

31 

32class Parse_Error(Exception): 

33 pass 

34 

35 

36def is_git_main_module(path): 

37 return os.path.isdir(os.path.join(path, ".git")) 

38 

39 

40def is_dir_in_git_submodule(directory): 

41 """ 

42 Checks if a given directory is nested inside a Git submodule. 

43 

44 Args: 

45 directory (str): The path to the directory to check. 

46 

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, '' 

68 

69 

70def is_dir_in_git_main_module(directory): 

71 """ 

72 Checks if a given directory is nested inside a Git main module. 

73 

74 Args: 

75 directory (str): The path to the directory to check. 

76 

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, '' 

96 

97 

98def find_repo_main_root(file_path): 

99 """ 

100 Find the main root repository. 

101 

102 Args: 

103 file_path (str): The path to the file to check. 

104 

105 Returns: 

106 str: The path to the main root repository. 

107 """ 

108 file_path = os.path.abspath(file_path) 

109 

110 is_submodule, submodule_superproject_path = \ 

111 is_dir_in_git_submodule(os.path.dirname(file_path)) 

112 

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 

115 

116 is_mainmodule, mainmodule_path = \ 

117 is_dir_in_git_main_module(os.path.dirname(file_path)) 

118 

119 return mainmodule_path if is_mainmodule else os.getcwd() 

120 

121 

122def path_starts_with_subpath(path, subpath): 

123 path = os.path.normcase(path) 

124 subpath = os.path.normcase(subpath) 

125 return path.startswith(subpath) 

126 

127 

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" 

132 

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)) 

141 

142 return gh_root 

143 

144 

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)) 

152 

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 

161 

162 

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() 

189 

190 return actual_path, actual_repo, commit 

191 

192 

193def get_hash_for_git_commit(repo_root): 

194 return subprocess.check_output( 

195 ["git", "rev-parse", "HEAD"], cwd=repo_root 

196 ).decode().strip() 

197 

198 

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." 

203 

204 

205ap = argparse.ArgumentParser() 

206 

207 

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() 

221 

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) 

227 

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 

242 

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 

248 

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")) 

252 

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]) 

262 

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) 

267 

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 

272 

273 

274if __name__ == "__main__": 

275 sys.exit(main())