krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

v2.4.1: Hutch/Models/Git.swift · raw

  1import Foundation
  2
  3// MARK: - Enums
  4
  5/// Repository visibility level.
  6enum Visibility: String, Codable, Sendable {
  7    case `public` = "PUBLIC"
  8    case unlisted = "UNLISTED"
  9    case `private` = "PRIVATE"
 10}
 11
 12/// Repository access mode.
 13enum AccessMode: String, Codable, Sendable {
 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).
 22struct Entity: Codable, Sendable, Hashable {
 23    let canonicalName: String
 24}
 25
 26// MARK: - Repository
 27
 28/// A git repository from git.sr.ht.
 29struct 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    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).
 50struct 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").
 59struct 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.
 67struct 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).
 80struct Reference: Codable, Sendable, Hashable {
 81    let name: String
 82    let target: String?
 83}
 84
 85// MARK: - TreeEntry
 86
 87/// An entry in a Git tree (file or directory).
 88struct TreeEntry: Codable, Sendable, Identifiable {
 89    let id: String
 90    let name: String
 91    let mode: Int?
 92    let object: GitObject?
 93}
 94
 95/// A Git object returned by the git.sr.ht GraphQL API.
 96/// The API returns `type` as "TREE", "BLOB", "COMMIT", or "TAG".
 97/// Both TextBlob and BinaryBlob have type == "BLOB"; they are
 98/// distinguished by the presence of the "text" key (TextBlob) vs
 99/// the "content" key (BinaryBlob).
100enum GitObject: Sendable {
101    case tree(GitTree)
102    case textBlob(GitTextBlob)
103    case binaryBlob(GitBinaryBlob)
104    case unknown
105}
106
107struct GitTree: Codable, Sendable {
108    let id: String?
109    let shortId: String?
110    let entries: GitTreeEntryPage?
111}
112
113struct GitTextBlob: Codable, Sendable {
114    let id: String?
115    let shortId: String?
116    let text: String?
117    let size: Int?
118}
119
120struct GitBinaryBlob: Codable, Sendable {
121    let id: String?
122    let shortId: String?
123    let size: Int?
124    let content: String?
125}
126
127struct GitTreeEntryPage: Codable, Sendable {
128    let results: [TreeEntry]
129    let cursor: String?
130}
131
132// MARK: - GitObject Codable
133
134extension GitObject: Codable {
135    private enum CodingKeys: String, CodingKey {
136        case type, id, shortId, entries, text, size, content
137        case typename = "__typename"
138    }
139
140    init(from decoder: any Decoder) throws {
141        let container = try decoder.container(keyedBy: CodingKeys.self)
142        let type = try container.decodeIfPresent(String.self, forKey: .type)
143        let typename = try container.decodeIfPresent(String.self, forKey: .typename)
144
145        switch type ?? typename {
146        case "TREE", "Tree":
147            let tree = GitTree(
148                id: try container.decodeIfPresent(String.self, forKey: .id),
149                shortId: try container.decodeIfPresent(String.self, forKey: .shortId),
150                entries: try container.decodeIfPresent(GitTreeEntryPage.self, forKey: .entries)
151            )
152            self = .tree(tree)
153
154        case "BLOB", "TextBlob", "BinaryBlob":
155            if typename == "TextBlob" || container.contains(.text) {
156                let blob = GitTextBlob(
157                    id: try container.decodeIfPresent(String.self, forKey: .id),
158                    shortId: try container.decodeIfPresent(String.self, forKey: .shortId),
159                    text: try container.decodeIfPresent(String.self, forKey: .text),
160                    size: try container.decodeIfPresent(Int.self, forKey: .size)
161                )
162                self = .textBlob(blob)
163            } else {
164                let blob = GitBinaryBlob(
165                    id: try container.decodeIfPresent(String.self, forKey: .id),
166                    shortId: try container.decodeIfPresent(String.self, forKey: .shortId),
167                    size: try container.decodeIfPresent(Int.self, forKey: .size),
168                    content: try container.decodeIfPresent(String.self, forKey: .content)
169                )
170                self = .binaryBlob(blob)
171            }
172
173        default:
174            self = .unknown
175        }
176    }
177
178    func encode(to encoder: any Encoder) throws {
179        var container = encoder.container(keyedBy: CodingKeys.self)
180        switch self {
181        case .tree(let tree):
182            try container.encode("TREE", forKey: .type)
183            try container.encodeIfPresent(tree.id, forKey: .id)
184            try container.encodeIfPresent(tree.shortId, forKey: .shortId)
185            try container.encodeIfPresent(tree.entries, forKey: .entries)
186        case .textBlob(let blob):
187            try container.encode("BLOB", forKey: .type)
188            try container.encode("TextBlob", forKey: .typename)
189            try container.encodeIfPresent(blob.id, forKey: .id)
190            try container.encodeIfPresent(blob.shortId, forKey: .shortId)
191            try container.encodeIfPresent(blob.text, forKey: .text)
192            try container.encodeIfPresent(blob.size, forKey: .size)
193        case .binaryBlob(let blob):
194            try container.encode("BLOB", forKey: .type)
195            try container.encode("BinaryBlob", forKey: .typename)
196            try container.encodeIfPresent(blob.id, forKey: .id)
197            try container.encodeIfPresent(blob.shortId, forKey: .shortId)
198            try container.encodeIfPresent(blob.size, forKey: .size)
199            try container.encodeIfPresent(blob.content, forKey: .content)
200        case .unknown:
201            break
202        }
203    }
204}
205
206/// Convenience helpers for checking object type.
207extension GitObject {
208    var isTree: Bool {
209        if case .tree = self { return true }
210        return false
211    }
212
213    var treeId: String? {
214        switch self {
215        case .tree(let t): t.id
216        case .textBlob(let b): b.id
217        case .binaryBlob(let b): b.id
218        case .unknown: nil
219        }
220    }
221}
222
223// MARK: - Tag
224
225/// An annotated Git tag from git.sr.ht.
226struct Tag: Codable, Sendable, Identifiable {
227    let id: String
228    let shortId: String
229    let name: String
230    let message: String?
231    let tagger: Signature?
232}
233
234// MARK: - Artifact
235
236/// A release artifact attached to a Git tag.
237struct Artifact: Codable, Sendable, Identifiable {
238    let id: Int
239    let created: Date
240    let filename: String
241    let checksum: String
242    let size: Int
243    let url: URL
244}