krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
main: Hutch/Models/Git.swift · raw
1import Foundation
2
3// MARK: - Enums
4
5/// Repository visibility level.
6nonisolated enum Visibility: String, Codable, Sendable {
7 case publicVisibility = "PUBLIC"
8 case unlisted = "UNLISTED"
9 case privateVisibility = "PRIVATE"
10}
11
12/// Repository access mode.
13nonisolated enum AccessMode: String, Codable, Sendable, CaseIterable {
14 case ro = "RO"
15 case rw = "RW"
16}
17
18// MARK: - Entity
19
20/// The `Entity` GraphQL interface from git.sr.ht. Represents the owner of a
21/// resource (typically a user).
22nonisolated struct Entity: Codable, Sendable, Hashable {
23 let canonicalName: String
24}
25
26// MARK: - Repository
27
28/// A git repository from git.sr.ht.
29nonisolated struct Repository: Codable, Sendable, Identifiable {
30 let id: Int
31 let created: Date
32 let updated: Date
33 let name: String
34 let description: String?
35 let visibility: Visibility
36 let readme: String?
37 let accessMode: AccessMode
38 let owner: Entity
39
40 nonisolated enum CodingKeys: String, CodingKey {
41 case id, created, updated, name, description, visibility, readme
42 case accessMode = "access"
43 case owner
44 }
45}
46
47// MARK: - Signature
48
49/// A Git commit/tag signature (author or committer).
50nonisolated struct Signature: Codable, Sendable {
51 let name: String
52 let email: String
53 let time: Date
54}
55
56// MARK: - Trailer
57
58/// A Git commit trailer (e.g. "Signed-off-by", "Co-authored-by").
59nonisolated struct Trailer: Codable, Sendable {
60 let name: String
61 let value: String
62}
63
64// MARK: - Commit
65
66/// A Git commit from git.sr.ht.
67nonisolated struct Commit: Codable, Sendable, Identifiable {
68 let id: String
69 let shortId: String
70 let author: Signature
71 let committer: Signature
72 let message: String
73 let diff: String?
74 let trailers: [Trailer]
75}
76
77// MARK: - Reference
78
79/// A Git reference (branch or tag name).
80nonisolated struct Reference: Codable, Sendable, Hashable {
81 let name: String
82 let target: String?
83}
84
85/// A Git reference enriched with the date of its tip commit or tag, for display in the Refs tab.
86nonisolated struct ReferenceDetail: Sendable, Hashable {
87 let name: String
88 let target: String?
89 let date: Date?
90}
91
92// MARK: - TreeEntry
93
94/// An entry in a Git tree (file or directory).
95nonisolated struct TreeEntry: Codable, Sendable, Identifiable {
96 let id: String
97 let name: String
98 let mode: Int?
99 let object: GitObject?
100}
101
102/// A Git object returned by the git.sr.ht GraphQL API.
103/// The API returns `type` as "TREE", "BLOB", "COMMIT", or "TAG".
104/// Both TextBlob and BinaryBlob have type == "BLOB"; they are
105/// distinguished by the presence of the "text" key (TextBlob) vs
106/// the "content" key (BinaryBlob).
107nonisolated enum GitObject: Sendable {
108 case tree(GitTree)
109 case textBlob(GitTextBlob)
110 case binaryBlob(GitBinaryBlob)
111 case unknown
112}
113
114nonisolated struct GitTree: Codable, Sendable {
115 let id: String?
116 let shortId: String?
117 let entries: GitTreeEntryPage?
118}
119
120nonisolated struct GitTextBlob: Codable, Sendable {
121 let id: String?
122 let shortId: String?
123 let text: String?
124 let size: Int?
125}
126
127nonisolated struct GitBinaryBlob: Codable, Sendable {
128 let id: String?
129 let shortId: String?
130 let size: Int?
131 let content: String?
132}
133
134nonisolated struct GitTreeEntryPage: Codable, Sendable {
135 let results: [TreeEntry]
136 let cursor: String?
137}
138
139// MARK: - GitObject Codable
140
141nonisolated extension GitObject: Codable {
142 private enum CodingKeys: String, CodingKey {
143 case type, id, shortId, entries, text, size, content
144 case typename = "__typename"
145 }
146
147 init(from decoder: any Decoder) throws {
148 let container = try decoder.container(keyedBy: CodingKeys.self)
149 let type = try container.decodeIfPresent(String.self, forKey: .type)
150 let typename = try container.decodeIfPresent(String.self, forKey: .typename)
151
152 switch type ?? typename {
153 case "TREE", "Tree":
154 let tree = GitTree(
155 id: try container.decodeIfPresent(String.self, forKey: .id),
156 shortId: try container.decodeIfPresent(String.self, forKey: .shortId),
157 entries: try container.decodeIfPresent(GitTreeEntryPage.self, forKey: .entries)
158 )
159 self = .tree(tree)
160
161 case "BLOB", "TextBlob", "BinaryBlob":
162 if typename == "TextBlob" || container.contains(.text) {
163 let blob = GitTextBlob(
164 id: try container.decodeIfPresent(String.self, forKey: .id),
165 shortId: try container.decodeIfPresent(String.self, forKey: .shortId),
166 text: try container.decodeIfPresent(String.self, forKey: .text),
167 size: try container.decodeIfPresent(Int.self, forKey: .size)
168 )
169 self = .textBlob(blob)
170 } else {
171 let blob = GitBinaryBlob(
172 id: try container.decodeIfPresent(String.self, forKey: .id),
173 shortId: try container.decodeIfPresent(String.self, forKey: .shortId),
174 size: try container.decodeIfPresent(Int.self, forKey: .size),
175 content: try container.decodeIfPresent(String.self, forKey: .content)
176 )
177 self = .binaryBlob(blob)
178 }
179
180 default:
181 self = .unknown
182 }
183 }
184
185 func encode(to encoder: any Encoder) throws {
186 var container = encoder.container(keyedBy: CodingKeys.self)
187 switch self {
188 case .tree(let tree):
189 try container.encode("TREE", forKey: .type)
190 try container.encodeIfPresent(tree.id, forKey: .id)
191 try container.encodeIfPresent(tree.shortId, forKey: .shortId)
192 try container.encodeIfPresent(tree.entries, forKey: .entries)
193 case .textBlob(let blob):
194 try container.encode("BLOB", forKey: .type)
195 try container.encode("TextBlob", forKey: .typename)
196 try container.encodeIfPresent(blob.id, forKey: .id)
197 try container.encodeIfPresent(blob.shortId, forKey: .shortId)
198 try container.encodeIfPresent(blob.text, forKey: .text)
199 try container.encodeIfPresent(blob.size, forKey: .size)
200 case .binaryBlob(let blob):
201 try container.encode("BLOB", forKey: .type)
202 try container.encode("BinaryBlob", forKey: .typename)
203 try container.encodeIfPresent(blob.id, forKey: .id)
204 try container.encodeIfPresent(blob.shortId, forKey: .shortId)
205 try container.encodeIfPresent(blob.size, forKey: .size)
206 try container.encodeIfPresent(blob.content, forKey: .content)
207 case .unknown:
208 break
209 }
210 }
211}
212
213/// Convenience helpers for checking object type.
214nonisolated extension GitObject {
215 var isTree: Bool {
216 if case .tree = self { return true }
217 return false
218 }
219
220 var treeId: String? {
221 switch self {
222 case .tree(let t): t.id
223 case .textBlob(let b): b.id
224 case .binaryBlob(let b): b.id
225 case .unknown: nil
226 }
227 }
228}
229
230// MARK: - Tag
231
232/// An annotated Git tag from git.sr.ht.
233nonisolated struct Tag: Codable, Sendable, Identifiable {
234 let id: String
235 let shortId: String
236 let name: String
237 let message: String?
238 let tagger: Signature?
239}
240
241// MARK: - Artifact
242
243/// A release artifact attached to a Git tag.
244nonisolated struct Artifact: Codable, Sendable, Identifiable {
245 let id: Int
246 let created: Date
247 let filename: String
248 let checksum: String
249 let size: Int
250 let url: URL
251}