summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Hutch.xcodeproj/project.pbxproj16
-rw-r--r--Hutch/App/HutchIntents.swift78
-rw-r--r--Hutch/Extensions/Date+Relative.swift2
-rw-r--r--Hutch/Extensions/DateFormatter+SRHT.swift2
-rw-r--r--Hutch/Extensions/Foundation+Sendable.swift7
-rw-r--r--Hutch/Extensions/SRHTWebURL.swift2
-rw-r--r--Hutch/Models/AccountEntry.swift2
-rw-r--r--Hutch/Models/ArtifactInfo.swift6
-rw-r--r--Hutch/Models/Builds.swift18
-rw-r--r--Hutch/Models/CommitDetail.swift16
-rw-r--r--Hutch/Models/CommitSummary.swift4
-rw-r--r--Hutch/Models/ContributionCalendar.swift28
-rw-r--r--Hutch/Models/Git.swift40
-rw-r--r--Hutch/Models/Inbox.swift20
-rw-r--r--Hutch/Models/Meta.swift16
-rw-r--r--Hutch/Models/Paste.swift6
-rw-r--r--Hutch/Models/Patchset.swift12
-rw-r--r--Hutch/Models/Project.swift24
-rw-r--r--Hutch/Models/RepositoryACL.swift4
-rw-r--r--Hutch/Models/RepositorySummary.swift8
-rw-r--r--Hutch/Models/SystemStatusModels.swift10
-rw-r--r--Hutch/Models/TicketBulkAction.swift6
-rw-r--r--Hutch/Models/Todo.swift30
-rw-r--r--Hutch/Models/User.swift2
-rw-r--r--Hutch/Views/Lists/MailingListListView.swift2
-rw-r--r--Hutch/Views/More/TipStoreViewModel.swift4
-rw-r--r--Hutch/Views/Repositories/RepositoryDetailViewModel.swift2
-rw-r--r--Hutch/Views/Repositories/SyntaxHighlighter.swift6
-rw-r--r--Hutch/Views/Settings/SettingsViewModel.swift10
-rw-r--r--HutchTests/APICacheTests.swift1
-rw-r--r--HutchTests/AppStateTests.swift1
-rw-r--r--HutchTests/BuildListViewModelTests.swift1
-rw-r--r--HutchTests/BuildTaskLogSearchTests.swift1
-rw-r--r--HutchTests/BuildTaskTests.swift1
-rw-r--r--HutchTests/BundleUserAgentTests.swift1
-rw-r--r--HutchTests/ContributionCalendarTests.swift1
-rw-r--r--HutchTests/DeepLinkTests.swift1
-rw-r--r--HutchTests/GitObjectTests.swift1
-rw-r--r--HutchTests/HomeViewModelTests.swift1
-rw-r--r--HutchTests/HutchTests.swift1
-rw-r--r--HutchTests/InboxViewModelTests.swift1
-rw-r--r--HutchTests/LookupHistoryStoreTests.swift1
-rw-r--r--HutchTests/MailingListActivityTests.swift1
-rw-r--r--HutchTests/ManPageCatalogTests.swift1
-rw-r--r--HutchTests/PatchsetTests.swift2
-rw-r--r--HutchTests/ProjectFormTests.swift1
-rw-r--r--HutchTests/ProjectPinStoreTests.swift1
-rw-r--r--HutchTests/ProjectTests.swift1
-rw-r--r--HutchTests/ReadmeViewTests.swift5
-rw-r--r--HutchTests/RecentActivityStoreTests.swift1
-rw-r--r--HutchTests/RepositoryACLViewModelTests.swift1
-rw-r--r--HutchTests/RepositoryDeployKeyTests.swift1
-rw-r--r--HutchTests/RepositoryListViewModelTests.swift1
-rw-r--r--HutchTests/RepositorySettingsViewModelTests.swift1
-rw-r--r--HutchTests/SRHTClientTests.swift1
-rw-r--r--HutchTests/SRHTWebURLTests.swift1
-rw-r--r--HutchTests/ScopedSearchHistoryStoreTests.swift1
-rw-r--r--HutchTests/SettingsViewModelTests.swift1
-rw-r--r--HutchTests/SettingsViewTests.swift1
-rw-r--r--HutchTests/SourceHutWebDeepLinkMapperTests.swift1
-rw-r--r--HutchTests/SyntaxHighlighterTests.swift9
-rw-r--r--HutchTests/SystemStatusRepositoryTests.swift9
-rw-r--r--HutchTests/SystemStatusServiceTests.swift1
-rw-r--r--HutchTests/TicketBulkActionTests.swift1
-rw-r--r--HutchTests/TicketDetailViewModelTests.swift1
-rw-r--r--HutchTests/TicketSavedFilterStoreTests.swift1
-rw-r--r--HutchTests/TipStoreViewModelTests.swift1
-rw-r--r--HutchTests/TrackerListViewModelTests.swift1
-rw-r--r--HutchTests/TrackerManagementViewModelTests.swift1
-rw-r--r--HutchWidgetExtension/ContributionGraphWidget.swift4
-rw-r--r--ROADMAP.txt40
71 files changed, 276 insertions, 212 deletions
diff --git a/Hutch.xcodeproj/project.pbxproj b/Hutch.xcodeproj/project.pbxproj
index dfa2b62..355157b 100644
--- a/Hutch.xcodeproj/project.pbxproj
+++ b/Hutch.xcodeproj/project.pbxproj
@@ -622,7 +622,7 @@
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
- SWIFT_VERSION = 5.0;
+ SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
@@ -659,7 +659,7 @@
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
- SWIFT_VERSION = 5.0;
+ SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
@@ -680,7 +680,7 @@
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
- SWIFT_VERSION = 5.0;
+ SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Hutch.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Hutch";
};
@@ -702,7 +702,7 @@
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
- SWIFT_VERSION = 5.0;
+ SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Hutch.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Hutch";
};
@@ -732,7 +732,7 @@
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
- SWIFT_VERSION = 5.0;
+ SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
@@ -761,7 +761,7 @@
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
- SWIFT_VERSION = 5.0;
+ SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
@@ -790,7 +790,7 @@
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
- SWIFT_VERSION = 5.0;
+ SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
@@ -819,7 +819,7 @@
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
- SWIFT_VERSION = 5.0;
+ SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
diff --git a/Hutch/App/HutchIntents.swift b/Hutch/App/HutchIntents.swift
index fe4ebdc..3f4cd83 100644
--- a/Hutch/App/HutchIntents.swift
+++ b/Hutch/App/HutchIntents.swift
@@ -4,9 +4,9 @@ import Foundation
// MARK: - Navigation Intents
struct OpenWorkQueueIntent: AppIntent {
- static var title: LocalizedStringResource = "Open Work Queue"
- static var description = IntentDescription("Opens Hutch to your Work Queue.")
- static var openAppWhenRun = true
+ static let title: LocalizedStringResource = "Open Work Queue"
+ static let description = IntentDescription("Opens Hutch to your Work Queue.")
+ static let openAppWhenRun = true
var route: HutchRoute { .workQueue(scope: .all) }
@@ -18,9 +18,9 @@ struct OpenWorkQueueIntent: AppIntent {
}
struct OpenRecentActivityIntent: AppIntent {
- static var title: LocalizedStringResource = "Open Recent Activity"
- static var description = IntentDescription("Opens Hutch to recent activity.")
- static var openAppWhenRun = true
+ static let title: LocalizedStringResource = "Open Recent Activity"
+ static let description = IntentDescription("Opens Hutch to recent activity.")
+ static let openAppWhenRun = true
var route: HutchRoute { .recentActivity }
@@ -32,9 +32,9 @@ struct OpenRecentActivityIntent: AppIntent {
}
struct OpenSystemStatusIntent: AppIntent {
- static var title: LocalizedStringResource = "Open System Status"
- static var description = IntentDescription("Opens Hutch to SourceHut system status.")
- static var openAppWhenRun = true
+ static let title: LocalizedStringResource = "Open System Status"
+ static let description = IntentDescription("Opens Hutch to SourceHut system status.")
+ static let openAppWhenRun = true
var route: HutchRoute { .systemStatus }
@@ -46,9 +46,9 @@ struct OpenSystemStatusIntent: AppIntent {
}
struct OpenPinnedResourceIntent: AppIntent {
- static var title: LocalizedStringResource = "Open Pinned Resource"
- static var description = IntentDescription("Opens a pinned Hutch resource.")
- static var openAppWhenRun = true
+ static let title: LocalizedStringResource = "Open Pinned Resource"
+ static let description = IntentDescription("Opens a pinned Hutch resource.")
+ static let openAppWhenRun = true
@Parameter(title: "Pinned Resource")
var pinnedResource: PinnedResourceEntity
@@ -63,9 +63,9 @@ struct OpenPinnedResourceIntent: AppIntent {
}
struct OpenProjectDashboardIntent: AppIntent {
- static var title: LocalizedStringResource = "Open Project Dashboard"
- static var description = IntentDescription("Opens a pinned project dashboard in Hutch.")
- static var openAppWhenRun = true
+ static let title: LocalizedStringResource = "Open Project Dashboard"
+ static let description = IntentDescription("Opens a pinned project dashboard in Hutch.")
+ static let openAppWhenRun = true
@Parameter(title: "Project")
var project: ProjectEntity
@@ -84,16 +84,16 @@ struct OpenProjectDashboardIntent: AppIntent {
enum HutchShortcutScope: String, AppEnum {
case all
- static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Scope")
- static var caseDisplayRepresentations: [HutchShortcutScope: DisplayRepresentation] = [
+ static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Scope")
+ static let caseDisplayRepresentations: [HutchShortcutScope: DisplayRepresentation] = [
.all: "All"
]
}
struct OpenFailedBuildsIntent: AppIntent {
- static var title: LocalizedStringResource = "Open Failed Builds"
- static var description = IntentDescription("Opens Hutch to failed builds.")
- static var openAppWhenRun = true
+ static let title: LocalizedStringResource = "Open Failed Builds"
+ static let description = IntentDescription("Opens Hutch to failed builds.")
+ static let openAppWhenRun = true
@Parameter(title: "Scope", default: .all)
var scope: HutchShortcutScope
@@ -108,9 +108,9 @@ struct OpenFailedBuildsIntent: AppIntent {
}
struct OpenAssignedTicketsIntent: AppIntent {
- static var title: LocalizedStringResource = "Open Assigned Tickets"
- static var description = IntentDescription("Opens Hutch to tickets assigned to you.")
- static var openAppWhenRun = true
+ static let title: LocalizedStringResource = "Open Assigned Tickets"
+ static let description = IntentDescription("Opens Hutch to tickets assigned to you.")
+ static let openAppWhenRun = true
@Parameter(title: "Scope", default: .all)
var scope: HutchShortcutScope
@@ -125,9 +125,9 @@ struct OpenAssignedTicketsIntent: AppIntent {
}
struct SearchHutchIntent: AppIntent {
- static var title: LocalizedStringResource = "Search Hutch"
- static var description = IntentDescription("Opens Hutch lookup with a search query.")
- static var openAppWhenRun = true
+ static let title: LocalizedStringResource = "Search Hutch"
+ static let description = IntentDescription("Opens Hutch lookup with a search query.")
+ static let openAppWhenRun = true
@Parameter(title: "Query")
var query: String
@@ -155,8 +155,8 @@ struct SearchHutchIntent: AppIntent {
// MARK: - App Entities
struct PinnedResourceEntity: AppEntity, Identifiable {
- static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Pinned Resource")
- static var defaultQuery = PinnedResourceQuery()
+ static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Pinned Resource")
+ static let defaultQuery = PinnedResourceQuery()
let id: String
let name: String
@@ -181,8 +181,8 @@ struct PinnedResourceQuery: EntityQuery {
}
struct ProjectEntity: AppEntity, Identifiable {
- static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Project")
- static var defaultQuery = ProjectEntityQuery()
+ static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Project")
+ static let defaultQuery = ProjectEntityQuery()
let id: String
let name: String
@@ -278,8 +278,8 @@ enum HutchIntentEntityStore {
// MARK: - Existing Read-Only Summary Intents
struct CheckSystemStatusIntent: AppIntent {
- static var title: LocalizedStringResource = "Check SourceHut Status"
- static var description = IntentDescription("Returns the current SourceHut system status.")
+ static let title: LocalizedStringResource = "Check SourceHut Status"
+ static let description = IntentDescription("Returns the current SourceHut system status.")
@MainActor
func perform() async throws -> some IntentResult & ReturnsValue<String> & ProvidesDialog {
@@ -302,8 +302,8 @@ struct CheckSystemStatusIntent: AppIntent {
}
struct CheckBuildsIntent: AppIntent {
- static var title: LocalizedStringResource = "Check Hutch Builds"
- static var description = IntentDescription("Returns a summary of your recent build status.")
+ static let title: LocalizedStringResource = "Check Hutch Builds"
+ static let description = IntentDescription("Returns a summary of your recent build status.")
@MainActor
func perform() async throws -> some IntentResult & ReturnsValue<String> & ProvidesDialog {
@@ -337,7 +337,7 @@ struct CheckBuildsIntent: AppIntent {
// MARK: - Search Type
-extension LookupType: @retroactive AppEnum {
+extension LookupType: AppEnum {
public nonisolated static var typeDisplayRepresentation: TypeDisplayRepresentation {
TypeDisplayRepresentation(name: "Search Type")
}
@@ -357,8 +357,8 @@ extension LookupType: @retroactive AppEnum {
// MARK: - Mutating Intents
struct ClearRecentActivityIntent: AppIntent {
- static var title: LocalizedStringResource = "Clear Recent Activity"
- static var description = IntentDescription("Clears the Recent list on the Hutch Home tab.")
+ static let title: LocalizedStringResource = "Clear Recent Activity"
+ static let description = IntentDescription("Clears the Recent list on the Hutch Home tab.")
@MainActor
func perform() async throws -> some IntentResult & ProvidesDialog {
@@ -368,8 +368,8 @@ struct ClearRecentActivityIntent: AppIntent {
}
struct UnpinResourceIntent: AppIntent {
- static var title: LocalizedStringResource = "Unpin Resource"
- static var description = IntentDescription("Removes a pinned resource from the Hutch Home tab.")
+ static let title: LocalizedStringResource = "Unpin Resource"
+ static let description = IntentDescription("Removes a pinned resource from the Hutch Home tab.")
@Parameter(title: "Pinned Resource")
var pinnedResource: PinnedResourceEntity
diff --git a/Hutch/Extensions/Date+Relative.swift b/Hutch/Extensions/Date+Relative.swift
index f06ba59..b6c28ed 100644
--- a/Hutch/Extensions/Date+Relative.swift
+++ b/Hutch/Extensions/Date+Relative.swift
@@ -1,6 +1,6 @@
import Foundation
-extension Date {
+nonisolated extension Date {
/// A short relative description like "2h ago", "3d ago", or "Jan 5, 2025".
var relativeDescription: String {
let formatter = RelativeDateTimeFormatter()
diff --git a/Hutch/Extensions/DateFormatter+SRHT.swift b/Hutch/Extensions/DateFormatter+SRHT.swift
index c017b11..a06b58a 100644
--- a/Hutch/Extensions/DateFormatter+SRHT.swift
+++ b/Hutch/Extensions/DateFormatter+SRHT.swift
@@ -1,6 +1,6 @@
import Foundation
-extension DateFormatter {
+nonisolated extension DateFormatter {
/// Formatter for the sr.ht `Time` scalar: `%Y-%m-%dT%H:%M:%SZ` (UTC).
static let srht: DateFormatter = {
let formatter = DateFormatter()
diff --git a/Hutch/Extensions/Foundation+Sendable.swift b/Hutch/Extensions/Foundation+Sendable.swift
new file mode 100644
index 0000000..9a12aa6
--- /dev/null
+++ b/Hutch/Extensions/Foundation+Sendable.swift
@@ -0,0 +1,7 @@
+import Foundation
+
+// `UserDefaults` is documented as thread-safe but is not marked `Sendable` by
+// Foundation. Hutch passes it into account sessions and the various per-account
+// stores, all of which need to cross concurrency boundaries. The retroactive
+// unchecked conformance reflects the documented thread-safety.
+extension UserDefaults: @retroactive @unchecked Sendable {}
diff --git a/Hutch/Extensions/SRHTWebURL.swift b/Hutch/Extensions/SRHTWebURL.swift
index fc5a1d5..5715ce4 100644
--- a/Hutch/Extensions/SRHTWebURL.swift
+++ b/Hutch/Extensions/SRHTWebURL.swift
@@ -1,6 +1,6 @@
import Foundation
-enum SRHTWebURL {
+nonisolated enum SRHTWebURL {
static let chat = URL(string: "https://chat.sr.ht")!
static let status = URL(string: "https://status.sr.ht")!
/// RSS/Atom feed for status incidents (HTML snapshot uses `status`).
diff --git a/Hutch/Models/AccountEntry.swift b/Hutch/Models/AccountEntry.swift
index 551cd19..cfc2f62 100644
--- a/Hutch/Models/AccountEntry.swift
+++ b/Hutch/Models/AccountEntry.swift
@@ -1,7 +1,7 @@
import Foundation
/// A stored sr.ht account (token + resolved username).
-struct AccountEntry: Codable, Identifiable, Equatable, Sendable {
+nonisolated struct AccountEntry: Codable, Identifiable, Equatable, Sendable {
let id: String
var username: String
var token: String
diff --git a/Hutch/Models/ArtifactInfo.swift b/Hutch/Models/ArtifactInfo.swift
index dbe9d96..6418ba8 100644
--- a/Hutch/Models/ArtifactInfo.swift
+++ b/Hutch/Models/ArtifactInfo.swift
@@ -1,7 +1,7 @@
import Foundation
/// An artifact with its parent reference name, used in the artifacts tab.
-struct ArtifactInfo: Codable, Sendable, Identifiable {
+nonisolated struct ArtifactInfo: Codable, Sendable, Identifiable {
let id: Int
let filename: String
let checksum: String
@@ -9,13 +9,13 @@ struct ArtifactInfo: Codable, Sendable, Identifiable {
let url: URL
}
-struct ArtifactPage: Codable, Sendable {
+nonisolated struct ArtifactPage: Codable, Sendable {
let results: [ArtifactInfo]
let cursor: String?
}
/// A reference (tag) that has associated artifacts.
-struct ReferenceWithArtifacts: Codable, Sendable, Identifiable {
+nonisolated struct ReferenceWithArtifacts: Codable, Sendable, Identifiable {
var id: String { name }
let name: String
let artifacts: [ArtifactInfo]
diff --git a/Hutch/Models/Builds.swift b/Hutch/Models/Builds.swift
index 3926888..48a9c91 100644
--- a/Hutch/Models/Builds.swift
+++ b/Hutch/Models/Builds.swift
@@ -3,7 +3,7 @@ import Foundation
// MARK: - Enums
/// Status of a build job.
-enum JobStatus: String, Codable, Sendable {
+nonisolated enum JobStatus: String, Codable, Sendable {
case pending = "PENDING"
case queued = "QUEUED"
case running = "RUNNING"
@@ -30,7 +30,7 @@ enum JobStatus: String, Codable, Sendable {
}
/// Status of a single build task within a job.
-enum TaskStatus: String, Codable, Sendable {
+nonisolated enum TaskStatus: String, Codable, Sendable {
case pending = "PENDING"
case running = "RUNNING"
case success = "SUCCESS"
@@ -41,7 +41,7 @@ enum TaskStatus: String, Codable, Sendable {
// MARK: - Build Task
/// A single task within a build job.
-struct BuildTask: Codable, Sendable, Identifiable, Equatable {
+nonisolated struct BuildTask: Codable, Sendable, Identifiable, Equatable {
private(set) var ordinal: Int?
let name: String
let status: TaskStatus
@@ -74,7 +74,7 @@ struct BuildTask: Codable, Sendable, Identifiable, Equatable {
// MARK: - Job Summary (for list view)
/// Lightweight job model matching the fields returned by the jobs list query.
-struct JobSummary: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct JobSummary: Codable, Sendable, Identifiable, Hashable {
let id: Int
let created: Date
let updated: Date
@@ -103,7 +103,7 @@ struct JobSummary: Codable, Sendable, Identifiable, Hashable {
}
/// Minimal task info for the list query.
-struct JobTaskSummary: Codable, Sendable, Hashable {
+nonisolated struct JobTaskSummary: Codable, Sendable, Hashable {
let name: String
let status: TaskStatus
}
@@ -111,7 +111,7 @@ struct JobTaskSummary: Codable, Sendable, Hashable {
// MARK: - Job Detail (for detail view)
/// Full job model with all fields for the detail view.
-struct JobDetail: Codable, Sendable, Equatable {
+nonisolated struct JobDetail: Codable, Sendable, Equatable {
let id: Int
let created: Date
let updated: Date
@@ -128,7 +128,7 @@ struct JobDetail: Codable, Sendable, Equatable {
}
/// A downloadable artifact emitted by a build job.
-struct BuildArtifact: Codable, Sendable, Identifiable, Equatable {
+nonisolated struct BuildArtifact: Codable, Sendable, Identifiable, Equatable {
let id: Int
let created: Date
let path: String
@@ -145,14 +145,14 @@ struct BuildArtifact: Codable, Sendable, Identifiable, Equatable {
}
/// The log associated with a build job.
-struct BuildLog: Codable, Sendable, Equatable {
+nonisolated struct BuildLog: Codable, Sendable, Equatable {
let fullURL: String
}
// MARK: - Job Group
/// A group of related build jobs.
-struct JobGroup: Codable, Sendable, Identifiable {
+nonisolated struct JobGroup: Codable, Sendable, Identifiable {
let id: Int
let created: Date
let note: String?
diff --git a/Hutch/Models/CommitDetail.swift b/Hutch/Models/CommitDetail.swift
index c95199d..962d2a4 100644
--- a/Hutch/Models/CommitDetail.swift
+++ b/Hutch/Models/CommitDetail.swift
@@ -1,7 +1,7 @@
import Foundation
/// Full commit detail returned by the revparse_single query.
-struct CommitDetail: Codable, Sendable, Identifiable {
+nonisolated struct CommitDetail: Codable, Sendable, Identifiable {
let id: String
let shortId: String
let author: CommitAuthor
@@ -26,39 +26,39 @@ struct CommitDetail: Codable, Sendable, Identifiable {
}
}
-struct CommitTrailer: Codable, Sendable, Identifiable {
+nonisolated struct CommitTrailer: Codable, Sendable, Identifiable {
var id: String { "\(name):\(value)" }
let name: String
let value: String
}
-struct ParentCommit: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct ParentCommit: Codable, Sendable, Identifiable, Hashable {
let id: String
let shortId: String
let author: ParentAuthor
}
-struct ParentAuthor: Codable, Sendable, Hashable {
+nonisolated struct ParentAuthor: Codable, Sendable, Hashable {
let name: String
}
-struct CommitTree: Codable, Sendable {
+nonisolated struct CommitTree: Codable, Sendable {
let entries: CommitTreeEntries
}
-struct CommitTreeEntries: Codable, Sendable {
+nonisolated struct CommitTreeEntries: Codable, Sendable {
let results: [CommitTreeEntry]
let cursor: String?
}
-struct CommitTreeEntry: Codable, Sendable, Identifiable {
+nonisolated struct CommitTreeEntry: Codable, Sendable, Identifiable {
let id: String
let name: String
let mode: Int
let object: CommitTreeObject?
}
-struct CommitTreeObject: Codable, Sendable {
+nonisolated struct CommitTreeObject: Codable, Sendable {
let type: String?
let id: String?
let shortId: String?
diff --git a/Hutch/Models/CommitSummary.swift b/Hutch/Models/CommitSummary.swift
index 88c3e7d..99ed233 100644
--- a/Hutch/Models/CommitSummary.swift
+++ b/Hutch/Models/CommitSummary.swift
@@ -2,7 +2,7 @@ import Foundation
/// Lightweight commit model for list views. Matches the subset of fields
/// returned by the repository log query.
-struct CommitSummary: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct CommitSummary: Codable, Sendable, Identifiable, Hashable {
let id: String
let shortId: String
let author: CommitAuthor
@@ -15,7 +15,7 @@ struct CommitSummary: Codable, Sendable, Identifiable, Hashable {
}
/// A compact author representation used in commit list responses.
-struct CommitAuthor: Codable, Sendable, Hashable {
+nonisolated struct CommitAuthor: Codable, Sendable, Hashable {
let name: String
let email: String?
let time: Date
diff --git a/Hutch/Models/ContributionCalendar.swift b/Hutch/Models/ContributionCalendar.swift
index a429fa2..cf87ca7 100644
--- a/Hutch/Models/ContributionCalendar.swift
+++ b/Hutch/Models/ContributionCalendar.swift
@@ -1,6 +1,6 @@
import Foundation
-struct ContributionCalendarResponse: Decodable, Sendable, Hashable {
+nonisolated struct ContributionCalendarResponse: Decodable, Sendable, Hashable {
let actor: String
let from: Date
let to: Date
@@ -9,7 +9,7 @@ struct ContributionCalendarResponse: Decodable, Sendable, Hashable {
let indexingState: ContributionIndexingState
let days: [ContributionDay]
- enum CodingKeys: String, CodingKey {
+ nonisolated enum CodingKeys: String, CodingKey {
case actor
case from
case to
@@ -59,14 +59,14 @@ struct ContributionCalendarResponse: Decodable, Sendable, Hashable {
}
}
-struct ContributionDay: Decodable, Sendable, Hashable, Identifiable {
+nonisolated struct ContributionDay: Decodable, Sendable, Hashable, Identifiable {
var id: Date { date }
let date: Date
let count: Int
let score: Double
- enum CodingKeys: String, CodingKey {
+ nonisolated enum CodingKeys: String, CodingKey {
case date
case count
case score
@@ -90,7 +90,7 @@ struct ContributionDay: Decodable, Sendable, Hashable, Identifiable {
}
}
-struct ContributionStatsResponse: Decodable, Sendable, Hashable {
+nonisolated struct ContributionStatsResponse: Decodable, Sendable, Hashable {
let actor: String
let from: Date
let to: Date
@@ -103,7 +103,7 @@ struct ContributionStatsResponse: Decodable, Sendable, Hashable {
let longestStreak: Int
let currentStreak: Int
- enum CodingKeys: String, CodingKey {
+ nonisolated enum CodingKeys: String, CodingKey {
case actor
case from
case to
@@ -117,7 +117,7 @@ struct ContributionStatsResponse: Decodable, Sendable, Hashable {
case currentStreak = "current_streak"
}
- struct StatsWindow: Sendable, Hashable {
+ nonisolated struct StatsWindow: Sendable, Hashable {
let actor: String
let from: Date
let to: Date
@@ -126,7 +126,7 @@ struct ContributionStatsResponse: Decodable, Sendable, Hashable {
let indexingState: ContributionIndexingState
}
- struct StatsTotals: Sendable, Hashable {
+ nonisolated struct StatsTotals: Sendable, Hashable {
let totalEvents: Int
let totalScore: Double
let activeDays: Int
@@ -181,13 +181,13 @@ struct ContributionStatsResponse: Decodable, Sendable, Hashable {
}
}
-enum ContributionIndexingState: String, Codable, Sendable, Hashable {
+nonisolated enum ContributionIndexingState: String, Codable, Sendable, Hashable {
case pending
case indexed
case error
}
-enum ContributionIntensity: Int, Sendable, CaseIterable {
+nonisolated enum ContributionIntensity: Int, Sendable, CaseIterable {
case empty = 0
case level1 = 1
case level2 = 2
@@ -210,12 +210,12 @@ enum ContributionIntensity: Int, Sendable, CaseIterable {
}
}
-struct ContributionWeek: Sendable, Hashable {
+nonisolated struct ContributionWeek: Sendable, Hashable {
let startDate: Date
let days: [ContributionDay]
}
-enum ContributionCalendarLayout {
+nonisolated enum ContributionCalendarLayout {
static func weekColumns(
from days: [ContributionDay],
calendar: Calendar = .contributionCalendar
@@ -241,7 +241,7 @@ enum ContributionCalendarLayout {
}
}
-enum ContributionDateParser {
+nonisolated enum ContributionDateParser {
static func parse(_ rawValue: String) -> Date? {
let parts = rawValue.split(separator: "-", omittingEmptySubsequences: false)
guard
@@ -348,7 +348,7 @@ enum ContributionDateParser {
}
}
-extension Calendar {
+nonisolated extension Calendar {
static var contributionCalendar: Calendar {
var calendar = Calendar(identifier: .gregorian)
calendar.firstWeekday = 1
diff --git a/Hutch/Models/Git.swift b/Hutch/Models/Git.swift
index 26a5e2d..ae74f19 100644
--- a/Hutch/Models/Git.swift
+++ b/Hutch/Models/Git.swift
@@ -3,14 +3,14 @@ import Foundation
// MARK: - Enums
/// Repository visibility level.
-enum Visibility: String, Codable, Sendable {
+nonisolated enum Visibility: String, Codable, Sendable {
case publicVisibility = "PUBLIC"
case unlisted = "UNLISTED"
case privateVisibility = "PRIVATE"
}
/// Repository access mode.
-enum AccessMode: String, Codable, Sendable, CaseIterable {
+nonisolated enum AccessMode: String, Codable, Sendable, CaseIterable {
case ro = "RO"
case rw = "RW"
}
@@ -19,14 +19,14 @@ enum AccessMode: String, Codable, Sendable, CaseIterable {
/// The `Entity` GraphQL interface from git.sr.ht. Represents the owner of a
/// resource (typically a user).
-struct Entity: Codable, Sendable, Hashable {
+nonisolated struct Entity: Codable, Sendable, Hashable {
let canonicalName: String
}
// MARK: - Repository
/// A git repository from git.sr.ht.
-struct Repository: Codable, Sendable, Identifiable {
+nonisolated struct Repository: Codable, Sendable, Identifiable {
let id: Int
let created: Date
let updated: Date
@@ -37,7 +37,7 @@ struct Repository: Codable, Sendable, Identifiable {
let accessMode: AccessMode
let owner: Entity
- enum CodingKeys: String, CodingKey {
+ nonisolated enum CodingKeys: String, CodingKey {
case id, created, updated, name, description, visibility, readme
case accessMode = "access"
case owner
@@ -47,7 +47,7 @@ struct Repository: Codable, Sendable, Identifiable {
// MARK: - Signature
/// A Git commit/tag signature (author or committer).
-struct Signature: Codable, Sendable {
+nonisolated struct Signature: Codable, Sendable {
let name: String
let email: String
let time: Date
@@ -56,7 +56,7 @@ struct Signature: Codable, Sendable {
// MARK: - Trailer
/// A Git commit trailer (e.g. "Signed-off-by", "Co-authored-by").
-struct Trailer: Codable, Sendable {
+nonisolated struct Trailer: Codable, Sendable {
let name: String
let value: String
}
@@ -64,7 +64,7 @@ struct Trailer: Codable, Sendable {
// MARK: - Commit
/// A Git commit from git.sr.ht.
-struct Commit: Codable, Sendable, Identifiable {
+nonisolated struct Commit: Codable, Sendable, Identifiable {
let id: String
let shortId: String
let author: Signature
@@ -77,13 +77,13 @@ struct Commit: Codable, Sendable, Identifiable {
// MARK: - Reference
/// A Git reference (branch or tag name).
-struct Reference: Codable, Sendable, Hashable {
+nonisolated struct Reference: Codable, Sendable, Hashable {
let name: String
let target: String?
}
/// A Git reference enriched with the date of its tip commit or tag, for display in the Refs tab.
-struct ReferenceDetail: Sendable, Hashable {
+nonisolated struct ReferenceDetail: Sendable, Hashable {
let name: String
let target: String?
let date: Date?
@@ -92,7 +92,7 @@ struct ReferenceDetail: Sendable, Hashable {
// MARK: - TreeEntry
/// An entry in a Git tree (file or directory).
-struct TreeEntry: Codable, Sendable, Identifiable {
+nonisolated struct TreeEntry: Codable, Sendable, Identifiable {
let id: String
let name: String
let mode: Int?
@@ -104,41 +104,41 @@ struct TreeEntry: Codable, Sendable, Identifiable {
/// Both TextBlob and BinaryBlob have type == "BLOB"; they are
/// distinguished by the presence of the "text" key (TextBlob) vs
/// the "content" key (BinaryBlob).
-enum GitObject: Sendable {
+nonisolated enum GitObject: Sendable {
case tree(GitTree)
case textBlob(GitTextBlob)
case binaryBlob(GitBinaryBlob)
case unknown
}
-struct GitTree: Codable, Sendable {
+nonisolated struct GitTree: Codable, Sendable {
let id: String?
let shortId: String?
let entries: GitTreeEntryPage?
}
-struct GitTextBlob: Codable, Sendable {
+nonisolated struct GitTextBlob: Codable, Sendable {
let id: String?
let shortId: String?
let text: String?
let size: Int?
}
-struct GitBinaryBlob: Codable, Sendable {
+nonisolated struct GitBinaryBlob: Codable, Sendable {
let id: String?
let shortId: String?
let size: Int?
let content: String?
}
-struct GitTreeEntryPage: Codable, Sendable {
+nonisolated struct GitTreeEntryPage: Codable, Sendable {
let results: [TreeEntry]
let cursor: String?
}
// MARK: - GitObject Codable
-extension GitObject: Codable {
+nonisolated extension GitObject: Codable {
private enum CodingKeys: String, CodingKey {
case type, id, shortId, entries, text, size, content
case typename = "__typename"
@@ -211,7 +211,7 @@ extension GitObject: Codable {
}
/// Convenience helpers for checking object type.
-extension GitObject {
+nonisolated extension GitObject {
var isTree: Bool {
if case .tree = self { return true }
return false
@@ -230,7 +230,7 @@ extension GitObject {
// MARK: - Tag
/// An annotated Git tag from git.sr.ht.
-struct Tag: Codable, Sendable, Identifiable {
+nonisolated struct Tag: Codable, Sendable, Identifiable {
let id: String
let shortId: String
let name: String
@@ -241,7 +241,7 @@ struct Tag: Codable, Sendable, Identifiable {
// MARK: - Artifact
/// A release artifact attached to a Git tag.
-struct Artifact: Codable, Sendable, Identifiable {
+nonisolated struct Artifact: Codable, Sendable, Identifiable {
let id: Int
let created: Date
let filename: String
diff --git a/Hutch/Models/Inbox.swift b/Hutch/Models/Inbox.swift
index c8e41bb..dfed719 100644
--- a/Hutch/Models/Inbox.swift
+++ b/Hutch/Models/Inbox.swift
@@ -1,6 +1,6 @@
import Foundation
-struct InboxThreadSummary: Identifiable, Hashable, Sendable {
+nonisolated struct InboxThreadSummary: Identifiable, Hashable, Sendable {
let rootEmailID: Int
let rootMessageID: String
let threadRootEmailIDs: [Int]
@@ -76,7 +76,7 @@ struct InboxThreadSummary: Identifiable, Hashable, Sendable {
}
}
-struct InboxMessage: Identifiable, Hashable, Sendable {
+nonisolated struct InboxMessage: Identifiable, Hashable, Sendable {
let id: Int
let author: Entity
let date: Date
@@ -89,12 +89,12 @@ struct InboxMessage: Identifiable, Hashable, Sendable {
let rawMessageURL: URL?
}
-enum InboxMessageContentBlock: Hashable, Sendable {
+nonisolated enum InboxMessageContentBlock: Hashable, Sendable {
case plainText(String)
case diff(String)
}
-struct InboxThreadDetail: Sendable {
+nonisolated struct InboxThreadDetail: Sendable {
let id: String
let rootEmailID: Int
let rootMessageID: String
@@ -114,7 +114,7 @@ struct InboxThreadDetail: Sendable {
}
}
-extension InboxThreadDetail {
+nonisolated extension InboxThreadDetail {
var replyRecipient: String {
"\(listOwner.canonicalName)/\(listName)@lists.sr.ht"
}
@@ -144,7 +144,7 @@ extension InboxThreadDetail {
}
}
-struct MailComposeDraft: Sendable {
+nonisolated struct MailComposeDraft: Sendable {
let recipients: [String]
let ccRecipients: [String]
let subject: String
@@ -155,20 +155,20 @@ struct MailComposeDraft: Sendable {
}
}
-extension MailComposeDraft: Identifiable {}
+nonisolated extension MailComposeDraft: Identifiable {}
-struct InboxMailingListReference: Decodable, Sendable, Hashable, Identifiable {
+nonisolated struct InboxMailingListReference: Decodable, Sendable, Hashable, Identifiable {
let id: Int
let rid: String
let name: String
let owner: Entity
}
-struct InboxPatchPreview: Decodable, Sendable, Hashable {
+nonisolated struct InboxPatchPreview: Decodable, Sendable, Hashable {
let subject: String?
}
-enum InboxReadStateStore {
+nonisolated enum InboxReadStateStore {
private static let key = "InboxThreadLastViewed"
private static let baselineKey = "InboxUnreadBaseline"
diff --git a/Hutch/Models/Meta.swift b/Hutch/Models/Meta.swift
index f52fc76..e0a09de 100644
--- a/Hutch/Models/Meta.swift
+++ b/Hutch/Models/Meta.swift
@@ -3,7 +3,7 @@ import Foundation
// MARK: - User Profile (full)
/// Extended user profile from meta.sr.ht with all fields from the `me` query.
-struct UserProfile: Codable, Sendable {
+nonisolated struct UserProfile: Codable, Sendable {
let username: String
let canonicalName: String
let email: String
@@ -20,7 +20,7 @@ struct UserProfile: Codable, Sendable {
// MARK: - SSH Key
-struct SSHKey: Codable, Sendable, Identifiable {
+nonisolated struct SSHKey: Codable, Sendable, Identifiable {
let id: Int
let comment: String?
let created: Date
@@ -34,27 +34,27 @@ struct SSHKey: Codable, Sendable, Identifiable {
}
}
-struct SSHKeyPage: Codable, Sendable {
+nonisolated struct SSHKeyPage: Codable, Sendable {
let results: [SSHKey]
let cursor: String?
}
// MARK: - PGP Key
-struct PGPKey: Codable, Sendable, Identifiable {
+nonisolated struct PGPKey: Codable, Sendable, Identifiable {
let id: Int
let fingerprint: String
let created: Date
}
-struct PGPKeyPage: Codable, Sendable {
+nonisolated struct PGPKeyPage: Codable, Sendable {
let results: [PGPKey]
let cursor: String?
}
// MARK: - Subscription
-struct Subscription: Codable, Sendable {
+nonisolated struct Subscription: Codable, Sendable {
let status: String?
let autorenew: Bool?
let interval: String?
@@ -62,7 +62,7 @@ struct Subscription: Codable, Sendable {
// MARK: - Personal Access Token
-struct PersonalAccessToken: Codable, Sendable, Identifiable {
+nonisolated struct PersonalAccessToken: Codable, Sendable, Identifiable {
let id: Int
let issued: Date
let expires: Date?
@@ -72,7 +72,7 @@ struct PersonalAccessToken: Codable, Sendable, Identifiable {
/// One entry in meta.sr.ht's audit log: a security-relevant action on the
/// account, with the address it came from.
-struct AuditLogEntry: Codable, Sendable, Identifiable {
+nonisolated struct AuditLogEntry: Codable, Sendable, Identifiable {
let id: Int
let created: Date
let ipAddress: String
diff --git a/Hutch/Models/Paste.swift b/Hutch/Models/Paste.swift
index b4c8f18..a8bf926 100644
--- a/Hutch/Models/Paste.swift
+++ b/Hutch/Models/Paste.swift
@@ -1,6 +1,6 @@
import Foundation
-struct PasteFile: Codable, Sendable, Hashable, Identifiable {
+nonisolated struct PasteFile: Codable, Sendable, Hashable, Identifiable {
let filename: String?
let hash: String
let contents: URL?
@@ -8,7 +8,7 @@ struct PasteFile: Codable, Sendable, Hashable, Identifiable {
var id: String { hash }
}
-struct Paste: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct Paste: Codable, Sendable, Identifiable, Hashable {
let id: String
let created: Date
let visibility: Visibility
@@ -16,7 +16,7 @@ struct Paste: Codable, Sendable, Identifiable, Hashable {
let user: Entity
}
-struct PasteUploadDraft: Identifiable, Equatable, Sendable {
+nonisolated struct PasteUploadDraft: Identifiable, Equatable, Sendable {
let id: UUID
var filename: String
var contents: String
diff --git a/Hutch/Models/Patchset.swift b/Hutch/Models/Patchset.swift
index 330bbbc..cc10b35 100644
--- a/Hutch/Models/Patchset.swift
+++ b/Hutch/Models/Patchset.swift
@@ -1,7 +1,7 @@
import Foundation
/// Review state of a patchset on lists.sr.ht.
-enum PatchsetStatus: String, Codable, Sendable, CaseIterable {
+nonisolated enum PatchsetStatus: String, Codable, Sendable, CaseIterable {
case unknown = "UNKNOWN"
case proposed = "PROPOSED"
case needsRevision = "NEEDS_REVISION"
@@ -55,7 +55,7 @@ enum PatchsetStatus: String, Codable, Sendable, CaseIterable {
/// A patchset as it appears in a mailing list listing, derived from the thread's
/// root email rather than a dedicated patchsets query — `MailingList` exposes no
/// such field.
-struct PatchsetSummary: Identifiable, Hashable, Sendable {
+nonisolated struct PatchsetSummary: Identifiable, Hashable, Sendable {
let id: Int
let subject: String
let version: Int
@@ -70,7 +70,7 @@ struct PatchsetSummary: Identifiable, Hashable, Sendable {
}
/// One email within a patchset: either the cover letter or a single patch.
-struct PatchsetEmail: Identifiable, Hashable, Sendable {
+nonisolated struct PatchsetEmail: Identifiable, Hashable, Sendable {
let id: Int
let subject: String
let date: Date?
@@ -88,13 +88,13 @@ struct PatchsetEmail: Identifiable, Hashable, Sendable {
}
/// A build or check reported against a patchset.
-struct PatchsetToolResult: Identifiable, Hashable, Sendable {
+nonisolated struct PatchsetToolResult: Identifiable, Hashable, Sendable {
let id: Int
let icon: PatchsetToolIcon
let details: String
}
-enum PatchsetToolIcon: String, Codable, Sendable {
+nonisolated enum PatchsetToolIcon: String, Codable, Sendable {
case pending = "PENDING"
case waiting = "WAITING"
case success = "SUCCESS"
@@ -112,7 +112,7 @@ enum PatchsetToolIcon: String, Codable, Sendable {
}
/// A patchset with its cover letter, patches, and review context.
-struct PatchsetDetail: Sendable {
+nonisolated struct PatchsetDetail: Sendable {
let id: Int
let created: Date
let updated: Date
diff --git a/Hutch/Models/Project.swift b/Hutch/Models/Project.swift
index 8f5aa68..bd1a688 100644
--- a/Hutch/Models/Project.swift
+++ b/Hutch/Models/Project.swift
@@ -1,7 +1,7 @@
import Foundation
-struct Project: Identifiable, Hashable, Sendable {
- struct MailingList: Identifiable, Hashable, Sendable {
+nonisolated struct Project: Identifiable, Hashable, Sendable {
+ nonisolated struct MailingList: Identifiable, Hashable, Sendable {
let id: String
let name: String
let description: String?
@@ -22,8 +22,8 @@ struct Project: Identifiable, Hashable, Sendable {
}
}
- struct SourceRepo: Identifiable, Hashable, Sendable {
- enum RepoType: String, Decodable, Sendable {
+ nonisolated struct SourceRepo: Identifiable, Hashable, Sendable {
+ nonisolated enum RepoType: String, Decodable, Sendable {
case git = "GIT"
case hg = "HG"
@@ -47,7 +47,7 @@ struct Project: Identifiable, Hashable, Sendable {
}
}
- struct Tracker: Identifiable, Hashable, Sendable {
+ nonisolated struct Tracker: Identifiable, Hashable, Sendable {
let id: String
let name: String
let description: String?
@@ -72,7 +72,7 @@ struct Project: Identifiable, Hashable, Sendable {
let isFullyLoaded: Bool
/// Identity and display fields for a project.
- struct Metadata: Sendable, Hashable {
+ nonisolated struct Metadata: Sendable, Hashable {
let id: String
let name: String
let description: String?
@@ -82,7 +82,7 @@ struct Project: Identifiable, Hashable, Sendable {
let updated: Date
}
- struct Resources: Sendable, Hashable {
+ nonisolated struct Resources: Sendable, Hashable {
let mailingLists: [MailingList]
let sources: [SourceRepo]
let trackers: [Tracker]
@@ -171,7 +171,7 @@ struct Project: Identifiable, Hashable, Sendable {
}
}
-extension Project.MailingList {
+nonisolated extension Project.MailingList {
var displayName: String {
Project.normalizedText(name) ?? "Untitled Mailing List"
}
@@ -185,7 +185,7 @@ extension Project.MailingList {
}
}
-extension Project.SourceRepo {
+nonisolated extension Project.SourceRepo {
var displayName: String {
Project.normalizedText(name) ?? "Untitled Repository"
}
@@ -203,7 +203,7 @@ extension Project.SourceRepo {
}
}
-extension Project.Tracker {
+nonisolated extension Project.Tracker {
var displayName: String {
Project.normalizedText(name) ?? "Untitled Tracker"
}
@@ -221,13 +221,13 @@ extension Project.Tracker {
}
}
-extension String {
+nonisolated extension String {
var srhtUsername: String {
hasPrefix("~") ? String(dropFirst()) : self
}
}
-extension Visibility {
+nonisolated extension Visibility {
var displayName: String {
switch self {
case .publicVisibility:
diff --git a/Hutch/Models/RepositoryACL.swift b/Hutch/Models/RepositoryACL.swift
index e5f5eb1..e5d93f6 100644
--- a/Hutch/Models/RepositoryACL.swift
+++ b/Hutch/Models/RepositoryACL.swift
@@ -1,12 +1,12 @@
import Foundation
-struct RepositoryACLEntry: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct RepositoryACLEntry: Codable, Sendable, Identifiable, Hashable {
let id: Int
let mode: AccessMode
let entity: Entity
}
-extension AccessMode {
+nonisolated extension AccessMode {
var shortLabel: String { rawValue }
var displayName: String {
diff --git a/Hutch/Models/RepositorySummary.swift b/Hutch/Models/RepositorySummary.swift
index b41589b..40ec7a4 100644
--- a/Hutch/Models/RepositorySummary.swift
+++ b/Hutch/Models/RepositorySummary.swift
@@ -3,7 +3,7 @@ import Foundation
/// Lightweight repository model matching the fields returned by the
/// repositories list query. Avoids optionalizing all fields on the full
/// `Repository` model.
-struct RepositorySummary: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct RepositorySummary: Codable, Sendable, Identifiable, Hashable {
let id: Int
/// GraphQL resource identifier used by `repository(rid:)` queries.
let rid: String
@@ -15,13 +15,13 @@ struct RepositorySummary: Codable, Sendable, Identifiable, Hashable {
let owner: Entity
let head: Reference?
- enum CodingKeys: String, CodingKey {
+ nonisolated enum CodingKeys: String, CodingKey {
case id, rid, service, name, description, visibility, updated, owner
case head = "HEAD"
}
/// Grouped initializer fields (single parameter keeps APIs explicit without exceeding parameter-count limits).
- struct Fields: Sendable, Hashable {
+ nonisolated struct Fields: Sendable, Hashable {
let id: Int
let rid: String
let service: SRHTService
@@ -59,7 +59,7 @@ struct RepositorySummary: Codable, Sendable, Identifiable, Hashable {
}
}
-extension RepositorySummary {
+nonisolated extension RepositorySummary {
var defaultBranchName: String? {
head?.name.replacingOccurrences(of: "refs/heads/", with: "")
}
diff --git a/Hutch/Models/SystemStatusModels.swift b/Hutch/Models/SystemStatusModels.swift
index 9a664b0..ddd659c 100644
--- a/Hutch/Models/SystemStatusModels.swift
+++ b/Hutch/Models/SystemStatusModels.swift
@@ -1,6 +1,6 @@
import Foundation
-enum StatusLevel: String, Codable, Sendable {
+nonisolated enum StatusLevel: String, Codable, Sendable {
case operational
case degraded
case majorOutage
@@ -32,7 +32,7 @@ enum StatusLevel: String, Codable, Sendable {
}
}
-struct StatusServiceState: Identifiable, Hashable, Codable, Sendable {
+nonisolated struct StatusServiceState: Identifiable, Hashable, Codable, Sendable {
let id: String
let name: String
let slug: String?
@@ -40,7 +40,7 @@ struct StatusServiceState: Identifiable, Hashable, Codable, Sendable {
let description: String?
}
-struct StatusIncident: Identifiable, Hashable, Codable, Sendable {
+nonisolated struct StatusIncident: Identifiable, Hashable, Codable, Sendable {
let id: String
let title: String
let summary: String?
@@ -50,7 +50,7 @@ struct StatusIncident: Identifiable, Hashable, Codable, Sendable {
let isActive: Bool?
}
-struct SystemStatusSnapshot: Hashable, Codable, Sendable {
+nonisolated struct SystemStatusSnapshot: Hashable, Codable, Sendable {
let services: [StatusServiceState]
let activeIncidents: [StatusIncident]
let lastUpdated: Date
@@ -78,7 +78,7 @@ struct SystemStatusSnapshot: Hashable, Codable, Sendable {
}
}
-struct SystemStatusPageData: Sendable {
+nonisolated struct SystemStatusPageData: Sendable {
let snapshot: SystemStatusSnapshot
let recentIncidents: [StatusIncident]
}
diff --git a/Hutch/Models/TicketBulkAction.swift b/Hutch/Models/TicketBulkAction.swift
index dcf7676..ea9cc01 100644
--- a/Hutch/Models/TicketBulkAction.swift
+++ b/Hutch/Models/TicketBulkAction.swift
@@ -1,6 +1,6 @@
import Foundation
-enum TicketBulkActionKind: String, Sendable {
+nonisolated enum TicketBulkActionKind: String, Sendable {
case close
case assign
@@ -23,7 +23,7 @@ enum TicketBulkActionKind: String, Sendable {
}
}
-struct TicketBulkActionResult: Identifiable, Sendable {
+nonisolated struct TicketBulkActionResult: Identifiable, Sendable {
let id = UUID()
let action: TicketBulkActionKind
let totalCount: Int
@@ -82,7 +82,7 @@ struct TicketBulkActionResult: Identifiable, Sendable {
}
}
-struct TicketBulkActionFailure: Sendable {
+nonisolated struct TicketBulkActionFailure: Sendable {
let ticketID: Int
let message: String
}
diff --git a/Hutch/Models/Todo.swift b/Hutch/Models/Todo.swift
index 129d4bf..d2ae889 100644
--- a/Hutch/Models/Todo.swift
+++ b/Hutch/Models/Todo.swift
@@ -3,7 +3,7 @@ import Foundation
// MARK: - Enums
/// Status of a ticket.
-enum TicketStatus: String, Codable, Sendable, CaseIterable {
+nonisolated enum TicketStatus: String, Codable, Sendable, CaseIterable {
case reported = "REPORTED"
case confirmed = "CONFIRMED"
case inProgress = "IN_PROGRESS"
@@ -30,7 +30,7 @@ enum TicketStatus: String, Codable, Sendable, CaseIterable {
}
/// Resolution of a ticket.
-enum TicketResolution: String, Codable, Sendable {
+nonisolated enum TicketResolution: String, Codable, Sendable {
case unresolved = "UNRESOLVED"
case fixed = "FIXED"
case implemented = "IMPLEMENTED"
@@ -64,7 +64,7 @@ enum TicketResolution: String, Codable, Sendable {
}
/// Authenticity of a ticket or comment.
-enum Authenticity: String, Codable, Sendable {
+nonisolated enum Authenticity: String, Codable, Sendable {
case authentic = "AUTHENTIC"
case tampered = "TAMPERED"
case unauthenticated = "UNAUTHENTICATED"
@@ -74,7 +74,7 @@ enum Authenticity: String, Codable, Sendable {
/// A label that can be applied to tickets.
/// Named `TicketLabel` to avoid collision with `SwiftUI.Label`.
-struct TicketLabel: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct TicketLabel: Codable, Sendable, Identifiable, Hashable {
let id: Int
let name: String
let backgroundColor: String
@@ -83,7 +83,7 @@ struct TicketLabel: Codable, Sendable, Identifiable, Hashable {
// MARK: - Tracker ACL
-struct TrackerACLPermissions: Codable, Sendable, Hashable {
+nonisolated struct TrackerACLPermissions: Codable, Sendable, Hashable {
let browse: Bool
let submit: Bool
let comment: Bool
@@ -91,7 +91,7 @@ struct TrackerACLPermissions: Codable, Sendable, Hashable {
let triage: Bool
}
-struct TrackerACL: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct TrackerACL: Codable, Sendable, Identifiable, Hashable {
let id: Int
let created: Date
let entity: Entity
@@ -112,7 +112,7 @@ struct TrackerACL: Codable, Sendable, Identifiable, Hashable {
}
}
-struct DefaultTrackerACL: Codable, Sendable, Hashable {
+nonisolated struct DefaultTrackerACL: Codable, Sendable, Hashable {
let browse: Bool
let submit: Bool
let comment: Bool
@@ -133,7 +133,7 @@ struct DefaultTrackerACL: Codable, Sendable, Hashable {
// MARK: - Tracker
/// A bug tracker from todo.sr.ht.
-struct Tracker: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct Tracker: Codable, Sendable, Identifiable, Hashable {
let id: Int
let created: Date
let updated: Date
@@ -154,7 +154,7 @@ struct Tracker: Codable, Sendable, Identifiable, Hashable {
// MARK: - Tracker Summary (for list view)
/// Lightweight tracker model matching the fields returned by the trackers list query.
-struct TrackerSummary: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct TrackerSummary: Codable, Sendable, Identifiable, Hashable {
let id: Int
/// GraphQL resource identifier used by `tracker(id:)` queries.
let rid: String
@@ -168,7 +168,7 @@ struct TrackerSummary: Codable, Sendable, Identifiable, Hashable {
// MARK: - Ticket Summary (for list view)
/// Lightweight ticket model for the list query.
-struct TicketSummary: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct TicketSummary: Codable, Sendable, Identifiable, Hashable {
let id: Int
let title: String
let status: TicketStatus
@@ -190,7 +190,7 @@ struct TicketSummary: Codable, Sendable, Identifiable, Hashable {
// MARK: - Ticket Detail
/// Full ticket model for the detail view.
-struct TicketDetail: Codable, Sendable {
+nonisolated struct TicketDetail: Codable, Sendable {
let id: Int
let created: Date
let updated: Date
@@ -207,7 +207,7 @@ struct TicketDetail: Codable, Sendable {
// MARK: - Event
/// A timeline event on a ticket. Each event contains one or more changes.
-struct TicketEvent: Codable, Sendable, Identifiable {
+nonisolated struct TicketEvent: Codable, Sendable, Identifiable {
let id: Int
let created: Date
var changes: [EventChange]
@@ -215,7 +215,7 @@ struct TicketEvent: Codable, Sendable, Identifiable {
/// A single change within an event, decoded from the polymorphic EventDetail
/// interface using inline fragments.
-struct EventChange: Codable, Sendable, Identifiable {
+nonisolated struct EventChange: Codable, Sendable, Identifiable {
let id: UUID
let eventType: String
@@ -272,12 +272,12 @@ struct EventChange: Codable, Sendable, Identifiable {
/// Decoded from the `mentioned` field which may be a Ticket (with `id`) or
/// an Entity (with `canonicalName`) depending on the event type.
-struct MentionTarget: Codable, Sendable {
+nonisolated struct MentionTarget: Codable, Sendable {
let id: Int?
let canonicalName: String?
}
/// Label info as returned within a LabelUpdate event change.
-struct EventLabel: Codable, Sendable {
+nonisolated struct EventLabel: Codable, Sendable {
let name: String
}
diff --git a/Hutch/Models/User.swift b/Hutch/Models/User.swift
index 4ab7315..10576ad 100644
--- a/Hutch/Models/User.swift
+++ b/Hutch/Models/User.swift
@@ -1,7 +1,7 @@
import Foundation
/// A Sourcehut user from meta.sr.ht.
-struct User: Decodable, Sendable, Hashable {
+nonisolated struct User: Decodable, Sendable, Hashable {
let id: Int
let created: String?
let updated: String?
diff --git a/Hutch/Views/Lists/MailingListListView.swift b/Hutch/Views/Lists/MailingListListView.swift
index 1b159bf..b6ed2ff 100644
--- a/Hutch/Views/Lists/MailingListListView.swift
+++ b/Hutch/Views/Lists/MailingListListView.swift
@@ -100,7 +100,7 @@ final class MailingListListViewModel {
query: Self.createMailingListMutation,
variables: [
"name": trimmedName,
- "description": trimmedDescription.isEmpty ? nil as String? as Any : trimmedDescription,
+ "description": trimmedDescription.isEmpty ? nil as String? as any Sendable : trimmedDescription,
"visibility": visibility.rawValue
],
responseType: Response.self
diff --git a/Hutch/Views/More/TipStoreViewModel.swift b/Hutch/Views/More/TipStoreViewModel.swift
index bc909c1..043b218 100644
--- a/Hutch/Views/More/TipStoreViewModel.swift
+++ b/Hutch/Views/More/TipStoreViewModel.swift
@@ -32,7 +32,9 @@ final class TipStoreViewModel {
var errorMessage: String?
var statusMessage: String?
- private var transactionUpdatesTask: Task<Void, Never>?
+ // Internal task handle, not observable state; assigned only on the main
+ // actor and read once from the nonisolated deinit.
+ @ObservationIgnored nonisolated(unsafe) private var transactionUpdatesTask: Task<Void, Never>?
init() {
transactionUpdatesTask = Task.detached(priority: .background) {
diff --git a/Hutch/Views/Repositories/RepositoryDetailViewModel.swift b/Hutch/Views/Repositories/RepositoryDetailViewModel.swift
index 82e1592..62a8b12 100644
--- a/Hutch/Views/Repositories/RepositoryDetailViewModel.swift
+++ b/Hutch/Views/Repositories/RepositoryDetailViewModel.swift
@@ -557,7 +557,7 @@ final class RepositoryDetailViewModel {
variables: [
"repoId": repository.id,
"revspec": revspec,
- "file": nil as String? as Any
+ "file": nil as String? as any Sendable
],
file: MultipartUploadFile(
variablePath: "file",
diff --git a/Hutch/Views/Repositories/SyntaxHighlighter.swift b/Hutch/Views/Repositories/SyntaxHighlighter.swift
index 5ae8f58..dfab443 100644
--- a/Hutch/Views/Repositories/SyntaxHighlighter.swift
+++ b/Hutch/Views/Repositories/SyntaxHighlighter.swift
@@ -3,7 +3,7 @@ import Highlightr
import SwiftUI
import UIKit
-enum SyntaxHighlightTheme {
+nonisolated enum SyntaxHighlightTheme {
case light
case dark
@@ -31,7 +31,7 @@ enum SyntaxHighlightTheme {
/// instance must stay on the thread/task that created it. Callers that fail to
/// resolve a language — or hit an unavailable engine — get `nil` and should
/// fall back to plain, escaped text.
-final class SyntaxHighlighter {
+nonisolated final class SyntaxHighlighter {
private let highlightr: Highlightr?
private let supportedLanguages: Set<String>
@@ -208,7 +208,7 @@ final class SyntaxHighlighter {
]
}
-private extension UIColor {
+nonisolated private extension UIColor {
/// `#rrggbb` for HTML inline styles, or `nil` if the color isn't RGB-convertible.
var hexRGBString: String? {
var red: CGFloat = 0
diff --git a/Hutch/Views/Settings/SettingsViewModel.swift b/Hutch/Views/Settings/SettingsViewModel.swift
index 8536e60..2d57ba5 100644
--- a/Hutch/Views/Settings/SettingsViewModel.swift
+++ b/Hutch/Views/Settings/SettingsViewModel.swift
@@ -198,9 +198,9 @@ final class SettingsViewModel {
do {
let input: [String: any Sendable] = [
"email": email,
- "url": url.isEmpty ? nil as String? as Any : url,
- "location": location.isEmpty ? nil as String? as Any : location,
- "bio": bio.isEmpty ? nil as String? as Any : bio
+ "url": url.isEmpty ? nil as String? as any Sendable : url,
+ "location": location.isEmpty ? nil as String? as any Sendable : location,
+ "bio": bio.isEmpty ? nil as String? as any Sendable : bio
]
let result = try await client.execute(
service: .meta,
@@ -243,7 +243,7 @@ final class SettingsViewModel {
do {
// The input variable has avatar set to null; the actual file
// is sent as a separate multipart part per graphql-multipart-request-spec.
- let input: [String: any Sendable] = ["avatar": nil as String? as Any]
+ let input: [String: any Sendable] = ["avatar": nil as String? as any Sendable]
let result = try await client.executeMultipart(
service: .meta,
query: Self.updateUserMutation,
@@ -286,7 +286,7 @@ final class SettingsViewModel {
error = nil
do {
- let input: [String: any Sendable] = ["avatar": nil as String? as Any]
+ let input: [String: any Sendable] = ["avatar": nil as String? as any Sendable]
let result = try await client.execute(
service: .meta,
query: Self.updateUserMutation,
diff --git a/HutchTests/APICacheTests.swift b/HutchTests/APICacheTests.swift
index 24db870..9a93783 100644
--- a/HutchTests/APICacheTests.swift
+++ b/HutchTests/APICacheTests.swift
@@ -3,6 +3,7 @@ import Testing
@testable import Hutch
@Suite(.serialized)
+@MainActor
struct APICacheTests {
private struct Payload: Codable, Sendable, Equatable {
let value: String
diff --git a/HutchTests/AppStateTests.swift b/HutchTests/AppStateTests.swift
index b7e1755..49baa17 100644
--- a/HutchTests/AppStateTests.swift
+++ b/HutchTests/AppStateTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct AppStateTests {
@Test
diff --git a/HutchTests/BuildListViewModelTests.swift b/HutchTests/BuildListViewModelTests.swift
index 8ee5b81..9b64621 100644
--- a/HutchTests/BuildListViewModelTests.swift
+++ b/HutchTests/BuildListViewModelTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct BuildListViewModelTests {
@Test
diff --git a/HutchTests/BuildTaskLogSearchTests.swift b/HutchTests/BuildTaskLogSearchTests.swift
index 4cb9984..8c7ddc2 100644
--- a/HutchTests/BuildTaskLogSearchTests.swift
+++ b/HutchTests/BuildTaskLogSearchTests.swift
@@ -1,6 +1,7 @@
import Testing
@testable import Hutch
+@MainActor
struct BuildTaskLogSearchTests {
@Test
diff --git a/HutchTests/BuildTaskTests.swift b/HutchTests/BuildTaskTests.swift
index 2e2a1e0..c7baae7 100644
--- a/HutchTests/BuildTaskTests.swift
+++ b/HutchTests/BuildTaskTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct BuildTaskTests {
@Test
diff --git a/HutchTests/BundleUserAgentTests.swift b/HutchTests/BundleUserAgentTests.swift
index c1a20b8..a4bda55 100644
--- a/HutchTests/BundleUserAgentTests.swift
+++ b/HutchTests/BundleUserAgentTests.swift
@@ -40,6 +40,7 @@ private final class CapturingURLProtocol: URLProtocol, @unchecked Sendable {
/// Tests are serialized because CapturingURLProtocol uses shared static state.
@Suite(.serialized)
+@MainActor
struct BundleUserAgentTests {
// MARK: Bundle extension
diff --git a/HutchTests/ContributionCalendarTests.swift b/HutchTests/ContributionCalendarTests.swift
index 0328ad5..e5d625a 100644
--- a/HutchTests/ContributionCalendarTests.swift
+++ b/HutchTests/ContributionCalendarTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct ContributionCalendarTests {
@Test
diff --git a/HutchTests/DeepLinkTests.swift b/HutchTests/DeepLinkTests.swift
index cdc5bab..dc4f605 100644
--- a/HutchTests/DeepLinkTests.swift
+++ b/HutchTests/DeepLinkTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct DeepLinkTests {
@Test
diff --git a/HutchTests/GitObjectTests.swift b/HutchTests/GitObjectTests.swift
index 547c6f3..3546d11 100644
--- a/HutchTests/GitObjectTests.swift
+++ b/HutchTests/GitObjectTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct GitObjectTests {
@Test
diff --git a/HutchTests/HomeViewModelTests.swift b/HutchTests/HomeViewModelTests.swift
index 851b520..a050931 100644
--- a/HutchTests/HomeViewModelTests.swift
+++ b/HutchTests/HomeViewModelTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct HomeViewModelTests {
@Test
diff --git a/HutchTests/HutchTests.swift b/HutchTests/HutchTests.swift
index c681efa..98c68d3 100644
--- a/HutchTests/HutchTests.swift
+++ b/HutchTests/HutchTests.swift
@@ -7,6 +7,7 @@
import Testing
+@MainActor
struct HutchTests {
@Test func example() async throws {
diff --git a/HutchTests/InboxViewModelTests.swift b/HutchTests/InboxViewModelTests.swift
index 5780b6a..9c35001 100644
--- a/HutchTests/InboxViewModelTests.swift
+++ b/HutchTests/InboxViewModelTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct InboxViewModelTests {
@Test
diff --git a/HutchTests/LookupHistoryStoreTests.swift b/HutchTests/LookupHistoryStoreTests.swift
index 414d016..af7c69b 100644
--- a/HutchTests/LookupHistoryStoreTests.swift
+++ b/HutchTests/LookupHistoryStoreTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct LookupHistoryStoreTests {
@Test
diff --git a/HutchTests/MailingListActivityTests.swift b/HutchTests/MailingListActivityTests.swift
index 46208cb..61a7e77 100644
--- a/HutchTests/MailingListActivityTests.swift
+++ b/HutchTests/MailingListActivityTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct MailingListActivityTests {
@Test
diff --git a/HutchTests/ManPageCatalogTests.swift b/HutchTests/ManPageCatalogTests.swift
index df995f8..e6bd2fc 100644
--- a/HutchTests/ManPageCatalogTests.swift
+++ b/HutchTests/ManPageCatalogTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct ManPageCatalogTests {
@Test
func loadsBundledCatalog() {
diff --git a/HutchTests/PatchsetTests.swift b/HutchTests/PatchsetTests.swift
index 945131f..e181f14 100644
--- a/HutchTests/PatchsetTests.swift
+++ b/HutchTests/PatchsetTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct PatchsetStatusTests {
@Test
@@ -46,6 +47,7 @@ struct PatchsetStatusTests {
}
}
+@MainActor
struct PatchsetSummaryTests {
@Test
diff --git a/HutchTests/ProjectFormTests.swift b/HutchTests/ProjectFormTests.swift
index a4b4dfa..3e1fffc 100644
--- a/HutchTests/ProjectFormTests.swift
+++ b/HutchTests/ProjectFormTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct ProjectFormTests {
@Test
func parsesTagsSplittingAndTrimming() {
diff --git a/HutchTests/ProjectPinStoreTests.swift b/HutchTests/ProjectPinStoreTests.swift
index 13df539..c040868 100644
--- a/HutchTests/ProjectPinStoreTests.swift
+++ b/HutchTests/ProjectPinStoreTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct ProjectPinStoreTests {
@Test
func storesPinnedProjectsPerUser() {
diff --git a/HutchTests/ProjectTests.swift b/HutchTests/ProjectTests.swift
index 23faa0d..785c98f 100644
--- a/HutchTests/ProjectTests.swift
+++ b/HutchTests/ProjectTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct ProjectTests {
private enum Fixture {
static let exampleWebsite = "https://example.com"
diff --git a/HutchTests/ReadmeViewTests.swift b/HutchTests/ReadmeViewTests.swift
index 9e053b8..96eb8c0 100644
--- a/HutchTests/ReadmeViewTests.swift
+++ b/HutchTests/ReadmeViewTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct ReadmeViewTests {
@Test
@@ -28,6 +29,7 @@ struct ReadmeViewTests {
}
}
+@MainActor
struct MarkdownRenderingTests {
@Test
@@ -219,6 +221,7 @@ struct MarkdownRenderingTests {
}
}
+@MainActor
struct OrgRenderingTests {
@Test
@@ -376,6 +379,7 @@ struct OrgRenderingTests {
}
}
+@MainActor
struct RepositoryAssetURLTests {
@Test
@@ -391,6 +395,7 @@ struct RepositoryAssetURLTests {
}
}
+@MainActor
struct RepositoryLinkURLTests {
@Test
diff --git a/HutchTests/RecentActivityStoreTests.swift b/HutchTests/RecentActivityStoreTests.swift
index e6fcc04..7713e6d 100644
--- a/HutchTests/RecentActivityStoreTests.swift
+++ b/HutchTests/RecentActivityStoreTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct RecentActivityStoreTests {
@Test
func removeDropsMatchingEntryKeepingOthers() {
diff --git a/HutchTests/RepositoryACLViewModelTests.swift b/HutchTests/RepositoryACLViewModelTests.swift
index 9640b93..501074c 100644
--- a/HutchTests/RepositoryACLViewModelTests.swift
+++ b/HutchTests/RepositoryACLViewModelTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct RepositoryACLViewModelTests {
@Test
diff --git a/HutchTests/RepositoryDeployKeyTests.swift b/HutchTests/RepositoryDeployKeyTests.swift
index c6ff667..d7a0dad 100644
--- a/HutchTests/RepositoryDeployKeyTests.swift
+++ b/HutchTests/RepositoryDeployKeyTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct RepositoryDeployKeyTests {
@Test
func decodesDeployKey() throws {
diff --git a/HutchTests/RepositoryListViewModelTests.swift b/HutchTests/RepositoryListViewModelTests.swift
index 5fe3974..7787fca 100644
--- a/HutchTests/RepositoryListViewModelTests.swift
+++ b/HutchTests/RepositoryListViewModelTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct RepositoryListViewModelTests {
@Test
diff --git a/HutchTests/RepositorySettingsViewModelTests.swift b/HutchTests/RepositorySettingsViewModelTests.swift
index 13d0b21..25e46c9 100644
--- a/HutchTests/RepositorySettingsViewModelTests.swift
+++ b/HutchTests/RepositorySettingsViewModelTests.swift
@@ -10,6 +10,7 @@ private struct UpdateRepositoryInfoPayload: Decodable {
let id: Int
}
+@MainActor
struct RepositorySettingsViewModelTests {
@Test
diff --git a/HutchTests/SRHTClientTests.swift b/HutchTests/SRHTClientTests.swift
index 12d6d0f..51b6a68 100644
--- a/HutchTests/SRHTClientTests.swift
+++ b/HutchTests/SRHTClientTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct SRHTClientTests {
@Test
diff --git a/HutchTests/SRHTWebURLTests.swift b/HutchTests/SRHTWebURLTests.swift
index d1053e6..f20f964 100644
--- a/HutchTests/SRHTWebURLTests.swift
+++ b/HutchTests/SRHTWebURLTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct SRHTWebURLTests {
private enum Expected {
static let chatOrigin = "https://chat.sr.ht"
diff --git a/HutchTests/ScopedSearchHistoryStoreTests.swift b/HutchTests/ScopedSearchHistoryStoreTests.swift
index 856d69f..8effa06 100644
--- a/HutchTests/ScopedSearchHistoryStoreTests.swift
+++ b/HutchTests/ScopedSearchHistoryStoreTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct ScopedSearchHistoryStoreTests {
@Test
diff --git a/HutchTests/SettingsViewModelTests.swift b/HutchTests/SettingsViewModelTests.swift
index 906f33f..e88d064 100644
--- a/HutchTests/SettingsViewModelTests.swift
+++ b/HutchTests/SettingsViewModelTests.swift
@@ -64,6 +64,7 @@ private struct DeleteResultPayload: Decodable {
}
@Suite(.serialized)
+@MainActor
struct SettingsViewModelTests {
@Test
diff --git a/HutchTests/SettingsViewTests.swift b/HutchTests/SettingsViewTests.swift
index 950c3dc..f0f7fc1 100644
--- a/HutchTests/SettingsViewTests.swift
+++ b/HutchTests/SettingsViewTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct SettingsViewTests {
@Test
diff --git a/HutchTests/SourceHutWebDeepLinkMapperTests.swift b/HutchTests/SourceHutWebDeepLinkMapperTests.swift
index 0fa3aaa..2c638c2 100644
--- a/HutchTests/SourceHutWebDeepLinkMapperTests.swift
+++ b/HutchTests/SourceHutWebDeepLinkMapperTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct SourceHutWebDeepLinkMapperTests {
@Test
func mapsRepositoryURLs() {
diff --git a/HutchTests/SyntaxHighlighterTests.swift b/HutchTests/SyntaxHighlighterTests.swift
index 258e81e..3150814 100644
--- a/HutchTests/SyntaxHighlighterTests.swift
+++ b/HutchTests/SyntaxHighlighterTests.swift
@@ -3,6 +3,7 @@ import Testing
import UIKit
@testable import Hutch
+@MainActor
struct SyntaxHighlighterTests {
@Test
func mapsFileNamesToLanguages() {
@@ -24,9 +25,8 @@ struct SyntaxHighlighterTests {
let highlighter = SyntaxHighlighter(theme: .light)
let html = highlighter.highlightedHTML(for: "let answer = 42", language: "swift")
- let unwrapped = try? #require(html)
- #expect(unwrapped?.contains("<span style=\"color:") == true)
- #expect(unwrapped?.contains("answer") == true)
+ #expect(html?.contains("<span style=\"color:") == true)
+ #expect(html?.contains("answer") == true)
}
@Test
@@ -42,7 +42,6 @@ struct SyntaxHighlighterTests {
let font = UIFont.monospacedSystemFont(ofSize: 12, weight: .regular)
let attributed = highlighter.attributedText(for: "print(\"hi\")", language: "python", font: font)
- let unwrapped = try? #require(attributed)
- #expect(unwrapped?.string.contains("print") == true)
+ #expect(attributed?.string.contains("print") == true)
}
}
diff --git a/HutchTests/SystemStatusRepositoryTests.swift b/HutchTests/SystemStatusRepositoryTests.swift
index 56fa7c0..45d32b9 100644
--- a/HutchTests/SystemStatusRepositoryTests.swift
+++ b/HutchTests/SystemStatusRepositoryTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct SystemStatusRepositoryTests {
@Test
@@ -111,21 +112,21 @@ private struct TestSystemStatusService: SystemStatusServing {
}
private extension SystemStatusRepositoryTests {
- static let cachedSnapshotHTML = #"""
+ nonisolated static let cachedSnapshotHTML = #"""
<div class="component" data-status="disrupted">
<a href="/affected/git.sr.ht/">git.sr.ht</a>
<span class="component-status">Disrupted</span>
</div>
"""#
- static let cachedOperationalHTML = #"""
+ nonisolated static let cachedOperationalHTML = #"""
<div class="component" data-status="ok">
<a href="/affected/meta.sr.ht/">meta.sr.ht</a>
<span class="component-status">Operational</span>
</div>
"""#
- static let cachedIncidentRSS = #"""
+ nonisolated static let cachedIncidentRSS = #"""
<rss version="2.0">
<channel>
<item>
@@ -139,7 +140,7 @@ private extension SystemStatusRepositoryTests {
</rss>
"""#
- static let emptyRSS = #"""
+ nonisolated static let emptyRSS = #"""
<rss version="2.0">
<channel></channel>
</rss>
diff --git a/HutchTests/SystemStatusServiceTests.swift b/HutchTests/SystemStatusServiceTests.swift
index d963fa4..9a7af1f 100644
--- a/HutchTests/SystemStatusServiceTests.swift
+++ b/HutchTests/SystemStatusServiceTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct SystemStatusServiceTests {
private enum Fixture {
static let ddosIssueURL = "https://status.sr.ht/issues/2026-04-06-ddos-attack/"
diff --git a/HutchTests/TicketBulkActionTests.swift b/HutchTests/TicketBulkActionTests.swift
index 2f4d1fd..2c1d738 100644
--- a/HutchTests/TicketBulkActionTests.swift
+++ b/HutchTests/TicketBulkActionTests.swift
@@ -1,6 +1,7 @@
import Testing
@testable import Hutch
+@MainActor
struct TicketBulkActionTests {
@Test
diff --git a/HutchTests/TicketDetailViewModelTests.swift b/HutchTests/TicketDetailViewModelTests.swift
index ba4a476..d2c0c8e 100644
--- a/HutchTests/TicketDetailViewModelTests.swift
+++ b/HutchTests/TicketDetailViewModelTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct TicketDetailViewModelTests {
@Test
diff --git a/HutchTests/TicketSavedFilterStoreTests.swift b/HutchTests/TicketSavedFilterStoreTests.swift
index 2ff64e6..cfdcba6 100644
--- a/HutchTests/TicketSavedFilterStoreTests.swift
+++ b/HutchTests/TicketSavedFilterStoreTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct TicketSavedFilterStoreTests {
@Test
diff --git a/HutchTests/TipStoreViewModelTests.swift b/HutchTests/TipStoreViewModelTests.swift
index ed58d1e..734a7cc 100644
--- a/HutchTests/TipStoreViewModelTests.swift
+++ b/HutchTests/TipStoreViewModelTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct TipStoreViewModelTests {
@Test
diff --git a/HutchTests/TrackerListViewModelTests.swift b/HutchTests/TrackerListViewModelTests.swift
index 5c57e04..85b051f 100644
--- a/HutchTests/TrackerListViewModelTests.swift
+++ b/HutchTests/TrackerListViewModelTests.swift
@@ -2,6 +2,7 @@ import Foundation
import Testing
@testable import Hutch
+@MainActor
struct TrackerListViewModelTests {
@Test
diff --git a/HutchTests/TrackerManagementViewModelTests.swift b/HutchTests/TrackerManagementViewModelTests.swift
index 531608c..211c838 100644
--- a/HutchTests/TrackerManagementViewModelTests.swift
+++ b/HutchTests/TrackerManagementViewModelTests.swift
@@ -1,6 +1,7 @@
import Testing
@testable import Hutch
+@MainActor
struct TrackerManagementViewModelTests {
@Test
diff --git a/HutchWidgetExtension/ContributionGraphWidget.swift b/HutchWidgetExtension/ContributionGraphWidget.swift
index 2fc07f2..bc56a03 100644
--- a/HutchWidgetExtension/ContributionGraphWidget.swift
+++ b/HutchWidgetExtension/ContributionGraphWidget.swift
@@ -28,12 +28,16 @@ struct ContributionGraphTimelineProvider: TimelineProvider {
}
func getSnapshot(in _: Context, completion: @escaping (ContributionGraphEntry) -> Void) {
+ // WidgetKit's completion is safe to call from the task; rebind as
+ // nonisolated(unsafe) so it can cross into the `sending` Task closure.
+ nonisolated(unsafe) let completion = completion
Task {
completion(await loadEntry())
}
}
func getTimeline(in _: Context, completion: @escaping (Timeline<ContributionGraphEntry>) -> Void) {
+ nonisolated(unsafe) let completion = completion
Task {
let entry = await loadEntry()
let refreshDate = Calendar.contributionCalendar.date(byAdding: .hour, value: 1, to: Date()) ?? Date().addingTimeInterval(3600)
diff --git a/ROADMAP.txt b/ROADMAP.txt
index 5dbca20..4352b9e 100644
--- a/ROADMAP.txt
+++ b/ROADMAP.txt
@@ -158,7 +158,7 @@ so "breaking change" does not apply. These buckets track *user-visible scale*.
| v3.11.0 | ~~Mailing list subscribe/unsubscribe toggle~~ (shipped) | Ingest-surfaced; state via the `subscriptions` query (the `subscription` field is a trap) |
| v3.12.0 | Accessibility | Independent, device-verified |
| v4.0.0 | Localization *with* translations | The only true re-presentation |
-| — | Swift 6 language mode; cache reads | Internal; ride along, no tag |
+| — | ~~Swift 6 language mode~~ (done); cache reads | Internal; ride along, no tag |
3.9.0 was cut this session, bundling the hub.sr.ht writes with the other
features listed. That reorders the original plan: the "What's cooking" ingest
@@ -384,21 +384,29 @@ rather than a bare `TODO`.)
saved-search store for an intent to open. Add the intent once global
saved-search persistence exists.
-### Swift 6 language mode — no release of its own
-
-The project builds in Swift 5 language mode with
-`SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`. Moving to Swift 6 is blocked on
-concurrency diagnostics that are warnings today and errors there:
-
-- `APICacheTests` and `BundleUserAgentTests` call main-actor-isolated
- initialisers and properties from nonisolated contexts, and `await` a few
- expressions without marking them. Roughly 20 warnings, all in tests.
-- Response types are implicitly `@MainActor` under the default isolation, so
- their `Decodable` conformances are too. Decoding one from a nonisolated
- context — an `async let` over a raw `client.execute`, say — warns now and
- fails then. The pattern that avoids it is `async let` over `@MainActor`
- methods, as in `HomeViewModel.loadDashboard` and
- `NotificationPreferencesViewModel.load`.
+### Swift 6 language mode — done
+
+`SWIFT_VERSION` is now `6.0` (keeping `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`).
+The migration was mostly mechanical, in a few buckets:
+
+- **Models are `nonisolated`.** Under MainActor-default, every value-type model
+ was implicitly `@MainActor`; the pure data types in `Models/` (and utility
+ extensions like `Date.relativeDescription`, `DateFormatter+SRHT`, `SRHTWebURL`)
+ are now `nonisolated`, so the nonisolated networking layer can use them.
+- **App Intents statics.** `AppIntent`/`AppEntity`/`AppEnum` `static var`s were
+ "global shared mutable state"; the stored ones are now `static let`.
+- **Sendable dictionaries.** `nil as String? as Any` in `[String: any Sendable]`
+ GraphQL variable dicts became `nil as String? as any Sendable`.
+- **`UserDefaults`** gets a retroactive `@unchecked Sendable` (documented
+ thread-safe) since it threads through account sessions and stores.
+- **WidgetKit** completion handlers are rebound `nonisolated(unsafe)` to cross
+ into their `Task {}`; the `@Observable` `TipStoreViewModel` task handle is
+ `@ObservationIgnored nonisolated(unsafe)` for its nonisolated `deinit`.
+- **Tests** run on `@MainActor` (they exercise MainActor app code), with a few
+ constant fixtures marked `nonisolated` for use inside `@Sendable` stub
+ closures.
+
+Builds and the full suite are clean in Swift 6 mode with no behaviour change.
### Cache reads that bypass the client — no release of its own