summaryrefslogtreecommitdiff
path: root/Hutch/Views
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-13 17:35:37 -0500
committerChristian Cleberg <[email protected]>2026-04-13 17:35:37 -0500
commit30decf6d9b6b95277cbd8b8a7d9e23d9ce5413ca (patch)
tree8ba13d62083d7c3555bd250e5cefe416eb698d31 /Hutch/Views
parent5e63048ab6a741ae947f7fe42efcb5206a3f3963 (diff)
downloadhutch-30decf6d9b6b95277cbd8b8a7d9e23d9ce5413ca.tar.gz
hutch-30decf6d9b6b95277cbd8b8a7d9e23d9ce5413ca.tar.bz2
hutch-30decf6d9b6b95277cbd8b8a7d9e23d9ce5413ca.zip
fix: show empty state for bare git repos instead of an error
git.sr.ht returns "internal system error" from the references query and "reference not found" from the log/readme queries when a repo has no commits. Broaden isEmptyRepositoryError to cover missingReference, unknownRevision, noRows, notFound, and those two message strings, then apply the same silent-empty treatment to loadReferences and loadArtifacts (which previously surfaced any error directly to the user). Fixes: https://todo.sr.ht/~ccleberg/hutch/60
Diffstat (limited to 'Hutch/Views')
-rw-r--r--Hutch/Views/Repositories/RepositoryDetailViewModel.swift24
-rw-r--r--Hutch/Views/Tickets/TicketListViewModel.swift34
2 files changed, 46 insertions, 12 deletions
diff --git a/Hutch/Views/Repositories/RepositoryDetailViewModel.swift b/Hutch/Views/Repositories/RepositoryDetailViewModel.swift
index ebd07d2..8dec51b 100644
--- a/Hutch/Views/Repositories/RepositoryDetailViewModel.swift
+++ b/Hutch/Views/Repositories/RepositoryDetailViewModel.swift
@@ -226,7 +226,7 @@ final class RepositoryDetailViewModel {
responseType: LogResponse.self
)
} catch {
- if isMissingGitReferenceError(error) {
+ if isEmptyRepositoryError(error) {
return LogPage(results: [], cursor: nil)
}
throw error
@@ -273,7 +273,12 @@ final class RepositoryDetailViewModel {
branches = allRefs.filter { $0.name.hasPrefix("refs/heads/") }.map { $0.toDetail() }
tags = allRefs.filter { $0.name.hasPrefix("refs/tags/") }.map { $0.toDetail() }
} catch {
- self.error = error.userFacingMessage
+ if isEmptyRepositoryError(error) {
+ branches = []
+ tags = []
+ } else {
+ self.error = error.userFacingMessage
+ }
}
isLoadingRefs = false
@@ -340,7 +345,7 @@ final class RepositoryDetailViewModel {
responseType: PathResponse.self
)
} catch {
- if isMissingGitReferenceError(error) {
+ if isEmptyRepositoryError(error) {
readmeContent = nil
readmePath = nil
readmeLoaded = true
@@ -371,8 +376,13 @@ final class RepositoryDetailViewModel {
}
}
- private func isMissingGitReferenceError(_ error: Error) -> Bool {
+ private func isEmptyRepositoryError(_ error: Error) -> Bool {
error.matchesGraphQLErrorClassification(.missingReference)
+ || error.matchesGraphQLErrorClassification(.unknownRevision)
+ || error.matchesGraphQLErrorClassification(.noRows)
+ || error.matchesGraphQLErrorClassification(.notFound)
+ || error.containsGraphQLErrorMessage("missing")
+ || error.containsGraphQLErrorMessage("internal system error")
}
// MARK: - Artifacts
@@ -417,7 +427,11 @@ final class RepositoryDetailViewModel {
.filter { !$0.artifacts.results.isEmpty }
.map { ReferenceWithArtifacts(name: $0.name, artifacts: $0.artifacts.results) }
} catch {
- self.error = error.userFacingMessage
+ if isEmptyRepositoryError(error) {
+ referenceArtifacts = []
+ } else {
+ self.error = error.userFacingMessage
+ }
}
isLoadingArtifacts = false
diff --git a/Hutch/Views/Tickets/TicketListViewModel.swift b/Hutch/Views/Tickets/TicketListViewModel.swift
index e23cc52..0255798 100644
--- a/Hutch/Views/Tickets/TicketListViewModel.swift
+++ b/Hutch/Views/Tickets/TicketListViewModel.swift
@@ -99,13 +99,13 @@ final class TicketListViewModel {
var filter: TicketFilter = .open {
didSet {
persistFilterState()
- updateFilteredTickets()
+ resetPaginationAndUpdateFilters()
}
}
var selectedLabelIDs: Set<Int> = [] {
didSet {
persistFilterState()
- updateFilteredTickets()
+ resetPaginationAndUpdateFilters()
}
}
var searchText = "" {
@@ -283,6 +283,13 @@ final class TicketListViewModel {
}
}
+ private func resetPaginationAndUpdateFilters() {
+ // Reset pagination when filters change since the cursor is tied to the unfiltered dataset
+ cursor = nil
+ hasMore = true
+ updateFilteredTickets()
+ }
+
// MARK: - Public API
func loadTickets() async {
@@ -305,19 +312,32 @@ final class TicketListViewModel {
}
func loadMoreIfNeeded(currentItem: TicketSummary) async {
- guard let last = tickets.last,
- last.id == currentItem.id,
- hasMore,
- !isLoadingMore else {
+ // Check if currentItem is in the filtered list and close to the end
+ guard hasMore, !isLoadingMore else { return }
+
+ guard let index = filteredTickets.firstIndex(where: { $0.id == currentItem.id }) else {
return
}
+ let itemsFromEnd = filteredTickets.count - index - 1
+ guard itemsFromEnd < 5 else { return }
+
isLoadingMore = true
do {
let page = try await fetchPage(cursor: cursor)
- tickets.append(contentsOf: page.results)
+
+ // Deduplicate: only add tickets that don't already exist
+ let existingIDs = Set(tickets.map(\.id))
+ let newTickets = page.results.filter { !existingIDs.contains($0.id) }
+
+ // If we got back the same tickets, the API cursor pagination isn't working
+ if newTickets.isEmpty && !page.results.isEmpty {
+ hasMore = false
+ }
+
cursor = page.cursor
+ tickets.append(contentsOf: newTickets)
hasMore = page.cursor != nil
reconcileSelectionWithLoadedTickets()
} catch {