Coverage for texgit / repository / git_manager.py: 90%
78 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-22 02:50 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-22 02:50 +0000
1"""
2A manager for repository repositories.
4This class allows to maintain a local stash of repository repositories that
5can consistently be accessed without loading any repository multiple
6times.
7"""
9from dataclasses import dataclass
10from os import rmdir
11from typing import Final
13from pycommons.io.path import Path
14from pycommons.net.url import URL
15from pycommons.types import type_error
17from texgit.repository.file_manager import FileManager
18from texgit.repository.git import GitRepository
21@dataclass(frozen=True, init=False, order=True)
22class GitPath:
23 """An immutable record of a path inside a git repository."""
25 #: the absolute path of the file or directory inside the repository
26 #: directory
27 path: Path
28 #: the base name of the file
29 basename: str
30 #: the repository
31 repo: GitRepository
32 #: the URL
33 url: URL
35 def __init__(self, path: Path, repo: GitRepository, url: URL,
36 basename: str | None = None):
37 """
38 Set up the information about a repository.
40 :param path: the absolute path to the file or directory
41 :param repo: the repository
42 """
43 if not isinstance(path, Path):
44 raise type_error(path, "path_in_repo", Path)
45 if not isinstance(repo, GitRepository):
46 raise type_error(repo, "repo", GitRepository)
47 if not isinstance(url, URL):
48 raise type_error(url, "url", URL)
49 basename = path.basename() if basename is None \
50 else str.strip(basename)
51 object.__setattr__(self, "path", path)
52 object.__setattr__(self, "repo", repo)
53 object.__setattr__(self, "url", url)
54 object.__setattr__(self, "basename", basename)
57def _make_key(u: URL) -> tuple[str, str]:
58 """
59 Turn a URL into a key.
61 :param u: the url
62 :return: the key
63 """
64 pt: str = u.path
65 while pt.startswith("/"):
66 pt = pt[1:]
67 while pt.endswith(".git"):
68 pt = pt[:-4]
69 while pt.endswith("/"):
70 pt = pt[:-1]
71 pt = pt.replace("/", "_")
72 return "gh" if u.host.lower() == "github.com" else u.host, pt
75class GitManager(FileManager):
76 """A git repository manager can provide a set of git repositories."""
78 def __init__(self, base_dir: str) -> None:
79 """
80 Set up the git repository manager.
82 :param base_dir: the base directory
83 """
84 super().__init__(base_dir)
85 #: the internal set of github repositories
86 self.__repos: Final[dict[tuple[str, str], GitRepository]] = {}
88 #: load all the repository repositories
89 for the_dir in self.list_realm("git", files=False, directories=True):
90 if the_dir.resolve_inside(".git").is_dir():
91 gr: GitRepository = GitRepository.from_local(the_dir)
92 self.__repos[_make_key(gr.url)] = gr
94 def _get_sensitive_paths(self) -> list[Path]:
95 """
96 Get the list of sensitive paths.
98 :return: the paths
99 """
100 paths: Final[list[Path]] = super()._get_sensitive_paths()
101 paths.extend(r.path for r in self.__repos.values())
102 return paths
104 def get_repository(self, url: str) -> GitRepository:
105 """
106 Get the git repository for the given URL.
108 :param url: the URL to load
109 :return: the repository
110 """
111 self._check_open()
112 use_url: Final[URL] = URL(url)
113 key: Final[tuple[str, str]] = _make_key(use_url)
114 if key in self.__repos:
115 return self.__repos[key]
116 name: str = "_".join(key)
117 dirpath, found = self.get_dir("git", name)
118 if not found:
119 raise ValueError("Inconsistent archive state!")
120 try:
121 gt: Final[GitRepository] = GitRepository.download(use_url, dirpath)
122 except ValueError:
123 rmdir(dirpath)
124 raise
125 self.__repos[key] = gt
126 self.__repos[_make_key(gt.url)] = gt
127 return gt
129 def __get_git(self, repo_url: str, relative_path: str,
130 is_file: bool) -> GitPath:
131 """
132 Get a file or directory from the given repository.
134 :param repo_url: the repository URL
135 :param relative_path: the relative path to the file or directory
136 :param is_file: should it be a file (`True`) or directory (`False`)
137 :return: the path and the URL
138 """
139 relative_path = str.strip(relative_path)
140 repo: Final[GitRepository] = self.get_repository(repo_url)
141 dest: Final[Path] = repo.path.resolve_inside(relative_path)
142 if is_file:
143 dest.enforce_file()
144 else:
145 dest.enforce_dir()
146 return GitPath(dest, repo, repo.make_url(dest))
148 def get_git_file(self, repo_url: str, relative_file: str) -> GitPath:
149 """
150 Get a path to a file from the given git repository and also the URL.
152 :param repo_url: the repository url.
153 :param relative_file: the relative path
154 :return: a tuple of file and URL
155 """
156 return self.__get_git(repo_url, relative_file, True)
158 def get_git_dir(self, repo_url: str, relative_dir: str) -> GitPath:
159 """
160 Get a path to a directory from the given git repository.
162 :param repo_url: the repository url.
163 :param relative_dir: the relative path
164 :return: a tuple of directory and URL
165 """
166 return self.__get_git(repo_url, relative_dir, False)