krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
v3.9.0: Hutch/Views/Inbox/InboxThreadUtilities.swift · raw
1import Foundation
2
3enum InboxThreadUtilities {
4 nonisolated static func deriveRepositoryName(from listName: String) -> String? {
5 let separators = ["-devel", "-patches", "-dev", ".patches"]
6 for separator in separators where listName.hasSuffix(separator) {
7 return String(listName.dropLast(separator.count))
8 }
9 return nil
10 }
11
12 /// Splits an email body into its commit message and diff, so patch mail can be
13 /// rendered as prose plus a diff rather than one undifferentiated blob.
14 ///
15 /// Shared by the inbox thread view and patchset review: sr.ht's `Patch` type
16 /// carries no diff, so the diff has to be recovered from the email body.
17 nonisolated static func segmentMessageBody(_ body: String, isPatch: Bool) -> [InboxMessageContentBlock] {
18 guard isPatch else {
19 let trimmedBody = body.trimmingCharacters(in: .whitespacesAndNewlines)
20 return trimmedBody.isEmpty ? [] : [.plainText(trimmedBody)]
21 }
22
23 let normalizedBody = normalizeLineEndings(in: body)
24 let lines = normalizedBody.components(separatedBy: "\n")
25 guard let diffStartIndex = actualDiffStartIndex(in: lines) else {
26 let trimmedBody = normalizedBody.trimmingCharacters(in: .whitespacesAndNewlines)
27 return trimmedBody.isEmpty ? [] : [.plainText(trimmedBody)]
28 }
29
30 var blocks: [InboxMessageContentBlock] = []
31 let leadingPlainText = lines[..<diffStartIndex]
32 .joined(separator: "\n")
33 .trimmingCharacters(in: .whitespacesAndNewlines)
34 if !leadingPlainText.isEmpty {
35 blocks.append(.plainText(leadingPlainText))
36 }
37
38 let remainingLines = Array(lines[diffStartIndex...])
39 let signatureIndex = remainingLines.firstIndex(where: isEmailSignatureSeparator)
40
41 let diffLines: ArraySlice<String>
42 let trailingPlainText: String
43 if let signatureIndex {
44 diffLines = remainingLines[..<signatureIndex]
45 trailingPlainText = remainingLines[signatureIndex...]
46 .joined(separator: "\n")
47 .trimmingCharacters(in: .whitespacesAndNewlines)
48 } else {
49 diffLines = remainingLines[...]
50 trailingPlainText = ""
51 }
52
53 let diff = diffLines.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines)
54 if !diff.isEmpty {
55 blocks.append(.diff(diff))
56 }
57
58 if !trailingPlainText.isEmpty {
59 blocks.append(.plainText(trailingPlainText))
60 }
61 return blocks
62 }
63
64 nonisolated static func actualDiffStartIndex(in lines: [String]) -> Int? {
65 if let explicitDiffIndex = lines.firstIndex(where: { $0.hasPrefix("diff --git ") }) {
66 return explicitDiffIndex
67 }
68
69 for index in lines.indices {
70 let line = lines[index]
71 guard line.hasPrefix("--- ") else { continue }
72 let nextIndex = lines.index(after: index)
73 guard nextIndex < lines.endIndex else { continue }
74 let nextLine = lines[nextIndex]
75 guard nextLine.hasPrefix("+++ ") else { continue }
76
77 let oldPath = String(line.dropFirst(4))
78 let newPath = String(nextLine.dropFirst(4))
79 let looksLikeUnifiedDiff = (oldPath.hasPrefix("a/") || oldPath == "/dev/null") &&
80 (newPath.hasPrefix("b/") || newPath == "/dev/null")
81
82 if looksLikeUnifiedDiff {
83 return index
84 }
85 }
86
87 return nil
88 }
89
90 nonisolated static func isEmailSignatureSeparator(_ line: String) -> Bool {
91 line == "-- " || line == "--"
92 }
93
94 nonisolated static func normalizeLineEndings(in text: String) -> String {
95 text
96 .replacingOccurrences(of: "\r\n", with: "\n")
97 .replacingOccurrences(of: "\r", with: "\n")
98 }
99}