krz/hutch

an ios client for sourcehut

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

d6968ea122d910b432bd539cf2fa3d1cee962fdb

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2026-04-14T01:32:40Z

fix: more sonarqube quality fixes
 Hutch.xcodeproj/project.pbxproj                    | 16 +++++++-------
 Hutch/Networking/SRHTClient.swift                  | 25 +++++++++++-----------
 Hutch/Views/Builds/BuildListViewModel.swift        |  5 +++--
 Hutch/Views/Pastes/PasteListView.swift             |  8 +++----
 .../Views/Repositories/CommitDetailViewModel.swift |  9 +++++---
 Hutch/Views/Repositories/FileTreeViewModel.swift   |  8 +++++--
 6 files changed, 40 insertions(+), 31 deletions(-)

diff --git a/Hutch.xcodeproj/project.pbxproj b/Hutch.xcodeproj/project.pbxproj
index 3162b32..68ef58c 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 = 72;
+				CURRENT_PROJECT_VERSION = 73;
 				DEVELOPMENT_TEAM = ZCNAX3VL9D;
 				ENABLE_PREVIEWS = YES;
 				GENERATE_INFOPLIST_FILE = YES;
@@ -532,7 +532,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 3.1.4;
+				MARKETING_VERSION = 3.1.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 = 72;
+				CURRENT_PROJECT_VERSION = 73;
 				DEVELOPMENT_TEAM = ZCNAX3VL9D;
 				ENABLE_PREVIEWS = YES;
 				GENERATE_INFOPLIST_FILE = YES;
@@ -569,7 +569,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 3.1.4;
+				MARKETING_VERSION = 3.1.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 = 72;
+				CURRENT_PROJECT_VERSION = 73;
 				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.1.4;
+				MARKETING_VERSION = 3.1.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 = 72;
+				CURRENT_PROJECT_VERSION = 73;
 				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.1.4;
+				MARKETING_VERSION = 3.1.5;
 				PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SKIP_INSTALL = YES;
