Commit 1c32074b4f

1c32074b4f3bdd992fd40a65301ed3f821d0cebb

parent: e137533ecf

Unsigned

cmc <hello@cleberg.net> · 2026-04-20 17:51 UTC

fix: ensure all branches and tags are accounted for and not cutoff due to pagination

Layout: unified · split

Hutch/Views/Repositories/ReferencesListView.swift +63 −8
@@ -5,21 +5,39 @@ struct ReferencesListView: View {
55
66 var body: some View {
77 List {
8 if !viewModel.branches.isEmpty {
9 Section("Branches") {
10 ForEach(viewModel.branches, id: \.name) { ref in
11 ReferenceRow(reference: ref, prefix: "refs/heads/")
8 if let defaultBranch = viewModel.defaultBranchReference {
9 Section {
10 ReferenceRow(reference: defaultBranch, prefix: "refs/heads/")
11 .themedRow()
12
13 NavigationLink("See All Branches") {
14 ReferencesDetailListView(
15 title: "Branches",
16 references: viewModel.branches,
17 prefix: "refs/heads/"
18 )
1219 }
1320 .themedRow()
21 } header: {
22 sectionHeader(title: "Default Branch")
1423 }
1524 }
1625
17 if !viewModel.tags.isEmpty {
18 Section("Tags") {
19 ForEach(viewModel.tags, id: \.name) { ref in
20 ReferenceRow(reference: ref, prefix: "refs/tags/")
26 if let latestTag = viewModel.latestTagReference {
27 Section {
28 ReferenceRow(reference: latestTag, prefix: "refs/tags/")
29 .themedRow()
30
31 NavigationLink("See All Tags") {
32 ReferencesDetailListView(
33 title: "Tags",
34 references: viewModel.tags,
35 prefix: "refs/tags/"
36 )
2137 }
2238 .themedRow()
39 } header: {
40 sectionHeader(title: "Latest Tag")
2341 }
2442 }
2543 }
@@ -51,6 +69,41 @@ struct ReferencesListView: View {
5169 await viewModel.loadReferences()
5270 }
5371 }
72
73 @ViewBuilder
74 private func sectionHeader(title: String) -> some View {
75 HStack {
76 Text(title)
77 Spacer()
78 }
79 }
80}
81
82private struct ReferencesDetailListView: View {
83 let title: String
84 let references: [ReferenceDetail]
85 let prefix: String
86
87 var body: some View {
88 List {
89 ForEach(references, id: \.name) { reference in
90 ReferenceRow(reference: reference, prefix: prefix)
91 .themedRow()
92 }
93 }
94 .themedList()
95 .listStyle(.insetGrouped)
96 .navigationTitle(title)
97 .overlay {
98 if references.isEmpty {
99 ContentUnavailableView(
100 "No \(title)",
101 systemImage: prefix.contains("tags") ? "tag" : "arrow.triangle.branch",
102 description: Text("This repository does not have any \(title.lowercased()).")
103 )
104 }
105 }
106 }
54107}
55108
56109private struct ReferenceRow: View {
@@ -80,6 +133,8 @@ private struct ReferenceRow: View {
80133 .font(.caption.monospaced())
81134 .foregroundStyle(.secondary)
82135 }
136 .frame(maxWidth: .infinity, alignment: .leading)
137 .contentShape(Rectangle())
83138 }
84139
85140 private var shortName: String {
Hutch/Views/Repositories/RepositoryDetailViewModel.swift +69 −11
@@ -240,9 +240,9 @@ final class RepositoryDetailViewModel {
240240 // MARK: - References
241241
242242 private static let refsQuery = """
243 query refs($rid: ID!) {
243 query refs($rid: ID!, $cursor: Cursor) {
244244 repository(rid: $rid) {
245 references {
245 references(cursor: $cursor) {
246246 results {
247247 name
248248 target
@@ -263,15 +263,15 @@ final class RepositoryDetailViewModel {
263263 error = nil
264264
265265 do {
266 let result = try await client.execute(
267 service: service,
268 query: Self.refsQuery,
269 variables: ["rid": repository.rid],
270 responseType: RefsResponse.self
271 )
272 let allRefs = result.repository?.references.results ?? []
273 branches = allRefs.filter { $0.name.hasPrefix("refs/heads/") }.map { $0.toDetail() }
274 tags = allRefs.filter { $0.name.hasPrefix("refs/tags/") }.map { $0.toDetail() }
266 let allRefs = try await fetchAllReferences()
267 branches = allRefs
268 .filter { $0.name.hasPrefix("refs/heads/") }
269 .map { $0.toDetail() }
270 .sorted(by: Self.sortBranches)
271 tags = allRefs
272 .filter { $0.name.hasPrefix("refs/tags/") }
273 .map { $0.toDetail() }
274 .sorted(by: Self.sortTags)
275275 } catch {
276276 if isEmptyRepositoryError(error) {
277277 branches = []
@@ -284,6 +284,64 @@ final class RepositoryDetailViewModel {
284284 isLoadingRefs = false
285285 }
286286
287 var defaultBranchReference: ReferenceDetail? {
288 if let headName = repository.head?.name,
289 let branch = branches.first(where: { $0.name == headName }) {
290 return branch
291 }
292 return branches.first
293 }
294
295 var latestTagReference: ReferenceDetail? {
296 tags.first
297 }
298
299 private func fetchAllReferences() async throws -> [ReferencePayload] {
300 var allRefs: [ReferencePayload] = []
301 var cursor: String?
302
303 repeat {
304 let page = try await fetchReferencePage(cursor: cursor)
305 allRefs.append(contentsOf: page.results)
306 cursor = page.cursor
307 } while cursor != nil
308
309 return allRefs
310 }
311
312 private func fetchReferencePage(cursor: String?) async throws -> RefsPage {
313 var variables: [String: any Sendable] = ["rid": repository.rid]
314 if let cursor {
315 variables["cursor"] = cursor
316 }
317
318 let result = try await client.execute(
319 service: service,
320 query: Self.refsQuery,
321 variables: variables,
322 responseType: RefsResponse.self
323 )
324
325 return result.repository?.references ?? RefsPage(results: [], cursor: nil)
326 }
327
328 nonisolated private static func sortBranches(_ lhs: ReferenceDetail, _ rhs: ReferenceDetail) -> Bool {
329 lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending
330 }
331
332 nonisolated private static func sortTags(_ lhs: ReferenceDetail, _ rhs: ReferenceDetail) -> Bool {
333 switch (lhs.date, rhs.date) {
334 case let (lhsDate?, rhsDate?) where lhsDate != rhsDate:
335 return lhsDate > rhsDate
336 case (.some, nil):
337 return true
338 case (nil, .some):
339 return false
340 default:
341 return lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending
342 }
343 }
344
287345 // MARK: - README
288346
289347 private static let readmeQuery = """