import Foundation import SwiftUI @MainActor final class PRDetailViewModel: ObservableObject { let repo: Repository let prNumber: Int @Published var pr: PullRequest? @Published var comments: [IssueComment] = [] @Published var reviews: [PullReview] = [] @Published var files: [ChangedFile] = [] @Published var commits: [PRCommit] = [] @Published var runs: [ActionRun] = [] @Published var jobs: [Int64: [ActionJob]] = [:] @Published var isLoading = false @Published var isActionsLoading = false @Published var errorMessage: String? @Published var busyMessage: String? private var owner: String { repo.owner.login } init(repo: Repository, prNumber: Int) { self.repo = repo self.prNumber = prNumber } var ciStatus: CIStatus { CIStatus.from(runs: runs) } var reviewState: ReviewState { ReviewState.from(reviews: reviews) } var hasFailedRun: Bool { runs.contains { let c = ($0.conclusion ?? "").lowercased() return c == "failure" || c == "cancelled" } } func load() async { guard let client = SettingsStore.shared.client() else { return } isLoading = true errorMessage = nil defer { isLoading = false } do { async let prT = client.pullRequest(owner: owner, repo: repo.name, number: prNumber) async let commentsT = client.comments(owner: owner, repo: repo.name, number: prNumber) async let reviewsT = client.reviews(owner: owner, repo: repo.name, number: prNumber) async let filesT = client.changedFiles(owner: owner, repo: repo.name, number: prNumber) async let commitsT = client.commits(owner: owner, repo: repo.name, number: prNumber) pr = try await prT comments = try await commentsT reviews = try await reviewsT files = try await filesT commits = try await commitsT // CI runs depend on the head SHA from the PR. if let sha = pr?.head?.sha, !sha.isEmpty { await loadRuns(client: client, sha: sha) } } catch { errorMessage = error.localizedDescription } } private func loadRuns(client: GiteaClient, sha: String) async { isActionsLoading = true defer { isActionsLoading = false } do { runs = try await client.runs(owner: owner, repo: repo.name, sha: sha) // Load jobs for each run concurrently. for run in runs { let jobList = (try? await client.jobs(owner: owner, repo: repo.name, runID: run.id)) ?? [] jobs[run.id] = jobList } } catch { // CI is best-effort; don't fail the whole detail view. } } func rerun(runID: Int64) async { guard let client = SettingsStore.shared.client() else { return } busyMessage = "Re-running job…" defer { busyMessage = nil } do { try await client.rerun(owner: owner, repo: repo.name, runID: runID) if let sha = pr?.head?.sha { await loadRuns(client: client, sha: sha) } } catch { errorMessage = error.localizedDescription } } func postComment(_ body: String) async { guard let client = SettingsStore.shared.client() else { return } busyMessage = "Posting…" defer { busyMessage = nil } do { let c = try await client.postComment(owner: owner, repo: repo.name, number: prNumber, body: body) comments.append(c) } catch { errorMessage = error.localizedDescription } } func submitReview(state: String, comment: String?) async { guard let client = SettingsStore.shared.client() else { return } busyMessage = "Submitting…" defer { busyMessage = nil } do { let r = try await client.submitReview(owner: owner, repo: repo.name, number: prNumber, state: state, comment: comment) reviews.append(r) } catch { errorMessage = error.localizedDescription } } func merge(method: String) async -> Bool { guard let client = SettingsStore.shared.client() else { return false } busyMessage = "Merging…" defer { busyMessage = nil } do { try await client.merge(owner: owner, repo: repo.name, number: prNumber, method: method) // Refresh the PR state to reflect the merge. pr = try? await client.pullRequest(owner: owner, repo: repo.name, number: prNumber) return true } catch { errorMessage = error.localizedDescription return false } } func setState(_ state: String) async { guard let client = SettingsStore.shared.client() else { return } busyMessage = "Updating…" defer { busyMessage = nil } do { try await client.setState(owner: owner, repo: repo.name, number: prNumber, state: state) pr = try? await client.pullRequest(owner: owner, repo: repo.name, number: prNumber) } catch { errorMessage = error.localizedDescription } } }