Coverage for src/git_dag/git_objects.py: 93%

145 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-08 12:49 +0200

1"""Pydantic models of git objects. 

2 

3Warning 

4-------- 

5Pydantic objects defined in this module are documented `here 

6<../pydantic_models.html#git-objects>`_. 

7 

8""" 

9 

10from __future__ import annotations 

11 

12import abc 

13from enum import Enum 

14from typing import ClassVar, Optional, cast 

15 

16from pydantic import BaseModel, ConfigDict, Field, computed_field 

17 

18from .constants import DictStrStr 

19 

20GitCommitRawDataType = dict[str, str | list[str]] 

21""" 

22Type of the data associated with a git commit object. 

23 

24value ``str`` is for the tree associated with a commit 

25value ``list[str]`` is for the parents (there can be 0, 1 or many). 

26""" 

27 

28#: Type of raw data associated with a git tree object 

29GitTreeRawDataType = list[DictStrStr] 

30 

31#: Type of raw data associated with a git tag object 

32GitTagRawDataType = DictStrStr 

33 

34 

35class GitObjectKind(str, Enum): 

36 """Git object kind/type.""" 

37 

38 blob = "blob" 

39 tree = "tree" 

40 commit = "commit" 

41 tag = "tag" 

42 

43 

44class GitObject(BaseModel, abc.ABC): 

45 """A base class for git objects.""" 

46 

47 model_config = ConfigDict(extra="forbid") 

48 

49 @property 

50 @abc.abstractmethod 

51 def kind(self) -> GitObjectKind: 

52 """The object type.""" 

53 

54 @computed_field(repr=True) 

55 def is_ready(self) -> bool: 

56 """Indicates whether the object is ready to use. 

57 

58 Note 

59 ----- 

60 See note in :func:`~git_dag.git_repository.GitInspector.get_raw_objects`. 

61 

62 """ 

63 return self._is_ready 

64 

65 # https://docs.pydantic.dev/2.0/usage/computed_fields/ 

66 @is_ready.setter # type: ignore[no-redef] 

67 def is_ready(self, ready: bool) -> None: 

68 self._is_ready = ready 

69 

70 sha: str 

71 

72 _is_ready: bool = False 

73 

74 

75class GitBlob(GitObject): 

76 """Git blob object.""" 

77 

78 model_config = ConfigDict(extra="forbid") 

79 

80 kind: ClassVar[GitObjectKind] = GitObjectKind.blob 

81 _is_ready: bool = True 

82 

83 

84class GitTag(GitObject): 

85 """Git (annotated) tag object.""" 

86 

87 model_config = ConfigDict(extra="forbid") 

88 

89 kind: ClassVar[GitObjectKind] = GitObjectKind.tag 

90 name: str 

91 

92 raw_data: GitTagRawDataType = Field(repr=False) 

93 

94 # I keep track of deleted (annotated) tags that haven't been garbage-collected 

95 is_deleted: bool = False 

96 

97 _anchor: GitObject 

98 

99 @property 

100 def anchor(self) -> GitObject: 

101 """Return the associated anchor. 

102 

103 Note 

104 ----- 

105 An annotated tag can point to another tag: https://stackoverflow.com/a/19812276 

106 

107 """ 

108 return self._anchor 

109 

110 @anchor.setter 

111 def anchor(self, anchor: GitObject) -> None: 

112 self._anchor = anchor 

113 

114 @property 

115 def tagger(self) -> str: 

116 """Return tagger.""" 

117 return self.raw_data["taggername"] 

118 

119 @property 

120 def tagger_email(self) -> str: 

121 """Return tagger email.""" 

122 return self.raw_data["taggeremail"] 

123 

124 @property 

125 def tagger_date(self) -> str: 

126 """Return tagger date.""" 

127 return self.raw_data["taggerdate"] 

128 

129 @property 

130 def message(self) -> str: 

131 """Return the message.""" 

132 return self.raw_data["message"] 

133 

134 

135class GitCommit(GitObject): 

136 """Git commit object.""" 

137 

138 model_config = ConfigDict(extra="forbid") 

139 

140 kind: ClassVar[GitObjectKind] = GitObjectKind.commit 

141 is_reachable: bool 

142 

143 raw_data: GitCommitRawDataType = Field(repr=False) 