diff --git a/Hutch/Networking/SRHTClient.swift b/Hutch/Networking/SRHTClient.swift
index fa7b26b..70c7650 100644
--- a/Hutch/Networking/SRHTClient.swift
+++ b/Hutch/Networking/SRHTClient.swift
@@ -23,13 +23,13 @@ final class SRHTClient: Sendable {
 
     /// The personal access token used for `Authorization: Bearer` headers.
     /// Loaded from Keychain on init; can be refreshed via ``reloadToken()``.
-    private let _token: OSAllocatedUnfairLock<String?>
+    private let tokenLock: OSAllocatedUnfairLock<String?>
 
     /// In-memory response cache for stale-while-revalidate pattern.
     let responseCache = ResponseCache()
 
     var hasToken: Bool {
-        _token.withLock { $0 != nil }
+        tokenLock.withLock { $0 != nil }
     }
 
     init(session: URLSession = .shared, token: String? = nil) {
@@ -37,12 +37,12 @@ final class SRHTClient: Sendable {
         self.decoder = JSONDecoder()
         self.decoder.dateDecodingStrategy = .srhtFlexible
         self.encoder = JSONEncoder()
-        self._token = OSAllocatedUnfairLock(initialState: token)
+        self.tokenLock = OSAllocatedUnfairLock(initialState: token)
     }
 
     /// Update the stored token (e.g. after the user saves a new one in Keychain).
     func setToken(_ token: String?) {
-        _token.withLock { $0 = token }
+        tokenLock.withLock { $0 = token }
     }
 
     /// Execute a GraphQL query or mutation against a SourceHut service.
@@ -59,7 +59,7 @@ final class SRHTClient: Sendable {
         variables: [String: any Sendable]? = nil,
         responseType _: T.Type
     ) async throws -> T {
-        guard let token = _token.withLock({ $0 }), !token.isEmpty else {
+        guard let token = tokenLock.withLock({ $0 }), !token.isEmpty else {
             throw SRHTError.unauthorized
         }
 
@@ -173,7 +173,7 @@ final class SRHTClient: Sendable {
         file: MultipartUploadFile,
         responseType _: T.Type
     ) async throws -> T {
-        guard let token = _token.withLock({ $0 }), !token.isEmpty else {
+        guard let token = tokenLock.withLock({ $0 }), !token.isEmpty else {
             throw SRHTError.unauthorized
         }
 
@@ -307,7 +307,7 @@ final class SRHTClient: Sendable {
         files: [MultipartUploadFile],
         responseType _: T.Type
     ) async throws -> T {
-        guard let token = _token.withLock({ $0 }), !token.isEmpty else {
+        guard let token = tokenLock.withLock({ $0 }), !token.isEmpty else {
             throw SRHTError.unauthorized
         }
 
@@ -417,7 +417,7 @@ final class SRHTClient: Sendable {
             service: service,
             query: query,
             variables: variables,
-            responseType: responseType,
+            responseType: T.self,
             cacheKey: cacheKey
         )
     }
@@ -430,7 +430,7 @@ final class SRHTClient: Sendable {
         responseType _: T.Type,
         cacheKey: String
     ) async throws -> T {
-        guard let token = _token.withLock({ $0 }), !token.isEmpty else {
+        guard let token = tokenLock.withLock({ $0 }), !token.isEmpty else {
             throw SRHTError.unauthorized
         }
 
@@ -527,7 +527,7 @@ final class SRHTClient: Sendable {
     /// Fetch the contents of a URL as plain text, using the same authorization header.
     /// Used for build logs and other non-GraphQL resources.
     func fetchText(url: URL) async throws -> String {
-        guard let token = _token.withLock({ $0 }), !token.isEmpty else {
+        guard let token = tokenLock.withLock({ $0 }), !token.isEmpty else {
             throw SRHTError.unauthorized
         }
         guard Self.isTrustedAuthenticatedTextURL(url) else {
@@ -593,12 +593,13 @@ final class SRHTClient: Sendable {
         resultKeyPath: String
     ) async throws -> [T] {
         var all: [T] = []
-        for try await element in paginated(
+        let pages: SRHTPaginatedSequence<T> = paginated(
             service: service,
             query: query,
             variables: variables,
             resultKeyPath: resultKeyPath
-        ) {
+        )
+        for try await element in pages {
             all.append(element)
         }
         return all
diff --git a/Hutch/Views/Builds/BuildListViewModel.swift b/Hutch/Views/Builds/BuildListViewModel.swift
index 643bacd..cf5d786 100644
--- a/Hutch/Views/Builds/BuildListViewModel.swift
+++ b/Hutch/Views/Builds/BuildListViewModel.swift
@@ -182,12 +182,13 @@ final class BuildListViewModel {
     /// Fetch the first page of jobs. Shows cached data instantly if available,
     /// then refreshes from the network in the background.
     func loadJobs() async {
-        // Show cached data immediately on first load
+        // Show cached data immediately on first load (may populate `jobs` from cache).
         if jobs.isEmpty {
             loadFromCache()
         }
 
-        if jobs.isEmpty {
+        let treatAsInitialLoad = jobs.isEmpty
+        if treatAsInitialLoad {
             isLoading = true
         } else {
             isRefreshing = true
diff --git a/Hutch/Views/Pastes/PasteListView.swift b/Hutch/Views/Pastes/PasteListView.swift
index 17bb8ac..b325153 100644
--- a/Hutch/Views/Pastes/PasteListView.swift
+++ b/Hutch/Views/Pastes/PasteListView.swift
@@ -276,14 +276,14 @@ private struct CreatePasteSheet: View {
         NavigationStack {
             Form {
                 Section("Files") {
-                    ForEach($files) { $file in
+                    ForEach($files) { fileBinding in
                         VStack(alignment: .leading, spacing: 8) {
-                            TextField("Filename (optional)", text: $file.filename)
+                            TextField("Filename (optional)", text: fileBinding.filename)
                                 .autocorrectionDisabled()
                                 .textInputAutocapitalization(.never)
 
                             ZStack(alignment: .topLeading) {
-                                if file.contents.isEmpty {
+                                if fileBinding.wrappedValue.contents.isEmpty {
                                     Text("Paste contents")
                                         .foregroundStyle(.tertiary)
                                         .padding(.top, 8)
@@ -291,7 +291,7 @@ private struct CreatePasteSheet: View {
                                         .allowsHitTesting(false)
                                 }
 
-                                TextEditor(text: $file.contents)
+                                TextEditor(text: fileBinding.contents)
                                     .font(.system(.body, design: .monospaced))
                                     .frame(minHeight: 180)
                             }
diff --git a/Hutch/Views/Repositories/CommitDetailViewModel.swift b/Hutch/Views/Repositories/CommitDetailViewModel.swift
index f0898f5..92fb915 100644
--- a/Hutch/Views/Repositories/CommitDetailViewModel.swift
+++ b/Hutch/Views/Repositories/CommitDetailViewModel.swift
@@ -7,8 +7,11 @@ private struct CommitResponse: Decodable, Sendable {
 }
 
 private struct CommitRepository: Decodable, Sendable {
-    // swiftlint:disable:next identifier_name
-    let revparse_single: CommitDetail
+    let revparseSingle: CommitDetail
+
+    enum CodingKeys: String, CodingKey {
+        case revparseSingle = "revparse_single"
+    }
 }
 
 // MARK: - View Model
@@ -66,7 +69,7 @@ final class CommitDetailViewModel {
 
         do {
             let result = try await executeWithRetry()
-            commit = result.repository?.revparse_single
+            commit = result.repository?.revparseSingle
         } catch {
             self.error = error.userFacingMessage
         }
diff --git a/Hutch/Views/Repositories/FileTreeViewModel.swift b/Hutch/Views/Repositories/FileTreeViewModel.swift
index 98211bf..7330cd8 100644
--- a/Hutch/Views/Repositories/FileTreeViewModel.swift
+++ b/Hutch/Views/Repositories/FileTreeViewModel.swift
@@ -7,7 +7,11 @@ private struct RevparseResponse: Decodable, Sendable {
 }
 
 private struct RevparseRepository: Decodable, Sendable {
-    let revparse_single: RevparseCommit?
+    let revparseSingle: RevparseCommit?
+
+    enum CodingKeys: String, CodingKey {
+        case revparseSingle = "revparse_single"
+    }
 }
 
 private struct RevparseCommit: Decodable, Sendable {
@@ -277,7 +281,7 @@ final class FileTreeViewModel {
                 }
                 throw error
             }
-            if let tree = result.repository?.revparse_single?.tree,
+            if let tree = result.repository?.revparseSingle?.tree,
                let rootId = tree.id {
                 navStack = [FileNavEntry(name: "root", treeId: rootId)]
                 var allEntries = tree.entries?.results ?? []