Commit cb8fedaefe
Unsigned
Layout: unified · split
Hutch/Models/Git.swift +11 −8
| @@ -113,7 +113,7 @@ struct GitTree: Codable, Sendable { | |||
| 113 | struct GitTextBlob: Codable, Sendable { | 113 | struct GitTextBlob: Codable, Sendable { |
| 114 | let id: String? | 114 | let id: String? |
| 115 | let shortId: String? | 115 | let shortId: String? |
| 116 | let text: String | 116 | let text: String? |
| 117 | let size: Int? | 117 | let size: Int? |
| 118 | } | 118 | } |
| 119 | 119 | ||
| @@ -134,14 +134,16 @@ struct GitTreeEntryPage: Codable, Sendable { | |||
| 134 | extension GitObject: Codable { | 134 | extension GitObject: Codable { |
| 135 | private enum CodingKeys: String, CodingKey { | 135 | private enum CodingKeys: String, CodingKey { |
| 136 | case type, id, shortId, entries, text, size, content | 136 | case type, id, shortId, entries, text, size, content |
| 137 | case typename = "__typename" | ||
| 137 | } | 138 | } |
| 138 | 139 | ||
| 139 | init(from decoder: any Decoder) throws { | 140 | init(from decoder: any Decoder) throws { |
| 140 | let container = try decoder.container(keyedBy: CodingKeys.self) | 141 | let container = try decoder.container(keyedBy: CodingKeys.self) |
| 141 | let type = try container.decodeIfPresent(String.self, forKey: .type) | 142 | let type = try container.decodeIfPresent(String.self, forKey: .type) |
| 143 | let typename = try container.decodeIfPresent(String.self, forKey: .typename) | ||
| 142 | 144 | ||
| 143 | switch type { | 145 | switch type ?? typename { |
| 144 | case "TREE": | 146 | case "TREE", "Tree": |
| 145 | let tree = GitTree( | 147 | let tree = GitTree( |
| 146 | id: try container.decodeIfPresent(String.self, forKey: .id), | 148 | id: try container.decodeIfPresent(String.self, forKey: .id), |
| 147 | shortId: try container.decodeIfPresent(String.self, forKey: .shortId), | 149 | shortId: try container.decodeIfPresent(String.self, forKey: .shortId), |
| @@ -149,13 +151,12 @@ extension GitObject: Codable { | |||
| 149 | ) | 151 | ) |
| 150 | self = .tree(tree) | 152 | self = .tree(tree) |
| 151 | 153 | ||
| 152 | case "BLOB": | 154 | case "BLOB", "TextBlob", "BinaryBlob": |
| 153 | // TextBlob has a "text" key; BinaryBlob does not | 155 | if typename == "TextBlob" || container.contains(.text) { |
| 154 | if container.contains(.text) { | ||
| 155 | let blob = GitTextBlob( | 156 | let blob = GitTextBlob( |
| 156 | id: try container.decodeIfPresent(String.self, forKey: .id), | 157 | id: try container.decodeIfPresent(String.self, forKey: .id), |
| 157 | shortId: try container.decodeIfPresent(String.self, forKey: .shortId), | 158 | 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), |
| 159 | size: try container.decodeIfPresent(Int.self, forKey: .size) | 160 | size: try container.decodeIfPresent(Int.self, forKey: .size) |
| 160 | ) | 161 | ) |
| 161 | self = .textBlob(blob) | 162 | self = .textBlob(blob) |
| @@ -184,12 +185,14 @@ extension GitObject: Codable { | |||
| 184 | try container.encodeIfPresent(tree.entries, forKey: .entries) | 185 | try container.encodeIfPresent(tree.entries, forKey: .entries) |
| 185 | case .textBlob(let blob): | 186 | case .textBlob(let blob): |
| 186 | try container.encode("BLOB", forKey: .type) | 187 | try container.encode("BLOB", forKey: .type) |
| 188 | try container.encode("TextBlob", forKey: .typename) | ||
| 187 | try container.encodeIfPresent(blob.id, forKey: .id) | 189 | try container.encodeIfPresent(blob.id, forKey: .id) |
| 188 | try container.encodeIfPresent(blob.shortId, forKey: .shortId) | 190 | try container.encodeIfPresent(blob.shortId, forKey: .shortId) |
| 189 | try container.encode(blob.text, forKey: .text) | 191 | try container.encodeIfPresent(blob.text, forKey: .text) |
| 190 | try container.encodeIfPresent(blob.size, forKey: .size) | 192 | try container.encodeIfPresent(blob.size, forKey: .size) |
| 191 | case .binaryBlob(let blob): | 193 | case .binaryBlob(let blob): |
| 192 | try container.encode("BLOB", forKey: .type) | 194 | try container.encode("BLOB", forKey: .type) |
| 195 | try container.encode("BinaryBlob", forKey: .typename) | ||
| 193 | try container.encodeIfPresent(blob.id, forKey: .id) | 196 | try container.encodeIfPresent(blob.id, forKey: .id) |
| 194 | try container.encodeIfPresent(blob.shortId, forKey: .shortId) | 197 | try container.encodeIfPresent(blob.shortId, forKey: .shortId) |
| 195 | try container.encodeIfPresent(blob.size, forKey: .size) | 198 | try container.encodeIfPresent(blob.size, forKey: .size) |
Hutch/Views/Repositories/FileTreeView.swift +1 −1
| @@ -235,7 +235,7 @@ private struct FileTreeContentView: View { | |||
| 235 | 235 | ||
| 236 | GeometryReader { geometry in | 236 | GeometryReader { geometry in |
| 237 | ScrollView([.vertical, .horizontal]) { | 237 | ScrollView([.vertical, .horizontal]) { |
| 238 | Text(blob.text) | 238 | Text(blob.text ?? "") |
| 239 | .font(.system(.body, design: .monospaced)) | 239 | .font(.system(.body, design: .monospaced)) |
| 240 | .multilineTextAlignment(.leading) | 240 | .multilineTextAlignment(.leading) |
| 241 | .fixedSize(horizontal: true, vertical: false) | 241 | .fixedSize(horizontal: true, vertical: false) |
Hutch/Views/Repositories/FileTreeViewModel.swift +14 −10
| @@ -96,6 +96,7 @@ final class FileTreeViewModel { | |||
| 96 | mode | 96 | mode |
| 97 | object { | 97 | object { |
| 98 | type | 98 | type |
| 99 | __typename | ||
| 99 | id | 100 | id |
| 100 | shortId | 101 | shortId |
| 101 | ... on Tree { | 102 | ... on Tree { |
| @@ -110,12 +111,10 @@ final class FileTreeViewModel { | |||
| 110 | } | 111 | } |
| 111 | } | 112 | } |
| 112 | ... on TextBlob { | 113 | ... on TextBlob { |
| 113 | text | ||
| 114 | size | 114 | size |
| 115 | } | 115 | } |
| 116 | ... on BinaryBlob { | 116 | ... on BinaryBlob { |
| 117 | size | 117 | size |
| 118 | content | ||
| 119 | } | 118 | } |
| 120 | } | 119 | } |
| 121 | } | 120 | } |
| @@ -142,6 +141,7 @@ final class FileTreeViewModel { | |||
| 142 | mode | 141 | mode |
| 143 | object { | 142 | object { |
| 144 | type | 143 | type |
| 144 | __typename | ||
| 145 | id | 145 | id |
| 146 | shortId | 146 | shortId |
| 147 | ... on Tree { | 147 | ... on Tree { |
| @@ -156,12 +156,10 @@ final class FileTreeViewModel { | |||
| 156 | } | 156 | } |
| 157 | } | 157 | } |
| 158 | ... on TextBlob { | 158 | ... on TextBlob { |
| 159 | text | ||
| 160 | size | 159 | size |
| 161 | } | 160 | } |
| 162 | ... on BinaryBlob { | 161 | ... on BinaryBlob { |
| 163 | size | 162 | size |
| 164 | content | ||
| 165 | } | 163 | } |
| 166 | } | 164 | } |
| 167 | } | 165 | } |
| @@ -187,6 +185,7 @@ final class FileTreeViewModel { | |||
| 187 | mode | 185 | mode |
| 188 | object { | 186 | object { |
| 189 | type | 187 | type |
| 188 | __typename | ||
| 190 | id | 189 | id |
| 191 | shortId | 190 | shortId |
| 192 | ... on Tree { | 191 | ... on Tree { |
| @@ -201,12 +200,10 @@ final class FileTreeViewModel { | |||
| 201 | } | 200 | } |
| 202 | } | 201 | } |
| 203 | ... on TextBlob { | 202 | ... on TextBlob { |
| 204 | text | ||
| 205 | size | 203 | size |
| 206 | } | 204 | } |
| 207 | ... on BinaryBlob { | 205 | ... on BinaryBlob { |
| 208 | size | 206 | size |
| 209 | content | ||
| 210 | } | 207 | } |
| 211 | } | 208 | } |
| 212 | } | 209 | } |
| @@ -321,16 +318,23 @@ final class FileTreeViewModel { | |||
| 321 | // Otherwise fetch the subtree (handles pagination) | 318 | // Otherwise fetch the subtree (handles pagination) |
| 322 | await loadSubtree(name: entry.name, treeId: objectSHA) | 319 | await loadSubtree(name: entry.name, treeId: objectSHA) |
| 323 | 320 | ||
| 324 | case .textBlob: | 321 | case .textBlob(let blob): |
| 325 | viewingEntry = entry | 322 | if blob.text != nil { |
| 326 | viewingObject = object | 323 | viewingEntry = entry |
| 324 | viewingObject = object | ||
| 325 | } else if let blobId = blob.id { | ||
| 326 | await loadBlob(entry: entry, blobId: blobId) | ||
| 327 | } | ||
| 327 | 328 | ||
| 328 | case .binaryBlob(let blob): | 329 | case .binaryBlob(let blob): |
| 329 | if blob.content != nil || blob.size != nil { | 330 | if blob.content != nil { |
| 330 | viewingEntry = entry | 331 | viewingEntry = entry |
| 331 | viewingObject = object | 332 | viewingObject = object |
| 332 | } else if let blobId = blob.id { | 333 | } else if let blobId = blob.id { |
| 333 | await loadBlob(entry: entry, blobId: blobId) | 334 | await loadBlob(entry: entry, blobId: blobId) |
| 335 | } else { | ||
| 336 | viewingEntry = entry | ||
| 337 | viewingObject = object | ||
| 334 | } | 338 | } |
| 335 | 339 | ||
| 336 | case .unknown: | 340 | case .unknown: |
HutchTests/GitObjectTests.swift added +40
| @@ -0,0 +1,40 @@ | |||
| 1 | import Foundation | ||
| 2 | import Testing | ||
| 3 | @testable import Hutch | ||
| 4 | |||
| 5 | struct 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 | } | ||