diff --git a/GiteaPR.xcodeproj/project.pbxproj b/GiteaPR.xcodeproj/project.pbxproj index 7851c56..a56c2bf 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 = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR"; @@ -197,7 +197,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.3; + MARKETING_VERSION = 1.4; 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 = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR"; @@ -221,7 +221,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.3; + MARKETING_VERSION = 1.4; PRODUCT_BUNDLE_IDENTIFIER = com.bueso.giteapr; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/GiteaPR/Detail/DiffView.swift b/GiteaPR/Detail/DiffView.swift index 2451ede..27977cc 100644 --- a/GiteaPR/Detail/DiffView.swift +++ b/GiteaPR/Detail/DiffView.swift @@ -2,32 +2,79 @@ import SwiftUI /// Read-only unified diff viewer for a single changed file. /// -/// Parses a Gitea unified diff patch into line hunks so added/deleted lines can -/// be colour-coded. Gitea only returns `patch` for small diffs; large files -/// fall back to a raw text view. +/// This Gitea instance does NOT return a per-file `patch` from +/// `/pulls/{n}/files` (the field is absent), so the diff is fetched from the +/// PR's `.diff` endpoint and the hunks belonging to this file are extracted. +/// Added/deleted lines are colour-coded. struct DiffView: View { + let repo: Repository + let prNumber: Int let file: ChangedFile + @State private var lines: [DiffLine] = [] + @State private var isLoading = true + @State private var errorMessage: String? var body: some View { Group { - if let patch = file.patch, !patch.isEmpty { + if isLoading { + ProgressView("Loading diff…") + } else if let errorMessage { + ContentUnavailableView( + "Couldn't load diff", + systemImage: "doc.text", + description: Text(errorMessage)) + } else { ScrollView([.horizontal, .vertical]) { LazyVStack(alignment: .leading, spacing: 0) { - ForEach(parse(patch)) { line in + ForEach(lines) { line in diffLine(line) } } .font(.caption.monospaced()) } - } else { - ContentUnavailableView( - "Diff too large to preview", - systemImage: "doc.text", - description: Text("Gitea didn't return a patch for this file.")) } } .navigationTitle(file.filename) .navigationBarTitleDisplayMode(.inline) + .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 { + let full = try await client.diff(owner: repo.owner.login, + repo: repo.name, + number: prNumber) + let patch = extractSection(named: file.filename, from: full) + lines = parse(patch.isEmpty ? full : patch) + if lines.isEmpty { + errorMessage = "No diff content for this file." + } + } catch { + errorMessage = error.localizedDescription + } + } + + /// Extract the unified-diff block for `filename` from a multi-file diff. + /// Falls back to the whole diff if the header can't be matched. + private func extractSection(named filename: String, from diff: String) -> String { + let marker = "+++ b/\(filename)" + guard let idx = diff.range(of: marker)?.upperBound else { + return "" + } + let tail = String(diff[idx...]) + // Find the next "diff --git " after this file's header to delimit it. + let rest = tail.dropFirst(marker.count) + if let next = rest.range(of: "\ndiff --git ") { + return String(tail[.. some View { diff --git a/GiteaPR/Detail/Tabs.swift b/GiteaPR/Detail/Tabs.swift index 3954b63..1797324 100644 --- a/GiteaPR/Detail/Tabs.swift +++ b/GiteaPR/Detail/Tabs.swift @@ -106,7 +106,7 @@ struct FilesTab: View { } } .navigationDestination(for: ChangedFile.self) { file in - DiffView(file: file) + DiffView(repo: vm.repo, prNumber: vm.prNumber, file: file) } } } diff --git a/GiteaPR/Networking/GiteaClient.swift b/GiteaPR/Networking/GiteaClient.swift index 1fff7bc..dc76ce3 100644 --- a/GiteaPR/Networking/GiteaClient.swift +++ b/GiteaPR/Networking/GiteaClient.swift @@ -118,6 +118,18 @@ struct GiteaClient { _ = try await request(path, method: "POST") } + /// Raw unified diff for a PR as plain text (from the `.diff` endpoint). + /// This Gitea instance does NOT return `patch` per file in + /// `/pulls/{n}/files` (the field is absent), so the diff must come from + /// here. The response is `text/plain`, not JSON. + func diff(owner: String, repo: String, number: Int) async throws -> String { + let data = try await request("repos/\(owner)/\(repo)/pulls/\(number).diff") + guard let text = String(data: data, encoding: .utf8) else { + throw APIError.invalidResponse + } + return text + } + // MARK: - User /// Current authenticated user (for the "Test connection" button and