Build iOS App / Build .ipa and publish source (push) Failing after 5m44s
- LogStore: bounded in-memory + persisted log of API calls and errors, viewable and shareable from Settings → View logs. - GiteaClient: log every request/response status (esp. 404s). - Inbox: a single failing repo no longer wipes the whole inbox — per-repo errors are caught, logged, and the rest still load. - Bump to 1.2 so SideStore offers the update.
142 lines
5.2 KiB
Swift
142 lines
5.2 KiB
Swift
import Foundation
|
|
import SwiftUI
|
|
|
|
enum InboxFilter: String, CaseIterable, Identifiable {
|
|
case all = "All"
|
|
case assigned = "Assigned"
|
|
case created = "By me"
|
|
case review = "Review"
|
|
|
|
var id: String { rawValue }
|
|
}
|
|
|
|
/// A PR row enriched with its repo and aggregate status.
|
|
struct PRRow: Identifiable, Hashable {
|
|
let repo: Repository
|
|
let pr: PullRequest
|
|
let ciStatus: CIStatus
|
|
let reviewState: ReviewState
|
|
|
|
var id: String { "\(repo.fullName)#\(pr.number)" }
|
|
var fullName: String { repo.fullName }
|
|
}
|
|
|
|
@MainActor
|
|
final class InboxViewModel: ObservableObject {
|
|
@Published var rows: [PRRow] = []
|
|
@Published var isLoading = false
|
|
@Published var errorMessage: String?
|
|
@Published var filter: InboxFilter = .all {
|
|
didSet { recomputeDisplayed() }
|
|
}
|
|
// Raw rows before filtering, kept so switching filters is instant.
|
|
private var allRows: [PRRow] = []
|
|
@Published var displayedRows: [PRRow] = []
|
|
|
|
var isConfigured: Bool { SettingsStore.shared.isConfigured }
|
|
|
|
private func recomputeDisplayed() {
|
|
switch filter {
|
|
case .all:
|
|
displayedRows = allRows
|
|
case .assigned:
|
|
displayedRows = allRows.filter {
|
|
($0.pr.assignees ?? []).contains {
|
|
$0.login == AppSession.currentLogin
|
|
}
|
|
}
|
|
case .created:
|
|
displayedRows = allRows.filter {
|
|
$0.pr.user?.login == AppSession.currentLogin
|
|
}
|
|
case .review:
|
|
displayedRows = allRows.filter {
|
|
switch $0.reviewState {
|
|
case .needsReview, .unknown: return true
|
|
default: return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func load() async {
|
|
guard isConfigured, let client = SettingsStore.shared.client() else {
|
|
errorMessage = "Configure your Gitea instance in Settings."
|
|
return
|
|
}
|
|
isLoading = true
|
|
errorMessage = nil
|
|
defer { isLoading = false }
|
|
|
|
do {
|
|
let repos = try await client.repos()
|
|
LogStore.shared.log(.info, "Inbox: \(repos.count) repos")
|
|
// Fetch open PRs across all repos concurrently. A single repo that
|
|
// fails (e.g. 404) must NOT wipe the whole inbox — catch per-repo
|
|
// errors, log them, and keep whatever succeeded.
|
|
var rows: [PRRow] = []
|
|
var failures: [String] = []
|
|
await withTaskGroup(of: Result<[PRRow], Error>.self) { group in
|
|
for repo in repos {
|
|
group.addTask {
|
|
do {
|
|
let prs = try await client.pullRequests(owner: repo.owner.login,
|
|
repo: repo.name)
|
|
return .success(try await self.enrich(prs, in: repo, client: client))
|
|
} catch {
|
|
return .failure(error)
|
|
}
|
|
}
|
|
}
|
|
for await result in group {
|
|
switch result {
|
|
case .success(let batch):
|
|
rows.append(contentsOf: batch)
|
|
case .failure(let error):
|
|
failures.append(error.localizedDescription)
|
|
}
|
|
}
|
|
}
|
|
if !failures.isEmpty {
|
|
LogStore.shared.log(.warn, "Inbox: \(failures.count) repo(s) failed: \(failures.joined(separator: "; "))")
|
|
}
|
|
// Sort: most recently updated first.
|
|
rows.sort { ($0.pr.updatedAt ?? .distantPast) > ($1.pr.updatedAt ?? .distantPast) }
|
|
allRows = rows
|
|
recomputeDisplayed()
|
|
if rows.isEmpty && !failures.isEmpty {
|
|
errorMessage = "Couldn't load any PRs (\(failures.count) repo(s) failed). Check the Logs screen."
|
|
}
|
|
} catch {
|
|
LogStore.shared.log(.error, "Inbox load failed: \(error.localizedDescription)")
|
|
errorMessage = error.localizedDescription
|
|
allRows = []
|
|
recomputeDisplayed()
|
|
}
|
|
}
|
|
|
|
/// Enrich each PR with CI + review status. Runs sequentially per repo to
|
|
/// avoid hammering the API from one inbox refresh.
|
|
private func enrich(_ prs: [PullRequest], in repo: Repository,
|
|
client: GiteaClient) async throws -> [PRRow] {
|
|
var out: [PRRow] = []
|
|
for pr in prs {
|
|
let sha = pr.head?.sha
|
|
var ci: CIStatus = .unknown
|
|
var review: ReviewState = .unknown
|
|
if let sha, !sha.isEmpty {
|
|
let runs = (try? await client.runs(owner: repo.owner.login,
|
|
repo: repo.name, sha: sha)) ?? []
|
|
ci = CIStatus.from(runs: runs)
|
|
}
|
|
let reviews = (try? await client.reviews(owner: repo.owner.login,
|
|
repo: repo.name,
|
|
number: pr.number)) ?? []
|
|
review = ReviewState.from(reviews: reviews)
|
|
out.append(PRRow(repo: repo, pr: pr,
|
|
ciStatus: ci, reviewState: review))
|
|
}
|
|
return out
|
|
}
|
|
}
|