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
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-08 12:49 +0200
1"""Pydantic models of git objects.
3Warning
4--------
5Pydantic objects defined in this module are documented `here
6<../pydantic_models.html#git-objects>`_.
8"""
10from __future__ import annotations
12import abc
13from enum import Enum
14from typing import ClassVar, Optional, cast
16from pydantic import BaseModel, ConfigDict, Field, computed_field
18from .constants import DictStrStr
20GitCommitRawDataType = dict[str, str | list[str]]
21"""
22Type of the data associated with a git commit object.
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"""
28#: Type of raw data associated with a git tree object
29GitTreeRawDataType = list[DictStrStr]
31#: Type of raw data associated with a git tag object
32GitTagRawDataType = DictStrStr
35class GitObjectKind(str, Enum):
36 """Git object kind/type."""
38 blob = "blob"
39 tree = "tree"
40 commit = "commit"
41 tag = "tag"
44class GitObject(BaseModel, abc.ABC):
45 """A base class for git objects."""
47 model_config = ConfigDict(extra="forbid")
49 @property
50 @abc.abstractmethod
51 def kind(self) -> GitObjectKind:
52 """The object type."""
54 @computed_field(repr=True)
55 def is_ready(self) -> bool:
56 """Indicates whether the object is ready to use.
58 Note
59 -----
60 See note in :func:`~git_dag.git_repository.GitInspector.get_raw_objects`.
62 """
63 return self._is_ready
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
70 sha: str
72 _is_ready: bool = False
75class GitBlob(GitObject):
76 """Git blob object."""
78 model_config = ConfigDict(extra="forbid")
80 kind: ClassVar[GitObjectKind] = GitObjectKind.blob
81 _is_ready: bool = True
84class GitTag(GitObject):
85 """Git (annotated) tag object."""
87 model_config = ConfigDict(extra="forbid")
89 kind: ClassVar[GitObjectKind] = GitObjectKind.tag
90 name: str
92 raw_data: GitTagRawDataType = Field(repr=False)
94 # I keep track of deleted (annotated) tags that haven't been garbage-collected
95 is_deleted: bool = False
97 _anchor: GitObject
99 @property
100 def anchor(self) -> GitObject:
101 """Return the associated anchor.
103 Note
104 -----
105 An annotated tag can point to another tag: https://stackoverflow.com/a/19812276
107 """
108 return self._anchor
110 @anchor.setter
111 def anchor(self, anchor: GitObject) -> None:
112 self._anchor = anchor
114 @property
115 def tagger(self) -> str:
116 """Return tagger."""
117 return self.raw_data["taggername"]
119 @property
120 def tagger_email(self) -> str:
121 """Return tagger email."""
122 return self.raw_data["taggeremail"]
124 @property
125 def tagger_date(self) -> str:
126 """Return tagger date."""
127 return self.raw_data["taggerdate"]
129 @property
130 def message(self) -> str:
131 """Return the message."""
132 return self.raw_data["message"]
135class GitCommit(GitObject):
136 """Git commit object."""
138 model_config = ConfigDict(extra="forbid")
140 kind: ClassVar[GitObjectKind] = GitObjectKind.commit
141 is_reachable: bool
143 raw_data: GitCommitRawDataType = Field(repr=False)
144 _tree: GitTree
145 _parents: list[GitCommit]
147 @property
148 def tree(self) -> GitTree:
149 """Return the associated tree (there can be exactly one)."""
150 return self._tree
152 @tree.setter
153 def tree(self, tree: GitTree) -> None:
154 self._tree = tree
156 @property
157 def parents(self) -> list[GitCommit]:
158 """Return the parents."""
159 return self._parents
161 @parents.setter
162 def parents(self, parents: list[GitCommit]) -> None:
163 self._parents = parents
165 @property
166 def author(self) -> str:
167 """Return the author."""
168 return cast(str, self.raw_data["author"])
170 @property
171 def author_email(self) -> str:
172 """Return the author email."""
173 return cast(str, self.raw_data["author_email"])
175 @property
176 def author_date(self) -> str:
177 """Return the author date."""
178 return cast(str, self.raw_data["author_date"])
180 @property
181 def committer(self) -> str:
182 """Return the committer."""
183 return cast(str, self.raw_data["committer"])
185 @property
186 def committer_email(self) -> str:
187 """Return the committer email."""
188 return cast(str, self.raw_data["committer_email"])
190 @property
191 def committer_date(self) -> str:
192 """Return the committer date."""
193 return cast(str, self.raw_data["committer_date"])
195 @property
196 def message(self) -> str:
197 """Return the commit message."""
198 return cast(str, self.raw_data["message"])
201class GitTree(GitObject):
202 """Git tree object."""
204 model_config = ConfigDict(extra="forbid")
206 kind: ClassVar[GitObjectKind] = GitObjectKind.tree
208 #: Raw data.
209 raw_data: GitTreeRawDataType = Field(repr=False)
211 #: Child trees and blobs.
212 _children: list[GitTree | GitBlob]
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
218 @property
219 def children(self) -> list[GitTree | GitBlob]:
220 """Return the children."""
221 if self.no_children:
222 return []
223 return self._children
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
232class GitTagLightweight(BaseModel):
233 """Git lightweight tag (this is not a ``GitObject``)."""
235 model_config = ConfigDict(extra="forbid")
237 name: str
238 anchor: GitObject
241class GitBranch(BaseModel):
242 """A branch."""
244 model_config = ConfigDict(extra="forbid")
246 name: str
247 commit: GitCommit
248 is_local: bool = False
249 tracking: Optional[str] = None
252class GitStash(BaseModel):
253 """A stash."""
255 model_config = ConfigDict(extra="forbid")
257 index: int
258 title: str
259 commit: GitCommit
262class GitHead(BaseModel):
263 """A head (local or remote)."""
265 model_config = ConfigDict(extra="forbid")
267 commit: Optional[GitCommit] = None
268 branch: Optional[GitBranch] = None
270 @property
271 def is_defined(self) -> bool:
272 """Is the HEAD defined."""
273 return self.commit is not None
275 @property
276 def is_detached(self) -> bool:
277 """Is the HEAD detached."""
278 return self.branch is None
280 def __repr__(self) -> str:
281 if not self.is_defined:
282 return "None"
284 if self.is_detached:
285 return "DETACHED"
287 # type narrowing to make mypy happy
288 assert (self.commit is not None) and (self.branch is not None)
290 return f"{self.commit.sha} ({self.branch.name})"