Coverage for lobster/tools/core/online_report/path_to_url_converter.py: 36%
51 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-09-22 04:43 +0000
« 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/>.
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
27@dataclass
28class UrlParts:
29 url_start: str
30 commit_hash: str
31 path_html: str
34class NotInsideRepositoryException(Exception):
35 """Exception raised when a path is not inside a Git repository."""
38class PathToUrlConverter:
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)
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())
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('/')}"
57 def path_to_url(self, path: Path, commit_id: str) -> UrlParts:
58 """Convert a path to a URL based on the submodule configuration.
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
79 url_start = self._url_base.rstrip("/")
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)
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.
89 Example:
90 path = "/home/<user>/git/swh/repo/domains/driving/folder1/folder2/file.cpp"
91 submodule.path = "domains/driving"
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
106 raise KeyError(f"No submodule found for path: {path}")