summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-07-16 11:50:53 -0500
committerGitHub <[email protected]>2026-07-16 11:50:53 -0500
commit0b6819d2d08b552e62fd236614d4c3c86a13394b (patch)
treeefb71f83c6dc62778ab01de9951cd229b50f348c
parent2b913e1b727429a3a6bc7eb023003ef2815919ae (diff)
parentf3c70e04148c57dcafead89b4e02b83e5f5635f8 (diff)
downloadhutch-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.pbxproj12
-rw-r--r--Hutch/App/AccountSession.swift (renamed from Hutch/Hutch/App/AccountSession.swift)0
-rw-r--r--Hutch/App/HutchIntents.swift6
-rw-r--r--Hutch/App/RootView.swift8
-rw-r--r--Hutch/Networking/ProjectService.swift8
-rw-r--r--Hutch/Views/Home/HomeViewModel.swift4
-rw-r--r--Hutch/Views/Patchsets/PatchsetDetailView.swift2
-rw-r--r--Hutch/Views/Projects/ProjectsListView.swift6
-rw-r--r--Hutch/Views/Repositories/RepositoryDetailViewModel.swift2
-rw-r--r--Hutch/Views/Tickets/TicketDetailView.swift2
-rw-r--r--HutchSafariExtension/Resources/background.js3
-rw-r--r--HutchSafariExtension/Resources/content.js3
-rw-r--r--HutchTests/APICacheTests.swift3
-rw-r--r--ROADMAP.md102
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
diff --git a/ROADMAP.md b/ROADMAP.md
index 17072e4..43c3e7c 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -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.