Build iOS App / Build .ipa and publish source (push) Successful in 21s
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.
127 lines
4.5 KiB
Swift
127 lines
4.5 KiB
Swift
import SwiftUI
|
|
|
|
/// Read-only unified diff viewer for a single changed file.
|
|
///
|
|
/// 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 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(lines) { line in
|
|
diffLine(line)
|
|
}
|
|
}
|
|
.font(.caption.monospaced())
|
|
}
|
|
}
|
|
}
|
|
.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[..<next.lowerBound])
|
|
}
|
|
return tail
|
|
}
|
|
|
|
private func diffLine(_ line: DiffLine) -> some View {
|
|
Text(line.text)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 1)
|
|
.background(background(for: line.type))
|
|
.foregroundColor(foreground(for: line.type))
|
|
}
|
|
|
|
private func background(for type: DiffLineType) -> Color {
|
|
switch type {
|
|
case .addition: return Color.green.opacity(0.15)
|
|
case .deletion: return Color.red.opacity(0.15)
|
|
case .hunk, .meta: return Color(.secondarySystemBackground)
|
|
case .context: return .clear
|
|
}
|
|
}
|
|
private func foreground(for type: DiffLineType) -> Color {
|
|
switch type {
|
|
case .addition: return .green
|
|
case .deletion: return .red
|
|
case .hunk: return .secondary
|
|
case .meta, .context: return .primary
|
|
}
|
|
}
|
|
|
|
private func parse(_ patch: String) -> [DiffLine] {
|
|
patch.components(separatedBy: "\n").map { line in
|
|
if line.hasPrefix("@@") { return DiffLine(text: line, type: .hunk) }
|
|
if line.hasPrefix("diff ") || line.hasPrefix("index ")
|
|
|| line.hasPrefix("--- ") || line.hasPrefix("+++ ") {
|
|
return DiffLine(text: line, type: .meta)
|
|
}
|
|
if line.hasPrefix("+") { return DiffLine(text: line, type: .addition) }
|
|
if line.hasPrefix("-") { return DiffLine(text: line, type: .deletion) }
|
|
return DiffLine(text: line, type: .context)
|
|
}
|
|
}
|
|
}
|
|
|
|
private enum DiffLineType { case context, addition, deletion, hunk, meta }
|
|
|
|
private struct DiffLine: Identifiable {
|
|
let text: String
|
|
let type: DiffLineType
|
|
var id: UUID { UUID() }
|
|
}
|