From 8be8f3ac50f94ef9a333ee5da71609eb49330cd9 Mon Sep 17 00:00:00 2001 From: Hermes Date: Wed, 12 Aug 2026 21:18:01 +0000 Subject: [PATCH] Fix diff viewer: fetch PR .diff instead of relying on per-file patch; bump to 1.4 This Gitea instance never returns a 'patch' field from /pulls/{n}/files (the field is absent for all files, regardless of size), so the diff viewer always fell back to 'Diff too large to preview'. Now DiffView fetches the PR's .diff endpoint (plain-text unified diff), extracts the hunks for the selected file, and renders them colour-coded. Bump to 1.4. --- GiteaPR.xcodeproj/project.pbxproj | 8 ++-- GiteaPR/Detail/DiffView.swift | 67 +++++++++++++++++++++++----- GiteaPR/Detail/Tabs.swift | 2 +- GiteaPR/Networking/GiteaClient.swift | 12 +++++ 4 files changed, 74 insertions(+), 15 deletions(-) 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