diff options
| author | Christian Cleberg <[email protected]> | 2026-04-13 17:35:37 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-13 17:35:37 -0500 |
| commit | 30decf6d9b6b95277cbd8b8a7d9e23d9ce5413ca (patch) | |
| tree | 8ba13d62083d7c3555bd250e5cefe416eb698d31 | |
| parent | 5e63048ab6a741ae947f7fe42efcb5206a3f3963 (diff) | |
| download | hutch-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
| -rw-r--r-- | Hutch.xcodeproj/project.pbxproj | 16 | ||||
| -rw-r--r-- | Hutch/Views/Repositories/RepositoryDetailViewModel.swift | 24 | ||||
| -rw-r--r-- | Hutch/Views/Tickets/TicketListViewModel.swift | 34 |
3 files changed, 54 insertions, 20 deletions
diff --git a/Hutch.xcodeproj/project.pbxproj b/Hutch.xcodeproj/project.pbxproj index 2d7ef6d..19792d4 100644 --- a/Hutch.xcodeproj/project.pbxproj +++ b/Hutch.xcodeproj/project.pbxproj @@ -515,7 +515,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 65; + CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_TEAM = ZCNAX3VL9D; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -532,7 +532,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.0.4; + MARKETING_VERSION = 3.0.5; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -552,7 +552,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 65; + CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_TEAM = ZCNAX3VL9D; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -569,7 +569,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.0.4; + MARKETING_VERSION = 3.0.5; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -632,7 +632,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 65; + CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_TEAM = ZCNAX3VL9D; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = HutchWidgetExtension/Info.plist; @@ -642,7 +642,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 3.0.4; + MARKETING_VERSION = 3.0.5; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -661,7 +661,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 65; + CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_TEAM = ZCNAX3VL9D; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = HutchWidgetExtension/Info.plist; @@ -671,7 +671,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 3.0.4; + MARKETING_VERSION = 3.0.5; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; 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 { |
