Coverage for lobster/tools/core/online_report/path_to_url_converter.py: 96%

51 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-09-22 04:43 +0000

1# LOBSTER - Lightweight Open BMW Software Traceability Evidence Report 

2# Copyright (C) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) 

3# 

4# This program is free software: you can redistribute it and/or modify 

5# it under the terms of the GNU Affero General Public License as 

6# published by the Free Software Foundation, either version 3 of the 

7# License, or (at your option) any later version. 

8# 

9# This program is distributed in the hope that it will be useful, but 

10# WITHOUT ANY WARRANTY; without even the implied warranty of 

11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 

12# Affero General Public License for more details. 

13# 

14# You should have received a copy of the GNU Affero General Public 

15# License along with this program. If not, see 

16# <https://www.gnu.org/licenses/>. 

17 

18from dataclasses import dataclass 

19import logging 

20import os 

21from pathlib import Path 

22from typing import Tuple 

23from urllib.parse import quote 

24from git import Repo, Submodule 

25 

26 

27@dataclass 

28class UrlParts: 

29 url_start: str 

30 commit_hash: str 

31 path_html: str 

32 

33 

34class NotInsideRepositoryException(Exception): 

35 """Exception raised when a path is not inside a Git repository.""" 

36 

37 

38class PathToUrlConverter: 

39 

40 def __init__(self, repo_path, url_base: str): 

41 self._logger = logging.getLogger(self.__class__.__name__) 

42 self._repo = Repo(repo_path) 

43 self._url_base = url_base 

44 for submodule in self._repo.submodules: 

45 self._logger.info("Name: %s, Path: %s, URL: %s", 

46 submodule.name, submodule.path, submodule.url) 

47 

48 @staticmethod 

49 def _path_to_html_format(path: Path) -> str: 

50 """Convert a file path to a URL format suitable for HTML links.""" 

51 return quote(path.as_posix()) 

52 

53 def _get_submodule_full_url(self, submodule_url: str) -> str: 

54 """Get the full URL for a submodule.""" 

55 return f"{self._url_base.rstrip('/')}/{submodule_url.lstrip('/')}" 

56 

57 def path_to_url(self, path: Path, commit_id: str) -> UrlParts: 

58 """Convert a path to a URL based on the submodule configuration. 

59 

60 The path must be nested inside a submodule of the repository. 

61 """ 

62 path = path.resolve() 

63 try: 

64 submodule, relative_path = self._get_submodule_and_relative_path(path) 

65 url_start = self._get_submodule_full_url(submodule.url) 

66 commit_hash = submodule.hexsha 

67 except KeyError as e: 

68 self._logger.error("Path '%s' is not inside a submodule: %s", path, e) 

69 # Path is not inside a submodule — use main repo 

70 commit_hash = commit_id 

71 try: 

72 relative_path = path.resolve().relative_to(self._repo.working_tree_dir) 

73 except ValueError as value_error: 

74 raise NotInsideRepositoryException( 

75 f"Path '{path}' is not inside the repository '" 

76 f"{self._repo.working_tree_dir}'!", 

77 ) from value_error 

78 

79 url_start = self._url_base.rstrip("/") 

80 

81 path_html = self._path_to_html_format(relative_path) 

82 return UrlParts(url_start=url_start, commit_hash=commit_hash, 

83 path_html=path_html) 

84 

85 def _get_submodule_and_relative_path(self, path: Path) -> Tuple[Submodule, Path]: 

86 """Get the submodule and the relative path (relative to the submodule folder, 

87 not to repo root) for a given path. 

88 

89 Example: 

90 path = "/home/<user>/git/swh/repo/domains/driving/folder1/folder2/file.cpp" 

91 submodule.path = "domains/driving" 

92 

93 return Submodule instance of "domains/driving", Path of "folder1/folder2/ 

94 file.cpp" 

95 """ 

96 path = path.resolve() 

97 for submodule in self._repo.submodules: 

98 submodule_path = (Path(str(self._repo.working_tree_dir)) / 

99 str(submodule.path).replace("/", os.sep)) 

100 try: 

101 rel_path = path.relative_to(submodule_path) 

102 return submodule, rel_path 

103 except ValueError: 

104 continue 

105 

106 raise KeyError(f"No submodule found for path: {path}")