144 _tree: GitTree 

145 _parents: list[GitCommit] 

146 

147 @property 

148 def tree(self) -> GitTree: 

149 """Return the associated tree (there can be exactly one).""" 

150 return self._tree 

151 

152 @tree.setter 

153 def tree(self, tree: GitTree) -> None: 

154 self._tree = tree 

155 

156 @property 

157 def parents(self) -> list[GitCommit]: 

158 """Return the parents.""" 

159 return self._parents 

160 

161 @parents.setter 

162 def parents(self, parents: list[GitCommit]) -> None: 

163 self._parents = parents 

164 

165 @property 

166 def author(self) -> str: 

167 """Return the author.""" 

168 return cast(str, self.raw_data["author"]) 

169 

170 @property 

171 def author_email(self) -> str: 

172 """Return the author email.""" 

173 return cast(str, self.raw_data["author_email"]) 

174 

175 @property 

176 def author_date(self) -> str: 

177 """Return the author date.""" 

178 return cast(str, self.raw_data["author_date"]) 

179 

180 @property 

181 def committer(self) -> str: 

182 """Return the committer.""" 

183 return cast(str, self.raw_data["committer"]) 

184 

185 @property 

186 def committer_email(self) -> str: 

187 """Return the committer email.""" 

188 return cast(str, self.raw_data["committer_email"]) 

189 

190 @property 

191 def committer_date(self) -> str: 

192 """Return the committer date.""" 

193 return cast(str, self.raw_data["committer_date"]) 

194 

195 @property 

196 def message(self) -> str: 

197 """Return the commit message.""" 

198 return cast(str, self.raw_data["message"]) 

199 

200 

201class GitTree(GitObject): 

202 """Git tree object.""" 

203 

204 model_config = ConfigDict(extra="forbid") 

205 

206 kind: ClassVar[GitObjectKind] = GitObjectKind.tree 

207 

208 #: Raw data. 

209 raw_data: GitTreeRawDataType = Field(repr=False) 

210 

211 #: Child trees and blobs. 

212 _children: list[GitTree | GitBlob] 

213 

214 # Set to True when it is known apriory that there would be no children 

215 # e.g., for the empty git tree object 

216 no_children: bool = False 

217 

218 @property 

219 def children(self) -> list[GitTree | GitBlob]: 

220 """Return the children.""" 

221 if self.no_children: 

222 return [] 

223 return self._children 

224 

225 @children.setter 

226 def children(self, children: list[GitTree | GitBlob]) -> None: 

227 if self.no_children and children: 

228 raise TypeError("Attempting to set children when there should be none.") 

229 self._children = children 

230 

231 

232class GitTagLightweight(BaseModel): 

233 """Git lightweight tag (this is not a ``GitObject``).""" 

234 

235 model_config = ConfigDict(extra="forbid") 

236 

237 name: str 

238 anchor: GitObject 

239 

240 

241class GitBranch(BaseModel): 

242 """A branch.""" 

243 

244 model_config = ConfigDict(extra="forbid") 

245 

246 name: str 

247 commit: GitCommit 

248 is_local: bool = False 

249 tracking: Optional[str] = None 

250 

251 

252class GitStash(BaseModel): 

253 """A stash.""" 

254 

255 model_config = ConfigDict(extra="forbid") 

256 

257 index: int 

258 title: str 

259 commit: GitCommit 

260 

261 

262class GitHead(BaseModel): 

263 """A head (local or remote).""" 

264 

265 model_config = ConfigDict(extra="forbid") 

266 

267 commit: Optional[GitCommit] = None 

268 branch: Optional[GitBranch] = None 

269 

270 @property 

271 def is_defined(self) -> bool: 

272 """Is the HEAD defined.""" 

273 return self.commit is not None 

274 

275 @property 

276 def is_detached(self) -> bool: 

277 """Is the HEAD detached.""" 

278 return self.branch is None 

279 

280 def __repr__(self) -> str: 

281 if not self.is_defined: 

282 return "None" 

283 

284 if self.is_detached: 

285 return "DETACHED" 

286 

287 # type narrowing to make mypy happy 

288 assert (self.commit is not None) and (self.branch is not None) 

289 

290 return f"{self.commit.sha} ({self.branch.name})"