From ccec322f8eac4d14638f5abdae7dae7abc95eb7e Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Wed, 15 Jul 2026 21:42:38 -0500 Subject: refactor: share the email body diff splitter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit segmentMessageBody and its helpers were private to ThreadViewModel, reachable from tests only through a segmentMessageBodyForTesting shim. Patchset review needs the same splitting, because sr.ht's Patch type carries no diff — the diff only exists inside the email body — so this has to be shared rather than duplicated. Moved to InboxThreadUtilities. The shim is gone; the existing test calls the real function directly now. Also adds the Patchset model layer that the coming views build on. --- Hutch/Views/Inbox/InboxThreadUtilities.swift | 88 ++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) (limited to 'Hutch/Views/Inbox/InboxThreadUtilities.swift') diff --git a/Hutch/Views/Inbox/InboxThreadUtilities.swift b/Hutch/Views/Inbox/InboxThreadUtilities.swift index 1dd88a3..6958b50 100644 --- a/Hutch/Views/Inbox/InboxThreadUtilities.swift +++ b/Hutch/Views/Inbox/InboxThreadUtilities.swift @@ -8,4 +8,92 @@ enum InboxThreadUtilities { } return nil } + + /// Splits an email body into its commit message and diff, so patch mail can be + /// rendered as prose plus a diff rather than one undifferentiated blob. + /// + /// Shared by the inbox thread view and patchset review: sr.ht's `Patch` type + /// carries no diff, so the diff has to be recovered from the email body. + nonisolated static func segmentMessageBody(_ body: String, isPatch: Bool) -> [InboxMessageContentBlock] { + guard isPatch else { + let trimmedBody = body.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmedBody.isEmpty ? [] : [.plainText(trimmedBody)] + } + + let normalizedBody = normalizeLineEndings(in: body) + let lines = normalizedBody.components(separatedBy: "\n") + guard let diffStartIndex = actualDiffStartIndex(in: lines) else { + let trimmedBody = normalizedBody.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmedBody.isEmpty ? [] : [.plainText(trimmedBody)] + } + + var blocks: [InboxMessageContentBlock] = [] + let leadingPlainText = lines[.. + let trailingPlainText: String + if let signatureIndex { + diffLines = remainingLines[.. Int? { + if let explicitDiffIndex = lines.firstIndex(where: { $0.hasPrefix("diff --git ") }) { + return explicitDiffIndex + } + + for index in lines.indices { + let line = lines[index] + guard line.hasPrefix("--- ") else { continue } + let nextIndex = lines.index(after: index) + guard nextIndex < lines.endIndex else { continue } + let nextLine = lines[nextIndex] + guard nextLine.hasPrefix("+++ ") else { continue } + + let oldPath = String(line.dropFirst(4)) + let newPath = String(nextLine.dropFirst(4)) + let looksLikeUnifiedDiff = (oldPath.hasPrefix("a/") || oldPath == "/dev/null") && + (newPath.hasPrefix("b/") || newPath == "/dev/null") + + if looksLikeUnifiedDiff { + return index + } + } + + return nil + } + + nonisolated static func isEmailSignatureSeparator(_ line: String) -> Bool { + line == "-- " || line == "--" + } + + nonisolated static func normalizeLineEndings(in text: String) -> String { + text + .replacingOccurrences(of: "\r\n", with: "\n") + .replacingOccurrences(of: "\r", with: "\n") + } } -- cgit v1.2.3