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

1""" 

2A manager for repository repositories. 

3 

4This class allows to maintain a local stash of repository repositories that 

5can consistently be accessed without loading any repository multiple 

6times. 

7""" 

8 

9from dataclasses import dataclass 

10from os import rmdir 

11from typing import Final 

12 

13from pycommons.io.path import Path 

14from pycommons.net.url import URL 

15from pycommons.types import type_error 

16 

17from texgit.repository.file_manager import FileManager 

18from texgit.repository.git import GitRepository 

19 

20 

21@dataclass(frozen=True, init=False, order=True) 

22class GitPath: 

23 """An immutable record of a path inside a git repository.""" 

24 

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 

34 

35 def __init__(self, path: Path, repo: GitRepository, url: URL, 

36 basename: str | None = None): 

37 """ 

38 Set up the information about a repository. 

39 

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) 

55 

56 

57def _make_key(u: URL) -> tuple[str, str]: 

58 """ 

59 Turn a URL into a key. 

60 

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 

73 

74 

75class GitManager(FileManager): 

76 """A git repository manager can provide a set of git repositories.""" 

77 

78 def __init__(self, base_dir: str) -> None: 

79 """ 

80 Set up the git repository manager. 

81 

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]] = {} 

87 

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 

93 

94 def _get_sensitive_paths(self) -> list[Path]: 

95 """ 

96 Get the list of sensitive paths. 

97 

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 

103 

104 def get_repository(self, url: str) -> GitRepository: 

105 """ 

106 Get the git repository for the given URL. 

107 

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 

128 

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. 

133 

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

147 

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. 

151 

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) 

157 

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. 

161 

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)