Commit 104dd73fbc
Unsigned
Layout: unified · split
README.org +1 −1
| @@ -46,7 +46,7 @@ Any gitbay instance works. You just need to enter the host at sign-in. | ||
| 46 | 46 | |
| 47 | 47 | * Building |
| 48 | 48 | |
| 49 | Requires Xcode with an iOS 26.5 SDK. Swift 6 language mode, default =MainActor= | |
| 49 | Builds with the iOS 26.5 SDK and runs on iOS 18.0 and later. Swift 6 language mode, default =MainActor= | |
| 50 | 50 | isolation. |
| 51 | 51 | |
| 52 | 52 | #+begin_src sh |
gitbay.xcodeproj/project.pbxproj +4 −2
| @@ -338,7 +338,7 @@ | ||
| 338 | 338 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; |
| 339 | 339 | GCC_WARN_UNUSED_FUNCTION = YES; |
| 340 | 340 | GCC_WARN_UNUSED_VARIABLE = YES; |
| 341 | IPHONEOS_DEPLOYMENT_TARGET = 26.5; | |
| 341 | IPHONEOS_DEPLOYMENT_TARGET = 18.0; | |
| 342 | 342 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; |
| 343 | 343 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; |
| 344 | 344 | MTL_FAST_MATH = YES; |
| @@ -396,7 +396,7 @@ | ||
| 396 | 396 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; |
| 397 | 397 | GCC_WARN_UNUSED_FUNCTION = YES; |
| 398 | 398 | GCC_WARN_UNUSED_VARIABLE = YES; |
| 399 | IPHONEOS_DEPLOYMENT_TARGET = 26.5; | |
| 399 | IPHONEOS_DEPLOYMENT_TARGET = 18.0; | |
| 400 | 400 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; |
| 401 | 401 | MTL_ENABLE_DEBUG_INFO = NO; |
| 402 | 402 | MTL_FAST_MATH = YES; |
| @@ -416,6 +416,7 @@ | ||
| 416 | 416 | DEVELOPMENT_TEAM = ZCNAX3VL9D; |
| 417 | 417 | ENABLE_PREVIEWS = YES; |
| 418 | 418 | GENERATE_INFOPLIST_FILE = YES; |
| 419 | INFOPLIST_KEY_CFBundleDisplayName = gitbay; | |
| 419 | 420 | INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; |
| 420 | 421 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; |
| 421 | 422 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; |
| @@ -450,6 +451,7 @@ | ||
| 450 | 451 | DEVELOPMENT_TEAM = ZCNAX3VL9D; |
| 451 | 452 | ENABLE_PREVIEWS = YES; |
| 452 | 453 | GENERATE_INFOPLIST_FILE = YES; |
| 454 | INFOPLIST_KEY_CFBundleDisplayName = gitbay; | |
| 453 | 455 | INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; |
| 454 | 456 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; |
| 455 | 457 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; |
gitbay/Networking/GitbayClient.swift +4 −1
| @@ -255,7 +255,10 @@ nonisolated final class GitbayClient: Sendable { | ||
| 255 | 255 | |
| 256 | 256 | /// Dates on the wire are RFC 3339, some with fractional seconds |
| 257 | 257 | /// (SQLite's `%Y-%m-%dT%H:%M:%fZ` default), some without (`repo log`). |
| 258 | private static func decoder() -> JSONDecoder { | |
| 258 | /// Internal so tests decode fixtures the way the client does: the | |
| 259 | /// stock `.iso8601` strategy accepts fractional seconds on iOS 26 and | |
| 260 | /// refuses them on iOS 18. | |
| 261 | static func decoder() -> JSONDecoder { | |
| 259 | 262 | let decoder = JSONDecoder() |
| 260 | 263 | decoder.dateDecodingStrategy = .custom { decoder in |
| 261 | 264 | let text = try decoder.singleValueContainer().decode(String.self) |
gitbayTests/BodyFormatTests.swift +2 −4
| @@ -25,14 +25,12 @@ private func stdinOf(_ seen: StubProtocol.Seen) throws -> String? { | ||
| 25 | 25 | struct BodyFormatDecodingTests { |
| 26 | 26 | |
| 27 | 27 | private func decodeIssue(_ json: String) throws -> IssueDetail { |
| 28 | let decoder = JSONDecoder() | |
| 29 | decoder.dateDecodingStrategy = .iso8601 | |
| 28 | let decoder = GitbayClient.decoder() | |
| 30 | 29 | return try decoder.decode(IssueDetail.self, from: Data(json.utf8)) |
| 31 | 30 | } |
| 32 | 31 | |
| 33 | 32 | private func decodeMR(_ json: String) throws -> MRDetail { |
| 34 | let decoder = JSONDecoder() | |
| 35 | decoder.dateDecodingStrategy = .iso8601 | |
| 33 | let decoder = GitbayClient.decoder() | |
| 36 | 34 | return try decoder.decode(MRDetail.self, from: Data(json.utf8)) |
| 37 | 35 | } |
| 38 | 36 | |
gitbayTests/DepsTests.swift +1 −2
| @@ -33,8 +33,7 @@ private let fullStatus = """ | ||
| 33 | 33 | struct DepsDecodingTests { |
| 34 | 34 | |
| 35 | 35 | private func decode(_ json: String) throws -> DepsStatus { |
| 36 | let decoder = JSONDecoder() | |
| 37 | decoder.dateDecodingStrategy = .iso8601 | |
| 36 | let decoder = GitbayClient.decoder() | |
| 38 | 37 | return try decoder.decode(DepsStatus.self, from: Data(json.utf8)) |
| 39 | 38 | } |
| 40 | 39 | |
gitbayTests/DiscoveryTests.swift +1 −2
| @@ -15,8 +15,7 @@ private func makeClient() throws -> (GitbayClient, StubProtocol.Box) { | ||
| 15 | 15 | struct FeedEventTests { |
| 16 | 16 | |
| 17 | 17 | private func decode(_ json: String) throws -> FeedEvent { |
| 18 | let decoder = JSONDecoder() | |
| 19 | decoder.dateDecodingStrategy = .iso8601 | |
| 18 | let decoder = GitbayClient.decoder() | |
| 20 | 19 | return try decoder.decode(FeedEvent.self, from: Data(json.utf8)) |
| 21 | 20 | } |
| 22 | 21 | |
gitbayTests/MRLifecycleTests.swift +2 −4
| @@ -3,14 +3,12 @@ import Testing | ||
| 3 | 3 | @testable import gitbay |
| 4 | 4 | |
| 5 | 5 | private func decodeDetail(_ json: String) throws -> MRDetail { |
| 6 | let decoder = JSONDecoder() | |
| 7 | decoder.dateDecodingStrategy = .iso8601 | |
| 6 | let decoder = GitbayClient.decoder() | |
| 8 | 7 | return try decoder.decode(MRDetail.self, from: Data(json.utf8)) |
| 9 | 8 | } |
| 10 | 9 | |
| 11 | 10 | private func decodeRow(_ json: String) throws -> MergeRequest { |
| 12 | let decoder = JSONDecoder() | |
| 13 | decoder.dateDecodingStrategy = .iso8601 | |
| 11 | let decoder = GitbayClient.decoder() | |
| 14 | 12 | return try decoder.decode(MergeRequest.self, from: Data(json.utf8)) |
| 15 | 13 | } |
| 16 | 14 | |
gitbayTests/MRViewModelTests.swift +1 −2
| @@ -384,8 +384,7 @@ struct ThreadAnchoringTests { | ||
| 384 | 384 | "comments":[{"id":1,"author":"krz","body":"why?",\ |
| 385 | 385 | "created_at":"2026-08-20T10:30:00.000Z"}]} |
| 386 | 386 | """ |
| 387 | let decoder = JSONDecoder() | |
| 388 | decoder.dateDecodingStrategy = .iso8601 | |
| 387 | let decoder = GitbayClient.decoder() | |
| 389 | 388 | return try decoder.decode(ReviewThread.self, from: Data(json.utf8)) |
| 390 | 389 | } |
| 391 | 390 | |
gitbayTests/NotificationTests.swift +1 −2
| @@ -23,8 +23,7 @@ private func argvOf(_ seen: StubProtocol.Seen) throws -> [String] { | ||
| 23 | 23 | } |
| 24 | 24 | |
| 25 | 25 | private func decode(_ json: String) throws -> InboxNotification { |
| 26 | let decoder = JSONDecoder() | |
| 27 | decoder.dateDecodingStrategy = .iso8601 | |
| 26 | let decoder = GitbayClient.decoder() | |
| 28 | 27 | return try decoder.decode(InboxNotification.self, from: Data(json.utf8)) |
| 29 | 28 | } |
| 30 | 29 | |
gitbayTests/ReleaseTests.swift +1 −2
| @@ -161,8 +161,7 @@ struct ReleaseDetailViewModelTests { | ||
| 161 | 161 | struct FeedReleaseDestinationTests { |
| 162 | 162 | |
| 163 | 163 | @Test func releaseEventsNavigateToTheRelease() throws { |
| 164 | let decoder = JSONDecoder() | |
| 165 | decoder.dateDecodingStrategy = .iso8601 | |
| 164 | let decoder = GitbayClient.decoder() | |
| 166 | 165 | let event = try decoder.decode(FeedEvent.self, from: Data(""" |
| 167 | 166 | {"id":2,"repo":"krz/gitbay","actor":"cmc","kind":"release.created",\ |
| 168 | 167 | "data":{"tag":"v1.0.0"},"created_at":"2026-08-26T05:27:36Z"} |
gitbayTests/SearchTests.swift +2 −4
| @@ -29,11 +29,9 @@ private let mixedResults = """ | ||
| 29 | 29 | |
| 30 | 30 | struct SearchResultDecodingTests { |
| 31 | 31 | |
| 32 | /// Matches the established direct-decode pattern in | |
| 33 | /// `DiscoveryTests.swift`: a plain decoder with `.iso8601` dates. | |
| 32 | /// Decodes with the client's own date strategy, as `DiscoveryTests` does. | |
| 34 | 33 | private func decode(_ json: String) throws -> [SearchResult] { |
| 35 | let decoder = JSONDecoder() | |
| 36 | decoder.dateDecodingStrategy = .iso8601 | |
| 34 | let decoder = GitbayClient.decoder() | |
| 37 | 35 | return try decoder.decode([SearchResult].self, from: Data(json.utf8)) |
| 38 | 36 | } |
| 39 | 37 | |
gitbayUITests/LiveSmokeUITests.swift +19 −5
| @@ -39,6 +39,9 @@ final class LiveSmokeUITests: XCTestCase { | ||
| 39 | 39 | default: break // follow the device, as a real launch does |
| 40 | 40 | } |
| 41 | 41 | app.launch() |
| 42 | // A fresh device — a clone on another runtime, say — starts on | |
| 43 | // the token screen; the flows assume a signed-in app. | |
| 44 | signInIfNeeded() | |
| 42 | 45 | if name.contains("testRepoManagementFlows") { |
| 43 | 46 | deleteScratchRepo() |
| 44 | 47 | } |
| @@ -242,7 +245,7 @@ final class LiveSmokeUITests: XCTestCase { | ||
| 242 | 245 | createdRow.tap() |
| 243 | 246 | |
| 244 | 247 | // A fresh MR has one revision; the section lists it. |
| 245 | let revisions = app.staticTexts["Revisions"].firstMatch | |
| 248 | let revisions = header("Revisions") | |
| 246 | 249 | XCTAssertTrue(scrollTo(revisions, swipes: 8), "revisions section missing") |
| 247 | 250 | XCTAssertTrue(app.staticTexts["v1"].firstMatch.exists, "first revision not listed") |
| 248 | 251 | |
| @@ -320,6 +323,12 @@ final class LiveSmokeUITests: XCTestCase { | ||
| 320 | 323 | /// rows do not exist to XCUITest, so waitForExistence alone fails on |
| 321 | 324 | /// anything below the fold. |
| 322 | 325 | @discardableResult |
| 326 | /// A list section header. iOS 18 renders these uppercased and iOS 26 | |
| 327 | /// as written, so the match ignores case. | |
| 328 | func header(_ text: String) -> XCUIElement { | |
| 329 | app.staticTexts.matching(NSPredicate(format: "label ==[c] %@", text)).firstMatch | |
| 330 | } | |
| 331 | ||
| 323 | 332 | func scrollTo(_ element: XCUIElement, swipes: Int = 6) -> Bool { |
| 324 | 333 | for _ in 0..<swipes { |
| 325 | 334 | if element.exists { return true } |
| @@ -650,7 +659,7 @@ extension LiveSmokeUITests { | ||
| 650 | 659 | XCTAssertTrue(app.staticTexts["warez for the public"].firstMatch |
| 651 | 660 | .waitForExistence(timeout: 15), "profile did not load") |
| 652 | 661 | XCTAssertTrue(app.staticTexts |
| 653 | .containing(NSPredicate(format: "label BEGINSWITH 'Repositories'")).firstMatch | |
| 662 | .containing(NSPredicate(format: "label BEGINSWITH[c] 'Repositories'")).firstMatch | |
| 654 | 663 | .waitForExistence(timeout: 10), "profile repos missing") |
| 655 | 664 | back() |
| 656 | 665 | |
| @@ -862,6 +871,11 @@ extension LiveSmokeUITests { | ||
| 862 | 871 | } |
| 863 | 872 | |
| 864 | 873 | private func signInIfNeeded() { |
| 874 | // Signed in already: the tab bar is up and there is no token field. | |
| 875 | if app.tabBars.firstMatch.waitForExistence(timeout: 10), | |
| 876 | !app.secureTextFields.firstMatch.exists { | |
| 877 | return | |
| 878 | } | |
| 865 | 879 | let tokenField = app.secureTextFields.firstMatch |
| 866 | 880 | guard tokenField.waitForExistence(timeout: 5) else { return } |
| 867 | 881 | guard let token = ProcessInfo.processInfo.environment["GITBAY_UITEST_TOKEN"] else { |
| @@ -1010,7 +1024,7 @@ extension LiveSmokeUITests { | ||
| 1010 | 1024 | XCTAssertTrue(app.staticTexts |
| 1011 | 1025 | .containing(NSPredicate(format: "label CONTAINS 'Self-Hosting'")).firstMatch |
| 1012 | 1026 | .waitForExistence(timeout: 20), "profile description missing") |
| 1013 | XCTAssertTrue(app.staticTexts["Organizations"].firstMatch.exists, | |
| 1027 | XCTAssertTrue(header("Organizations").exists, | |
| 1014 | 1028 | "org memberships missing") |
| 1015 | 1029 | XCTAssertTrue(app.staticTexts["krz"].firstMatch.exists, "org row missing") |
| 1016 | 1030 | |
| @@ -1027,7 +1041,7 @@ extension LiveSmokeUITests { | ||
| 1027 | 1041 | .containing(NSPredicate(format: "label CONTAINS 'contributions in the last year'")).firstMatch), |
| 1028 | 1042 | "activity graph missing") |
| 1029 | 1043 | XCTAssertTrue(scrollTo(app.staticTexts |
| 1030 | .containing(NSPredicate(format: "label BEGINSWITH 'Repositories'")).firstMatch), | |
| 1044 | .containing(NSPredicate(format: "label BEGINSWITH[c] 'Repositories'")).firstMatch), | |
| 1031 | 1045 | "repositories missing") |
| 1032 | 1046 | |
| 1033 | 1047 | // --- a log entry opens its commit --- |
| @@ -1063,7 +1077,7 @@ extension LiveSmokeUITests { | ||
| 1063 | 1077 | XCTAssertTrue(main.waitForExistence(timeout: 20), "refs did not load") |
| 1064 | 1078 | XCTAssertTrue(app.staticTexts["default"].firstMatch.exists, |
| 1065 | 1079 | "the default branch is not marked") |
| 1066 | XCTAssertTrue(app.staticTexts["Tags"].firstMatch.exists, "tags section missing") | |
| 1080 | XCTAssertTrue(header("Tags").exists, "tags section missing") | |
| 1067 | 1081 | main.tap() |
| 1068 | 1082 | // Tapping a ref browses the repository there. |
| 1069 | 1083 | XCTAssertTrue(app.cells.firstMatch.waitForExistence(timeout: 20), |