Commit 3078d39ae2
Unsigned
Layout: unified · split
Hutch.xcodeproj/project.pbxproj +8 −8
| @@ -515,7 +515,7 @@ | ||
| 515 | 515 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; |
| 516 | 516 | CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements; |
| 517 | 517 | CODE_SIGN_STYLE = Automatic; |
| 518 | CURRENT_PROJECT_VERSION = 72; | |
| 518 | CURRENT_PROJECT_VERSION = 73; | |
| 519 | 519 | DEVELOPMENT_TEAM = ZCNAX3VL9D; |
| 520 | 520 | ENABLE_PREVIEWS = YES; |
| 521 | 521 | GENERATE_INFOPLIST_FILE = YES; |
| @@ -532,7 +532,7 @@ | ||
| 532 | 532 | "$(inherited)", |
| 533 | 533 | "@executable_path/Frameworks", |
| 534 | 534 | ); |
| 535 | MARKETING_VERSION = 3.1.4; | |
| 535 | MARKETING_VERSION = 3.1.5; | |
| 536 | 536 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; |
| 537 | 537 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 538 | 538 | STRING_CATALOG_GENERATE_SYMBOLS = YES; |
| @@ -552,7 +552,7 @@ | ||
| 552 | 552 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; |
| 553 | 553 | CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements; |
| 554 | 554 | CODE_SIGN_STYLE = Automatic; |
| 555 | CURRENT_PROJECT_VERSION = 72; | |
| 555 | CURRENT_PROJECT_VERSION = 73; | |
| 556 | 556 | DEVELOPMENT_TEAM = ZCNAX3VL9D; |
| 557 | 557 | ENABLE_PREVIEWS = YES; |
| 558 | 558 | GENERATE_INFOPLIST_FILE = YES; |
| @@ -569,7 +569,7 @@ | ||
| 569 | 569 | "$(inherited)", |
| 570 | 570 | "@executable_path/Frameworks", |
| 571 | 571 | ); |
| 572 | MARKETING_VERSION = 3.1.4; | |
| 572 | MARKETING_VERSION = 3.1.5; | |
| 573 | 573 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; |
| 574 | 574 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 575 | 575 | STRING_CATALOG_GENERATE_SYMBOLS = YES; |
| @@ -632,7 +632,7 @@ | ||
| 632 | 632 | APPLICATION_EXTENSION_API_ONLY = YES; |
| 633 | 633 | CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements; |
| 634 | 634 | CODE_SIGN_STYLE = Automatic; |
| 635 | CURRENT_PROJECT_VERSION = 72; | |
| 635 | CURRENT_PROJECT_VERSION = 73; | |
| 636 | 636 | DEVELOPMENT_TEAM = ZCNAX3VL9D; |
| 637 | 637 | GENERATE_INFOPLIST_FILE = NO; |
| 638 | 638 | INFOPLIST_FILE = HutchWidgetExtension/Info.plist; |
| @@ -642,7 +642,7 @@ | ||
| 642 | 642 | "@executable_path/Frameworks", |
| 643 | 643 | "@executable_path/../../Frameworks", |
| 644 | 644 | ); |
| 645 | MARKETING_VERSION = 3.1.4; | |
| 645 | MARKETING_VERSION = 3.1.5; | |
| 646 | 646 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; |
| 647 | 647 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 648 | 648 | SKIP_INSTALL = YES; |
| @@ -661,7 +661,7 @@ | ||
| 661 | 661 | APPLICATION_EXTENSION_API_ONLY = YES; |
| 662 | 662 | CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements; |
| 663 | 663 | CODE_SIGN_STYLE = Automatic; |
| 664 | CURRENT_PROJECT_VERSION = 72; | |
| 664 | CURRENT_PROJECT_VERSION = 73; | |
| 665 | 665 | DEVELOPMENT_TEAM = ZCNAX3VL9D; |
| 666 | 666 | GENERATE_INFOPLIST_FILE = NO; |
| 667 | 667 | INFOPLIST_FILE = HutchWidgetExtension/Info.plist; |
| @@ -671,7 +671,7 @@ | ||
| 671 | 671 | "@executable_path/Frameworks", |
| 672 | 672 | "@executable_path/../../Frameworks", |
| 673 | 673 | ); |
| 674 | MARKETING_VERSION = 3.1.4; | |
| 674 | MARKETING_VERSION = 3.1.5; | |
| 675 | 675 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; |
| 676 | 676 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 677 | 677 | SKIP_INSTALL = YES; |
Hutch/Networking/SRHTClient.swift +13 −12
| @@ -23,13 +23,13 @@ final class SRHTClient: Sendable { | ||
| 23 | 23 | |
| 24 | 24 | /// The personal access token used for `Authorization: Bearer` headers. |
| 25 | 25 | /// Loaded from Keychain on init; can be refreshed via ``reloadToken()``. |
| 26 | private let _token: OSAllocatedUnfairLock<String?> | |
| 26 | private let tokenLock: OSAllocatedUnfairLock<String?> | |
| 27 | 27 | |
| 28 | 28 | /// In-memory response cache for stale-while-revalidate pattern. |
| 29 | 29 | let responseCache = ResponseCache() |
| 30 | 30 | |
| 31 | 31 | var hasToken: Bool { |
| 32 | _token.withLock { $0 != nil } | |
| 32 | tokenLock.withLock { $0 != nil } | |
| 33 | 33 | } |
| 34 | 34 | |
| 35 | 35 | init(session: URLSession = .shared, token: String? = nil) { |
| @@ -37,12 +37,12 @@ final class SRHTClient: Sendable { | ||
| 37 | 37 | self.decoder = JSONDecoder() |
| 38 | 38 | self.decoder.dateDecodingStrategy = .srhtFlexible |
| 39 | 39 | self.encoder = JSONEncoder() |
| 40 | self._token = OSAllocatedUnfairLock(initialState: token) | |
| 40 | self.tokenLock = OSAllocatedUnfairLock(initialState: token) | |
| 41 | 41 | } |
| 42 | 42 | |
| 43 | 43 | /// Update the stored token (e.g. after the user saves a new one in Keychain). |
| 44 | 44 | func setToken(_ token: String?) { |
| 45 | _token.withLock { $0 = token } | |
| 45 | tokenLock.withLock { $0 = token } | |
| 46 | 46 | } |
| 47 | 47 | |
| 48 | 48 | /// Execute a GraphQL query or mutation against a SourceHut service. |
| @@ -59,7 +59,7 @@ final class SRHTClient: Sendable { | ||
| 59 | 59 | variables: [String: any Sendable]? = nil, |
| 60 | 60 | responseType _: T.Type |
| 61 | 61 | ) async throws -> T { |
| 62 | guard let token = _token.withLock({ $0 }), !token.isEmpty else { | |
| 62 | guard let token = tokenLock.withLock({ $0 }), !token.isEmpty else { | |
| 63 | 63 | throw SRHTError.unauthorized |
| 64 | 64 | } |
| 65 | 65 | |
| @@ -173,7 +173,7 @@ final class SRHTClient: Sendable { | ||
| 173 | 173 | file: MultipartUploadFile, |
| 174 | 174 | responseType _: T.Type |
| 175 | 175 | ) async throws -> T { |
| 176 | guard let token = _token.withLock({ $0 }), !token.isEmpty else { | |
| 176 | guard let token = tokenLock.withLock({ $0 }), !token.isEmpty else { | |
| 177 | 177 | throw SRHTError.unauthorized |
| 178 | 178 | } |
| 179 | 179 | |
| @@ -307,7 +307,7 @@ final class SRHTClient: Sendable { | ||
| 307 | 307 | files: [MultipartUploadFile], |
| 308 | 308 | responseType _: T.Type |
| 309 | 309 | ) async throws -> T { |
| 310 | guard let token = _token.withLock({ $0 }), !token.isEmpty else { | |
| 310 | guard let token = tokenLock.withLock({ $0 }), !token.isEmpty else { | |
| 311 | 311 | throw SRHTError.unauthorized |
| 312 | 312 | } |
| 313 | 313 | |
| @@ -417,7 +417,7 @@ final class SRHTClient: Sendable { | ||
| 417 | 417 | service: service, |
| 418 | 418 | query: query, |
| 419 | 419 | variables: variables, |
| 420 | responseType: responseType, | |
| 420 | responseType: T.self, | |
| 421 | 421 | cacheKey: cacheKey |
| 422 | 422 | ) |
| 423 | 423 | } |
| @@ -430,7 +430,7 @@ final class SRHTClient: Sendable { | ||
| 430 | 430 | responseType _: T.Type, |
| 431 | 431 | cacheKey: String |
| 432 | 432 | ) async throws -> T { |
| 433 | guard let token = _token.withLock({ $0 }), !token.isEmpty else { | |
| 433 | guard let token = tokenLock.withLock({ $0 }), !token.isEmpty else { | |
| 434 | 434 | throw SRHTError.unauthorized |
| 435 | 435 | } |
| 436 | 436 | |
| @@ -527,7 +527,7 @@ final class SRHTClient: Sendable { | ||
| 527 | 527 | /// Fetch the contents of a URL as plain text, using the same authorization header. |
| 528 | 528 | /// Used for build logs and other non-GraphQL resources. |
| 529 | 529 | func fetchText(url: URL) async throws -> String { |
| 530 | guard let token = _token.withLock({ $0 }), !token.isEmpty else { | |
| 530 | guard let token = tokenLock.withLock({ $0 }), !token.isEmpty else { | |
| 531 | 531 | throw SRHTError.unauthorized |
| 532 | 532 | } |
| 533 | 533 | guard Self.isTrustedAuthenticatedTextURL(url) else { |
| @@ -593,12 +593,13 @@ final class SRHTClient: Sendable { | ||
| 593 | 593 | resultKeyPath: String |
| 594 | 594 | ) async throws -> [T] { |
| 595 | 595 | var all: [T] = [] |
| 596 | for try await element in paginated( | |
| 596 | let pages: SRHTPaginatedSequence<T> = paginated( | |
| 597 | 597 | service: service, |
| 598 | 598 | query: query, |
| 599 | 599 | variables: variables, |
| 600 | 600 | resultKeyPath: resultKeyPath |
| 601 | ) { | |
| 601 | ) | |
| 602 | for try await element in pages { | |
| 602 | 603 | all.append(element) |
| 603 | 604 | } |
| 604 | 605 | return all |
Hutch/Views/Builds/BuildListViewModel.swift +3 −2
| @@ -182,12 +182,13 @@ final class BuildListViewModel { | ||
| 182 | 182 | /// Fetch the first page of jobs. Shows cached data instantly if available, |
| 183 | 183 | /// then refreshes from the network in the background. |
| 184 | 184 | func loadJobs() async { |
| 185 | // Show cached data immediately on first load | |
| 185 | // Show cached data immediately on first load (may populate `jobs` from cache). | |
| 186 | 186 | if jobs.isEmpty { |
| 187 | 187 | loadFromCache() |
| 188 | 188 | } |
| 189 | 189 | |
| 190 | if jobs.isEmpty { | |
| 190 | let treatAsInitialLoad = jobs.isEmpty | |
| 191 | if treatAsInitialLoad { | |
| 191 | 192 | isLoading = true |
| 192 | 193 | } else { |
| 193 | 194 | isRefreshing = true |
Hutch/Views/Pastes/PasteListView.swift +4 −4
| @@ -276,14 +276,14 @@ private struct CreatePasteSheet: View { | ||
| 276 | 276 | NavigationStack { |
| 277 | 277 | Form { |
| 278 | 278 | Section("Files") { |
| 279 | ForEach($files) { $file in | |
| 279 | ForEach($files) { fileBinding in | |
| 280 | 280 | VStack(alignment: .leading, spacing: 8) { |
| 281 | TextField("Filename (optional)", text: $file.filename) | |
| 281 | TextField("Filename (optional)", text: fileBinding.filename) | |
| 282 | 282 | .autocorrectionDisabled() |
| 283 | 283 | .textInputAutocapitalization(.never) |
| 284 | 284 | |
| 285 | 285 | ZStack(alignment: .topLeading) { |
| 286 | if file.contents.isEmpty { | |
| 286 | if fileBinding.wrappedValue.contents.isEmpty { | |
| 287 | 287 | Text("Paste contents") |
| 288 | 288 | .foregroundStyle(.tertiary) |
| 289 | 289 | .padding(.top, 8) |
| @@ -291,7 +291,7 @@ private struct CreatePasteSheet: View { | ||
| 291 | 291 | .allowsHitTesting(false) |
| 292 | 292 | } |
| 293 | 293 | |
| 294 | TextEditor(text: $file.contents) | |
| 294 | TextEditor(text: fileBinding.contents) | |
| 295 | 295 | .font(.system(.body, design: .monospaced)) |
| 296 | 296 | .frame(minHeight: 180) |
| 297 | 297 | } |
Hutch/Views/Repositories/CommitDetailViewModel.swift +6 −3
| @@ -7,8 +7,11 @@ private struct CommitResponse: Decodable, Sendable { | ||
| 7 | 7 | } |
| 8 | 8 | |
| 9 | 9 | private struct CommitRepository: Decodable, Sendable { |
| 10 | // swiftlint:disable:next identifier_name | |
| 11 | let revparse_single: CommitDetail | |
| 10 | let revparseSingle: CommitDetail | |
| 11 | ||
| 12 | enum CodingKeys: String, CodingKey { | |
| 13 | case revparseSingle = "revparse_single" | |
| 14 | } | |
| 12 | 15 | } |
| 13 | 16 | |
| 14 | 17 | // MARK: - View Model |
| @@ -66,7 +69,7 @@ final class CommitDetailViewModel { | ||
| 66 | 69 | |
| 67 | 70 | do { |
| 68 | 71 | let result = try await executeWithRetry() |
| 69 | commit = result.repository?.revparse_single | |
| 72 | commit = result.repository?.revparseSingle | |
| 70 | 73 | } catch { |
| 71 | 74 | self.error = error.userFacingMessage |
| 72 | 75 | } |
Hutch/Views/Repositories/FileTreeViewModel.swift +6 −2
| @@ -7,7 +7,11 @@ private struct RevparseResponse: Decodable, Sendable { | ||
| 7 | 7 | } |
| 8 | 8 | |
| 9 | 9 | private struct RevparseRepository: Decodable, Sendable { |
| 10 | let revparse_single: RevparseCommit? | |
| 10 | let revparseSingle: RevparseCommit? | |
| 11 | ||
| 12 | enum CodingKeys: String, CodingKey { | |
| 13 | case revparseSingle = "revparse_single" | |
| 14 | } | |
| 11 | 15 | } |
| 12 | 16 | |
| 13 | 17 | private struct RevparseCommit: Decodable, Sendable { |
| @@ -277,7 +281,7 @@ final class FileTreeViewModel { | ||
| 277 | 281 | } |
| 278 | 282 | throw error |
| 279 | 283 | } |
| 280 | if let tree = result.repository?.revparse_single?.tree, | |
| 284 | if let tree = result.repository?.revparseSingle?.tree, | |
| 281 | 285 | let rootId = tree.id { |
| 282 | 286 | navStack = [FileNavEntry(name: "root", treeId: rootId)] |
| 283 | 287 | var allEntries = tree.entries?.results ?? [] |