diff options
| author | Christian Cleberg <[email protected]> | 2026-07-16 11:50:53 -0500 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-07-16 11:50:53 -0500 |
| commit | 0b6819d2d08b552e62fd236614d4c3c86a13394b (patch) | |
| tree | efb71f83c6dc62778ab01de9951cd229b50f348c | |
| parent | 2b913e1b727429a3a6bc7eb023003ef2815919ae (diff) | |
| parent | f3c70e04148c57dcafead89b4e02b83e5f5635f8 (diff) | |
| download | hutch-0b6819d2d08b552e62fd236614d4c3c86a13394b.tar.gz hutch-0b6819d2d08b552e62fd236614d4c3c86a13394b.tar.bz2 hutch-0b6819d2d08b552e62fd236614d4c3c86a13394b.zip | |
Merge pull request #8 from zerolabsco/phase-3-sonarcloud-housekeepingv3.8.1
v3.8.1: SonarCloud triage, forceRefresh fix, housekeeping
| -rw-r--r-- | Hutch.xcodeproj/project.pbxproj | 12 | ||||
| -rw-r--r-- | Hutch/App/AccountSession.swift (renamed from Hutch/Hutch/App/AccountSession.swift) | 0 | ||||
| -rw-r--r-- | Hutch/App/HutchIntents.swift | 6 | ||||
| -rw-r--r-- | Hutch/App/RootView.swift | 8 | ||||
| -rw-r--r-- | Hutch/Networking/ProjectService.swift | 8 | ||||
| -rw-r--r-- | Hutch/Views/Home/HomeViewModel.swift | 4 | ||||
| -rw-r--r-- | Hutch/Views/Patchsets/PatchsetDetailView.swift | 2 | ||||
| -rw-r--r-- | Hutch/Views/Projects/ProjectsListView.swift | 6 | ||||
| -rw-r--r-- | Hutch/Views/Repositories/RepositoryDetailViewModel.swift | 2 | ||||
| -rw-r--r-- | Hutch/Views/Tickets/TicketDetailView.swift | 2 | ||||
| -rw-r--r-- | HutchSafariExtension/Resources/background.js | 3 | ||||
| -rw-r--r-- | HutchSafariExtension/Resources/content.js | 3 | ||||
| -rw-r--r-- | HutchTests/APICacheTests.swift | 3 | ||||
| -rw-r--r-- | ROADMAP.md | 102 |
14 files changed, 109 insertions, 52 deletions
diff --git a/Hutch.xcodeproj/project.pbxproj b/Hutch.xcodeproj/project.pbxproj index 32da475..eba1ba7 100644 --- a/Hutch.xcodeproj/project.pbxproj +++ b/Hutch.xcodeproj/project.pbxproj @@ -614,7 +614,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.8.0; + MARKETING_VERSION = 3.8.1; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -651,7 +651,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.8.0; + MARKETING_VERSION = 3.8.1; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -724,7 +724,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 3.8.0; + MARKETING_VERSION = 3.8.1; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -753,7 +753,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 3.8.0; + MARKETING_VERSION = 3.8.1; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -782,7 +782,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 3.8.0; + MARKETING_VERSION = 3.8.1; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchSafariExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -811,7 +811,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 3.8.0; + MARKETING_VERSION = 3.8.1; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchSafariExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; diff --git a/Hutch/Hutch/App/AccountSession.swift b/Hutch/App/AccountSession.swift index 1918b0a..1918b0a 100644 --- a/Hutch/Hutch/App/AccountSession.swift +++ b/Hutch/App/AccountSession.swift diff --git a/Hutch/App/HutchIntents.swift b/Hutch/App/HutchIntents.swift index c2f144a..947ec04 100644 --- a/Hutch/App/HutchIntents.swift +++ b/Hutch/App/HutchIntents.swift @@ -134,7 +134,8 @@ struct SearchHutchIntent: AppIntent { var route: HutchRoute { let normalized = query.trimmingCharacters(in: .whitespacesAndNewlines) - // TODO: Route to global local search once Hutch has one. + // Routes to Lookup for now; repoint at a global content search when Hutch + // gains one — tracked in ROADMAP.md § "App Intent gaps". return normalized.isEmpty ? .lookup : .search(query: normalized) } @@ -145,7 +146,8 @@ struct SearchHutchIntent: AppIntent { } } -// TODO: Add OpenSavedSearchIntent when Hutch has global saved-search persistence. +// An OpenSavedSearchIntent belongs here once Hutch has global saved-search +// persistence — tracked in ROADMAP.md § "App Intent gaps". // MARK: - App Entities diff --git a/Hutch/App/RootView.swift b/Hutch/App/RootView.swift index 10720ad..2e16718 100644 --- a/Hutch/App/RootView.swift +++ b/Hutch/App/RootView.swift @@ -198,11 +198,9 @@ struct RootView: View { } switch link { - case .home: - homePath = NavigationPath() - appState.selectedTab = .home - - case .recentActivity: + // Recent activity is a section of the Home tab, not a screen of its + // own, so its intent/widget deep link lands on Home like .home does. + case .home, .recentActivity: homePath = NavigationPath() appState.selectedTab = .home diff --git a/Hutch/Networking/ProjectService.swift b/Hutch/Networking/ProjectService.swift index 44320ca..91dc339 100644 --- a/Hutch/Networking/ProjectService.swift +++ b/Hutch/Networking/ProjectService.swift @@ -269,15 +269,15 @@ struct ProjectService: Sendable { self.client = client } - func fetchProjects() async throws -> [Project] { - try await fetchProjectSummaries().map(Self.makeSummaryProject) + func fetchProjects(forceRefresh: Bool = false) async throws -> [Project] { + try await fetchProjectSummaries(forceRefresh: forceRefresh).map(Self.makeSummaryProject) } func fetchProjectDetail(rid: String) async throws -> Project { try await fetchProjectDetailPayload(rid: rid) } - private func fetchProjectSummaries() async throws -> [ProjectSummaryPayload] { + private func fetchProjectSummaries(forceRefresh: Bool) async throws -> [ProjectSummaryPayload] { var results: [ProjectSummaryPayload] = [] var cursor: String? @@ -295,7 +295,7 @@ struct ProjectService: Sendable { cacheKey: APICacheKeys.projects(cursor: cursor), resourceType: .userProfile, ttl: APICacheTTLs.projectList, - policy: .cacheFirstThenRefresh + policy: forceRefresh ? .refreshIgnoringCache : .cacheFirstThenRefresh ) let response = cached.value diff --git a/Hutch/Views/Home/HomeViewModel.swift b/Hutch/Views/Home/HomeViewModel.swift index a3aed30..3789c11 100644 --- a/Hutch/Views/Home/HomeViewModel.swift +++ b/Hutch/Views/Home/HomeViewModel.swift @@ -683,7 +683,7 @@ final class HomeViewModel { private func loadProjects(forceRefresh: Bool) async -> Result<[Project], Error> { do { - return .success(try await projectService.fetchProjects()) + return .success(try await projectService.fetchProjects(forceRefresh: forceRefresh)) } catch { return .failure(error) } @@ -716,7 +716,7 @@ final class HomeViewModel { private func loadSystemStatusSnapshot(forceRefresh: Bool) async -> Result<CachedSystemStatusValue<SystemStatusSnapshot>, Error> { do { - return .success(try await systemStatusRepository.snapshotResult()) + return .success(try await systemStatusRepository.snapshotResult(forceRefresh: forceRefresh)) } catch { return .failure(error) } diff --git a/Hutch/Views/Patchsets/PatchsetDetailView.swift b/Hutch/Views/Patchsets/PatchsetDetailView.swift index f560326..3ef5ff4 100644 --- a/Hutch/Views/Patchsets/PatchsetDetailView.swift +++ b/Hutch/Views/Patchsets/PatchsetDetailView.swift @@ -71,7 +71,7 @@ struct PatchsetDetailView: View { Task { await viewModel.updateStatus(to: status) } } } - Button("Cancel", role: .cancel) {} + Button("Cancel", role: .cancel) { /* dismisses the dialog; no action needed */ } } .alert( "Couldn't Update Patchset", diff --git a/Hutch/Views/Projects/ProjectsListView.swift b/Hutch/Views/Projects/ProjectsListView.swift index f6d5766..783417c 100644 --- a/Hutch/Views/Projects/ProjectsListView.swift +++ b/Hutch/Views/Projects/ProjectsListView.swift @@ -25,14 +25,14 @@ final class ProjectsListViewModel { } } - func loadProjects() async { + func loadProjects(forceRefresh: Bool = false) async { guard !isLoading else { return } isLoading = true error = nil defer { isLoading = false } do { - projects = try await service.fetchProjects() + projects = try await service.fetchProjects(forceRefresh: forceRefresh) } catch { if projects.isEmpty { self.error = error.userFacingMessage @@ -115,7 +115,7 @@ struct ProjectsListView: View { ) ) .refreshable { - await viewModel.loadProjects() + await viewModel.loadProjects(forceRefresh: true) } .connectivityOverlay(hasContent: !viewModel.projects.isEmpty) { await viewModel.loadProjects() diff --git a/Hutch/Views/Repositories/RepositoryDetailViewModel.swift b/Hutch/Views/Repositories/RepositoryDetailViewModel.swift index 9b6b942..82e1592 100644 --- a/Hutch/Views/Repositories/RepositoryDetailViewModel.swift +++ b/Hutch/Views/Repositories/RepositoryDetailViewModel.swift @@ -627,7 +627,7 @@ final class RepositoryDetailViewModel { /// Artifacts are release tarballs and signatures rather than media, so a /// generic binary type is honest more often than guessing from the extension. - private nonisolated static func mimeType(for url: URL) -> String { + private nonisolated static func mimeType(for _: URL) -> String { "application/octet-stream" } diff --git a/Hutch/Views/Tickets/TicketDetailView.swift b/Hutch/Views/Tickets/TicketDetailView.swift index b114fa6..6339776 100644 --- a/Hutch/Views/Tickets/TicketDetailView.swift +++ b/Hutch/Views/Tickets/TicketDetailView.swift @@ -204,7 +204,7 @@ struct TicketDetailView: View { } } } - Button("Cancel", role: .cancel) {} + Button("Cancel", role: .cancel) { /* dismisses the dialog; no action needed */ } } message: { Text("This permanently deletes the ticket and its comments. This cannot be undone.") } diff --git a/HutchSafariExtension/Resources/background.js b/HutchSafariExtension/Resources/background.js index 81bf8b6..0147de0 100644 --- a/HutchSafariExtension/Resources/background.js +++ b/HutchSafariExtension/Resources/background.js @@ -68,7 +68,8 @@ function hutchDeepLinkFor(rawURL) { const path = deepLinkPath(url.hostname, normalizedPath(url.pathname)); const service = deepLinkService(url.hostname, path); - return `hutch://${service}${path ? `/${path}` : ""}${url.search}${url.hash}`; + const pathSegment = path ? `/${path}` : ""; + return `hutch://${service}${pathSegment}${url.search}${url.hash}`; } function showUnsupportedMessage(tabId) { diff --git a/HutchSafariExtension/Resources/content.js b/HutchSafariExtension/Resources/content.js index dba7c5d..e5cb087 100644 --- a/HutchSafariExtension/Resources/content.js +++ b/HutchSafariExtension/Resources/content.js @@ -63,7 +63,8 @@ function hutchDeepLinkForLocation() { const path = hutchDeepLinkPath(location.hostname, hutchNormalizedPath(location.pathname)); const service = hutchDeepLinkService(location.hostname, path); - return `hutch://${service}${path ? `/${path}` : ""}${location.search}${location.hash}`; + const pathSegment = path ? `/${path}` : ""; + return `hutch://${service}${pathSegment}${location.search}${location.hash}`; } function storageGet(defaults) { diff --git a/HutchTests/APICacheTests.swift b/HutchTests/APICacheTests.swift index 89c6c53..24db870 100644 --- a/HutchTests/APICacheTests.swift +++ b/HutchTests/APICacheTests.swift @@ -256,6 +256,7 @@ struct APICacheTests { _ = try await cache.read(cacheKey: key) Issue.record("Expected cache miss for \(key).") } catch APICacheError.miss { + // expected: a miss is the success path here } catch { Issue.record("Unexpected error for \(key): \(error).") } @@ -293,7 +294,7 @@ private final class CachedURLProtocol: URLProtocol, @unchecked Sendable { } } - override func stopLoading() {} + override func stopLoading() { /* required override; nothing to tear down */ } static func reset(responses: [CachedURLProtocolResponse], responseDelay: TimeInterval = 0) { Self.responses = responses @@ -198,23 +198,56 @@ Labels and hints appear in 17 of 89 view files. Mechanical and low-risk, but it cannot be verified from a build — it needs VoiceOver driven on a device. Independent of every other bucket, so it can move if a device pass is convenient. -### SonarCloud backlog — v3.8.1 - -51 open issues: **0 bugs, 0 vulnerabilities, 51 code smells**, plus 3 security -hotspots. The headline number is misleading, so trust the breakdown before -budgeting: - -- **35× `swift:S1075` (hardcoded URI)** — 28 of them in - `SourceHutWebDeepLinkMapperTests`, 5 in `Shared/HutchDeepLinkURLs`. A deep-link - mapper's tests exist precisely to assert against literal URLs, and a client for - one forge has fixed endpoints by definition. These want triaging as *Won't - Fix* in SonarCloud, not refactoring. "Fixing" them would make the code worse. -- **5× `swift:S1135`** — TODO comments. Two are in `HutchIntents` and name real - gaps. -- **3× `swift:S1186` (empty closure)** — all three CRITICAL, all three trivial: - `Button("Cancel", role: .cancel) {}` needs no body. A comment settles it. -- **2× `javascript:S4624`** in the Safari extension; **2× `swift:S1172`** unused - parameters. +### SonarCloud backlog — done in code (v3.8.1) + +The live count is **53 issues / 10 rules**, not the 51 / 5 an earlier pass +recorded — a reminder that this section rots like everything else, so query the +API before budgeting. **0 bugs, 0 vulnerabilities**; everything is a code smell +or hotspot. What the code side of v3.8.1 actually did: + +Fixed (`e93972f`): + +- **`swift:S1871`** — `RootView` had byte-identical `.home` / `.recentActivity` + deep-link cases. Merged; recent activity is a *section* of Home, not a screen, + so both correctly land on the Home tab. +- **3× `swift:S1186` (empty closure/function, CRITICAL)** — two are + `Button("Cancel", role: .cancel) {}` (dialog dismissal needs no body); the + third is an empty `URLProtocol.stopLoading()` override in a test. All three now + carry a nested comment. Note the earlier claim that "all three are Cancel + buttons" was wrong — only two are. +- **`swift:S108`** — the expected-miss `catch` in `APICacheTests` is commented. +- **`swift:S1172`** — the unused `url` in `mimeType(for:)` is now `_`. +- **2× `javascript:S4624`** — the nested template literal in the deep-link + builders (`background.js`, `content.js`) is extracted to a `pathSegment` var. + +Fixed as a real bug instead (`65412ee`), not silenced: + +- **2× `swift:S1172` on `forceRefresh`** — `HomeViewModel.loadProjects` and + `loadSystemStatusSnapshot` took the flag and dropped it, so dashboard + pull-to-refresh returned cached projects and status. This is the trap named at + the top of this file. `ProjectsListView` carried the same defect via its own + `.refreshable`. Both fixed at the root in `ProjectService.fetchProjects`. + +Won't Fix, with reasons (resolve in SonarCloud's web UI, not in code): + +- **35× `swift:S1075` (hardcoded URI)** — 28 in `SourceHutWebDeepLinkMapperTests`, + the rest in `HutchDeepLinkURLs`. A deep-link mapper's tests exist to assert + literal URLs, and a one-forge client has fixed endpoints. "Fixing" them makes + the code worse. +- **`swift:S107`** — `executeCached` has 8 params across **38 call sites**. A + param object would rewrite the hottest networking method for no behaviour or + correctness gain against an arbitrary 7-param line. Not worth the regression + surface. +- **`swift:S1481`** — `ArtifactsView`'s `@Bindable var vm` is flagged unused, but + `$vm.error` is used at line 134; Sonar's Swift analyzer misses the projected + value. False positive — removing it breaks the build. +- **`javascript:S7785`** — prefers top-level `await` for `injectBannerIfEnabled()`, + but `content.js` is a classic content script, not a module. Top-level `await` + would be a syntax error. Not applicable. +- **5× `swift:S1135`** — TODO comments (INFO). The two in `HutchIntents` named + real gaps and are now promoted to "App Intent gaps" below, with the inline + `TODO`s replaced by plain references — so those two clear. The remaining three + (`DeepLink`, `NotificationPreferencesViewModel` ×2) stay until addressed. The 3 hotspots are the part actually worth thought: @@ -234,10 +267,11 @@ The 3 hotspots are the part actually worth thought: Query it with: `https://sonarcloud.io/api/issues/search?componentKeys=zerolabsco_hutch&resolved=false` -This is a patch because nothing executes differently afterwards. The 35 hardcoded-URI -issues are resolved as *Won't Fix* in SonarCloud's web UI — not a commit at all — and -the rest is three comments and one annotation. If it produces a diff that changes a -runtime path, something has gone wrong. +This was scoped as a patch on the assumption nothing executes differently — and +that mostly held: the cosmetic fixes are comments, a merge, and a rename. The one +exception earns the release its own line: the `forceRefresh` fix changes what +pull-to-refresh does, so it needs a manual pass on a device before v3.8.1 ships, +not just a green suite. ### Ingest "What's cooking on SourceHut?" — v3.9.0 @@ -283,6 +317,24 @@ sequenced after the ingest rather than planned now. Read `api/graph/schema.graphqls` in `hub.sr.ht` before committing the version number. The bucket may be empty. +### App Intent gaps — unscheduled + +Two App Intents in `HutchIntents.swift` are placeholders for features Hutch does +not have yet. Both are gated on the same missing capability — a global +search/persistence layer — so neither is schedulable until that lands. (These +were the two `swift:S1135` TODOs; promoted here so the code carries a reference +rather than a bare `TODO`.) + +- **Global content search.** `SearchHutchIntent` accepts a query but routes to + the Lookup screen — sourcehut entity resolution — because Hutch has no + full-text search across tickets, repos, and lists. Its own description says + "Opens Hutch lookup with a search query." When a real search exists, repoint + the `.search` route in `SearchHutchIntent.route`. +- **`OpenSavedSearchIntent`.** Saved searches are per-tracker only + (`TicketSavedFilterStore`, `ScopedSearchHistoryStore`); there is no global + 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 @@ -309,7 +361,9 @@ which already consults the persistent cache before the memory layer. Like Swift 6 above, this is internal and rides along with whatever release already touches that area. Neither justifies a tag. -## Housekeeping — v3.8.1 +## Housekeeping -- `Hutch/Hutch/App/AccountSession.swift` sits in a stray nested directory; - `Hutch/HutchTests/` is empty. +- ~~`Hutch/Hutch/App/AccountSession.swift` sits in a stray nested directory; + `Hutch/HutchTests/` is empty.~~ Done (v3.8.1, `9834b78`). Moved beside the rest + of `App/`; both stray dirs removed. No pbxproj change — the target is a + synchronized root group, so the file compiled by path all along. |
