krz/hutch

an ios client for sourcehut

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

d29fbef60abdffaee190b758530dde9212745ed2

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2026-04-20T17:51:22Z

fix: ensure all branches and tags are accounted for and not cutoff due to pagination
 Hutch/Views/Repositories/ReferencesListView.swift  | 71 ++++++++++++++++---
 .../Repositories/RepositoryDetailViewModel.swift   | 80 +++++++++++++++++++---
 2 files changed, 132 insertions(+), 19 deletions(-)

diff --git a/Hutch/Views/Repositories/ReferencesListView.swift b/Hutch/Views/Repositories/ReferencesListView.swift
index 0e3be0a..a2eed11 100644
--- a/Hutch/Views/Repositories/ReferencesListView.swift
+++ b/Hutch/Views/Repositories/ReferencesListView.swift
@@ -5,21 +5,39 @@ struct ReferencesListView: View {
 
     var body: some View {
         List {
-            if !viewModel.branches.isEmpty {
-                Section("Branches") {
-                    ForEach(viewModel.branches, id: \.name) { ref in
-                        ReferenceRow(reference: ref, prefix: "refs/heads/")
+            if let defaultBranch = viewModel.defaultBranchReference {
+                Section {
+                    ReferenceRow(reference: defaultBranch, prefix: "refs/heads/")
+                        .themedRow()
+
+                    NavigationLink("See All Branches") {
+                        ReferencesDetailListView(
+                            title: "Branches",
+                            references: viewModel.branches,
+                            prefix: "refs/heads/"
+                        )
                     }
                     .themedRow()
+                } header: {
+                    sectionHeader(title: "Default Branch")
                 }
             }
 
-            if !viewModel.tags.isEmpty {
-                Section("Tags") {
-                    ForEach(viewModel.tags, id: \.name) { ref in
-                        ReferenceRow(reference: ref, prefix: "refs/tags/")
+            if let latestTag = viewModel.latestTagReference {
+                Section {
+                    ReferenceRow(reference: latestTag, prefix: "refs/tags/")
+                        .themedRow()
+
+                    NavigationLink("See All Tags") {
+                        ReferencesDetailListView(
+                            title: "Tags",
+                            references: viewModel.tags,
+                            prefix: "refs/tags/"
+                        )
                     }
                     .themedRow()
+                } header: {
+                    sectionHeader(title: "Latest Tag")
                 }
             }
         }
@@ -51,6 +69,41 @@ struct ReferencesListView: View {
             await viewModel.loadReferences()
         }
     }
+
+    @ViewBuilder
+    private func sectionHeader(title: String) -> some View {
+        HStack {
+            Text(title)
+            Spacer()
+        }
+    }
+}
+
+private struct ReferencesDetailListView: View {
+    let title: String
+    let references: [ReferenceDetail]
+    let prefix: String
+
+    var body: some View {
+        List {
+            ForEach(references, id: \.name) { reference in
+                ReferenceRow(reference: reference, prefix: prefix)
+                    .themedRow()
+            }
+        }
+        .themedList()
+        .listStyle(.insetGrouped)
+        .navigationTitle(title)
+        .overlay {
+            if references.isEmpty {
+                ContentUnavailableView(
+                    "No \(title)",
+                    systemImage: prefix.contains("tags") ? "tag" : "arrow.triangle.branch",
+                    description: Text("This repository does not have any \(title.lowercased()).")
+                )
+            }
+        }
+    }
 }
 
 private struct ReferenceRow: View {
@@ -80,6 +133,8 @@ private struct ReferenceRow: View {
                 .font(.caption.monospaced())
                 .foregroundStyle(.secondary)
         }
+        .frame(maxWidth: .infinity, alignment: .leading)
+        .contentShape(Rectangle())
     }
 
     private var shortName: String {
diff --git a/Hutch/Views/Repositories/RepositoryDetailViewModel.swift b/Hutch/Views/Repositories/RepositoryDetailViewModel.swift
index 8dec51b..9aff1a4 100644
--- a/Hutch/Views/Repositories/RepositoryDetailViewModel.swift
+++ b/Hutch/Views/Repositories/RepositoryDetailViewModel.swift
@@ -240,9 +240,9 @@ final class RepositoryDetailViewModel {
     // MARK: - References
 
     private static let refsQuery = """
-    query refs($rid: ID!) {
+    query refs($rid: ID!, $cursor: Cursor) {
         repository(rid: $rid) {
-            references {
+            references(cursor: $cursor) {
                 results {
                     name
                     target
@@ -263,15 +263,15 @@ final class RepositoryDetailViewModel {
         error = nil
 
         do {
-            let result = try await client.execute(
-                service: service,
-                query: Self.refsQuery,
-                variables: ["rid": repository.rid],
-                responseType: RefsResponse.self
-            )
-            let allRefs = result.repository?.references.results ?? []
-            branches = allRefs.filter { $0.name.hasPrefix("refs/heads/") }.map { $0.toDetail() }
-            tags = allRefs.filter { $0.name.hasPrefix("refs/tags/") }.map { $0.toDetail() }
+            let allRefs = try await fetchAllReferences()
+            branches = allRefs
+                .filter { $0.name.hasPrefix("refs/heads/") }
+                .map { $0.toDetail() }
+                .sorted(by: Self.sortBranches)
+            tags = allRefs
+                .filter { $0.name.hasPrefix("refs/tags/") }
+                .map { $0.toDetail() }
+                .sorted(by: Self.sortTags)
         } catch {
             if isEmptyRepositoryError(error) {
                 branches = []
@@ -284,6 +284,64 @@ final class RepositoryDetailViewModel {
         isLoadingRefs = false
     }
 
+    var defaultBranchReference: ReferenceDetail? {
+        if let headName = repository.head?.name,
+           let branch = branches.first(where: { $0.name == headName }) {
+            return branch
+        }
+        return branches.first
+    }
+
+    var latestTagReference: ReferenceDetail? {
+        tags.first
+    }
+
+    private func fetchAllReferences() async throws -> [ReferencePayload] {
+        var allRefs: [ReferencePayload] = []
+        var cursor: String?
+
+        repeat {
+            let page = try await fetchReferencePage(cursor: cursor)
+            allRefs.append(contentsOf: page.results)
+            cursor = page.cursor
+        } while cursor != nil
+
+        return allRefs
+    }
+
+    private func fetchReferencePage(cursor: String?) async throws -> RefsPage {
+        var variables: [String: any Sendable] = ["rid": repository.rid]
+        if let cursor {
+            variables["cursor"] = cursor
+        }
+
+        let result = try await client.execute(
+            service: service,
+            query: Self.refsQuery,
+            variables: variables,
+            responseType: RefsResponse.self
+        )
+
+        return result.repository?.references ?? RefsPage(results: [], cursor: nil)
+    }
+
+    nonisolated private static func sortBranches(_ lhs: ReferenceDetail, _ rhs: ReferenceDetail) -> Bool {
+        lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending
+    }
+
+    nonisolated private static func sortTags(_ lhs: ReferenceDetail, _ rhs: ReferenceDetail) -> Bool {
+        switch (lhs.date, rhs.date) {
+        case let (lhsDate?, rhsDate?) where lhsDate != rhsDate:
+            return lhsDate > rhsDate
+        case (.some, nil):
+            return true
+        case (nil, .some):
+            return false
+        default:
+            return lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending
+        }
+    }
+
     // MARK: - README
 
     private static let readmeQuery = """