Commit cb8fedaefe

cb8fedaefe6562f5281e3bee8d0ed75e9a506576

parent: 9656d31eb5

Unsigned

cmc <hello@cleberg.net> · 2026-03-19 00:28 UTC

lazily load file blobs from repository tree views

Layout: unified · split

Hutch/Models/Git.swift +11 −8
@@ -113,7 +113,7 @@ struct GitTree: Codable, Sendable {
113113struct GitTextBlob: Codable, Sendable {
114114 let id: String?
115115 let shortId: String?
116 let text: String
116 let text: String?
117117 let size: Int?
118118}
119119
@@ -134,14 +134,16 @@ struct GitTreeEntryPage: Codable, Sendable {
134134extension GitObject: Codable {
135135 private enum CodingKeys: String, CodingKey {
136136 case type, id, shortId, entries, text, size, content
137 case typename = "__typename"
137138 }
138139
139140 init(from decoder: any Decoder) throws {
140141 let container = try decoder.container(keyedBy: CodingKeys.self)
141142 let type = try container.decodeIfPresent(String.self, forKey: .type)
143 let typename = try container.decodeIfPresent(String.self, forKey: .typename)
142144
143 switch type {
144 case "TREE":
145 switch type ?? typename {
146 case "TREE", "Tree":
145147 let tree = GitTree(
146148 id: try container.decodeIfPresent(String.self, forKey: .id),
147149 shortId: try container.decodeIfPresent(String.self, forKey: .shortId),
@@ -149,13 +151,12 @@ extension GitObject: Codable {
149151 )
150152 self = .tree(tree)
151153
152 case "BLOB":
153 // TextBlob has a "text" key; BinaryBlob does not
154 if container.contains(.text) {
154 case "BLOB", "TextBlob", "BinaryBlob":
155 if typename == "TextBlob" || container.contains(.text) {
155156 let blob = GitTextBlob(
156157 id: try container.decodeIfPresent(String.self, forKey: .id),
157158 shortId: try container.decodeIfPresent(String.self, forKey: .shortId),
158 text: try container.decode(String.self, forKey: .text),
159 text: try container.decodeIfPresent(String.self, forKey: .text),
159160 size: try container.decodeIfPresent(Int.self, forKey: .size)
160161 )
161162 self = .textBlob(blob)
@@ -184,12 +185,14 @@ extension GitObject: Codable {
184185 try container.encodeIfPresent(tree.entries, forKey: .entries)
185186 case .textBlob(let blob):
186187 try container.encode("BLOB", forKey: .type)
188 try container.encode("TextBlob", forKey: .typename)
187189 try container.encodeIfPresent(blob.id, forKey: .id)
188190 try container.encodeIfPresent(blob.shortId, forKey: .shortId)
189 try container.encode(blob.text, forKey: .text)
191 try container.encodeIfPresent(blob.text, forKey: .text)
190192 try container.encodeIfPresent(blob.size, forKey: .size)
191193 case .binaryBlob(let blob):
192194 try container.encode("BLOB", forKey: .type)
195 try container.encode("BinaryBlob", forKey: .typename)
193196 try container.encodeIfPresent(blob.id, forKey: .id)
194197 try container.encodeIfPresent(blob.shortId, forKey: .shortId)
195198 try container.encodeIfPresent(blob.size, forKey: .size)
Hutch/Views/Repositories/FileTreeView.swift +1 −1
@@ -235,7 +235,7 @@ private struct FileTreeContentView: View {
235235
236236 GeometryReader { geometry in
237237 ScrollView([.vertical, .horizontal]) {
238 Text(blob.text)
238 Text(blob.text ?? "")
239239 .font(.system(.body, design: .monospaced))
240240 .multilineTextAlignment(.leading)
241241 .fixedSize(horizontal: true, vertical: false)
Hutch/Views/Repositories/FileTreeViewModel.swift +14 −10
@@ -96,6 +96,7 @@ final class FileTreeViewModel {
9696 mode
9797 object {
9898 type
99 __typename
99100 id
100101 shortId
101102 ... on Tree {
@@ -110,12 +111,10 @@ final class FileTreeViewModel {
110111 }
111112 }
112113 ... on TextBlob {
113 text
114114 size
115115 }
116116 ... on BinaryBlob {
117117 size
118 content
119118 }
120119 }
121120 }
@@ -142,6 +141,7 @@ final class FileTreeViewModel {
142141 mode
143142 object {
144143 type
144 __typename
145145 id
146146 shortId
147147 ... on Tree {
@@ -156,12 +156,10 @@ final class FileTreeViewModel {
156156 }
157157 }
158158 ... on TextBlob {
159 text
160159 size
161160 }
162161 ... on BinaryBlob {
163162 size
164 content
165163 }
166164 }
167165 }
@@ -187,6 +185,7 @@ final class FileTreeViewModel {
187185 mode
188186 object {
189187 type
188 __typename
190189 id
191190 shortId
192191 ... on Tree {
@@ -201,12 +200,10 @@ final class FileTreeViewModel {
201200 }
202201 }
203202 ... on TextBlob {
204 text
205203 size
206204 }
207205 ... on BinaryBlob {
208206 size
209 content
210207 }
211208 }
212209 }
@@ -321,16 +318,23 @@ final class FileTreeViewModel {
321318 // Otherwise fetch the subtree (handles pagination)
322319 await loadSubtree(name: entry.name, treeId: objectSHA)
323320
324 case .textBlob:
325 viewingEntry = entry
326 viewingObject = object
321 case .textBlob(let blob):
322 if blob.text != nil {
323 viewingEntry = entry
324 viewingObject = object
325 } else if let blobId = blob.id {
326 await loadBlob(entry: entry, blobId: blobId)
327 }
327328
328329 case .binaryBlob(let blob):
329 if blob.content != nil || blob.size != nil {
330 if blob.content != nil {
330331 viewingEntry = entry
331332 viewingObject = object
332333 } else if let blobId = blob.id {
333334 await loadBlob(entry: entry, blobId: blobId)
335 } else {
336 viewingEntry = entry
337 viewingObject = object
334338 }
335339
336340 case .unknown:
HutchTests/GitObjectTests.swift added +40
@@ -0,0 +1,40 @@
1import Foundation
2import Testing
3@testable import Hutch
4
5struct GitObjectTests {
6
7 @Test
8 @MainActor
9 func decodesMetadataOnlyTextBlobUsingTypename() throws {
10 let data = Data(#"{"type":"BLOB","__typename":"TextBlob","id":"blob123","shortId":"blob123","size":42}"#.utf8)
11
12 let blob = try JSONDecoder().decode(GitObject.self, from: data)
13
14 guard case .textBlob(let textBlob) = blob else {
15 Issue.record("Expected metadata-only blob to decode as text blob.")
16 return
17 }
18
19 #expect(textBlob.id == "blob123")
20 #expect(textBlob.size == 42)
21 #expect(textBlob.text == nil)
22 }
23
24 @Test
25 @MainActor
26 func decodesMetadataOnlyBinaryBlobUsingTypename() throws {
27 let data = Data(#"{"type":"BLOB","__typename":"BinaryBlob","id":"blob456","shortId":"blob456","size":64}"#.utf8)
28
29 let blob = try JSONDecoder().decode(GitObject.self, from: data)
30
31 guard case .binaryBlob(let binaryBlob) = blob else {
32 Issue.record("Expected metadata-only blob to decode as binary blob.")
33 return
34 }
35
36 #expect(binaryBlob.id == "blob456")
37 #expect(binaryBlob.size == 64)
38 #expect(binaryBlob.content == nil)
39 }
40}