summaryrefslogtreecommitdiff
path: root/Hutch/Views
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-20 12:51:22 -0500
committerChristian Cleberg <[email protected]>2026-04-20 12:51:22 -0500
commitd29fbef60abdffaee190b758530dde9212745ed2 (patch)
tree4615b6bc3ade3395188360e753ed7c757ee4eeb8 /Hutch/Views
parent454c459394437f346a2d08d59a5504c0f6ec299f (diff)
downloadhutch-d29fbef60abdffaee190b758530dde9212745ed2.tar.gz
hutch-d29fbef60abdffaee190b758530dde9212745ed2.tar.bz2
hutch-d29fbef60abdffaee190b758530dde9212745ed2.zip
fix: ensure all branches and tags are accounted for and not cutoff due to pagination
Diffstat (limited to 'Hutch/Views')
-rw-r--r--Hutch/Views/Repositories/ReferencesListView.swift71
-rw-r--r--Hutch/Views/Repositories/RepositoryDetailViewModel.swift80
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 = """