diff --git a/GiteaPR.xcodeproj/project.pbxproj b/GiteaPR.xcodeproj/project.pbxproj index 508aa4b..cfe6e4d 100644 --- a/GiteaPR.xcodeproj/project.pbxproj +++ b/GiteaPR.xcodeproj/project.pbxproj @@ -187,7 +187,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR"; @@ -197,7 +197,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.6; + MARKETING_VERSION = 1.7; PRODUCT_BUNDLE_IDENTIFIER = com.bueso.giteapr; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -211,7 +211,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR"; @@ -221,7 +221,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.6; + MARKETING_VERSION = 1.7; PRODUCT_BUNDLE_IDENTIFIER = com.bueso.giteapr; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/GiteaPR/Detail/JobLogsView.swift b/GiteaPR/Detail/JobLogsView.swift new file mode 100644 index 0000000..35ddfb9 --- /dev/null +++ b/GiteaPR/Detail/JobLogsView.swift @@ -0,0 +1,66 @@ +import SwiftUI + +/// Displays a Gitea Actions job's raw log text, with a share button. +struct JobLogsView: View { + let repo: Repository + let runID: Int64 + let jobID: Int64 + let jobName: String + + @State private var text = "" + @State private var isLoading = true + @State private var errorMessage: String? + @State private var showShare = false + + var body: some View { + Group { + if isLoading { + ProgressView("Loading logs…") + } else if let errorMessage { + ContentUnavailableView( + "Couldn't load logs", + systemImage: "doc.text", + description: Text(errorMessage)) + } else { + ScrollView { + Text(text) + .font(.caption.monospaced()) + .frame(maxWidth: .infinity, alignment: .leading) + .textSelection(.enabled) + .padding() + } + } + } + .navigationTitle(jobName) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button { + showShare = true + } label: { + Image(systemName: "square.and.arrow.up") + } + .disabled(text.isEmpty) + } + } + .sheet(isPresented: $showShare) { + ShareSheet(text: text) + } + .task { await load() } + } + + private func load() async { + isLoading = true + errorMessage = nil + defer { isLoading = false } + guard let client = SettingsStore.shared.client() else { + errorMessage = "Not configured." + return + } + do { + text = try await client.jobLogs(owner: repo.owner.login, repo: repo.name, jobID: jobID) + } catch { + errorMessage = error.localizedDescription + } + } +} diff --git a/GiteaPR/Detail/Tabs.swift b/GiteaPR/Detail/Tabs.swift index 1797324..4b7c350 100644 --- a/GiteaPR/Detail/Tabs.swift +++ b/GiteaPR/Detail/Tabs.swift @@ -218,6 +218,14 @@ struct ChecksTab: View { .font(.caption) .buttonStyle(.bordered) } + NavigationLink { + JobLogsView(repo: vm.repo, runID: run.id, jobID: job.id, + jobName: job.name) + } label: { + Text("Logs") + .font(.caption) + } + .buttonStyle(.bordered) } } } diff --git a/GiteaPR/Models/PullRequest.swift b/GiteaPR/Models/PullRequest.swift index 03ea2d2..e947fc8 100644 --- a/GiteaPR/Models/PullRequest.swift +++ b/GiteaPR/Models/PullRequest.swift @@ -82,22 +82,24 @@ struct IssueComment: Codable, Identifiable { } /// A commit in a pull request. -struct PRCommit: Codable, Identifiable { - struct Meta: Codable { +struct PRCommit: Codable, Hashable, Identifiable { + struct Meta: Codable, Hashable { let sha: String? let message: String? } - struct Author: Codable { + struct Author: Codable, Hashable { let name: String? let email: String? let login: String? } - let id: String // sha let sha: String? let commit: Meta? let author: Author? + /// Gitea's PR commits have no `id` field — only `sha`. Use sha as the + /// identity so the array decodes and SwiftUI can key rows on it. + var id: String { sha ?? commit?.sha ?? "\(author?.name ?? "")-\(commit?.message?.hashValue ?? 0)" } enum CodingKeys: String, CodingKey { - case id, sha, commit, author + case sha, commit, author } } diff --git a/GiteaPR/Networking/GiteaClient.swift b/GiteaPR/Networking/GiteaClient.swift index 5463d68..ce71044 100644 --- a/GiteaPR/Networking/GiteaClient.swift +++ b/GiteaPR/Networking/GiteaClient.swift @@ -250,4 +250,14 @@ struct GiteaClient { func rerun(owner: String, repo: String, runID: Int64) async throws { try await postNoBody("repos/\(owner)/\(repo)/actions/runs/\(runID)/rerun") } + + /// Raw log text for a single job (from `/actions/jobs/{job_id}/logs`). + /// The response is plain text, not JSON. + func jobLogs(owner: String, repo: String, jobID: Int64) async throws -> String { + let data = try await request("repos/\(owner)/\(repo)/actions/jobs/\(jobID)/logs") + guard let text = String(data: data, encoding: .utf8) else { + throw APIError.invalidResponse + } + return text + } }