Commit 63507dba22
Verified · cmc
Layout: unified · split
gitbay/Issues/IssueModels.swift +2
| @@ -46,10 +46,12 @@ nonisolated struct IssueDetail: Decodable, Sendable, Hashable { | |||
| 46 | nonisolated struct Comment: Decodable, Sendable, Hashable, Identifiable { | 46 | nonisolated struct Comment: Decodable, Sendable, Hashable, Identifiable { |
| 47 | let author: String | 47 | let author: String |
| 48 | let body: String | 48 | let body: String |
| 49 | let bodyFormat: String? | ||
| 49 | let createdAt: Date | 50 | let createdAt: Date |
| 50 | 51 | ||
| 51 | enum CodingKeys: String, CodingKey { | 52 | enum CodingKeys: String, CodingKey { |
| 52 | case author, body | 53 | case author, body |
| 54 | case bodyFormat = "body_format" | ||
| 53 | case createdAt = "created_at" | 55 | case createdAt = "created_at" |
| 54 | } | 56 | } |
| 55 | 57 | ||
gitbay/MRs/MRModels.swift +2
| @@ -156,10 +156,12 @@ nonisolated struct MRDetail: Decodable, Sendable, Hashable { | |||
| 156 | nonisolated struct MRComment: Decodable, Sendable, Hashable, Identifiable { | 156 | nonisolated struct MRComment: Decodable, Sendable, Hashable, Identifiable { |
| 157 | let author: String | 157 | let author: String |
| 158 | let body: String | 158 | let body: String |
| 159 | let bodyFormat: String? | ||
| 159 | let createdAt: Date | 160 | let createdAt: Date |
| 160 | 161 | ||
| 161 | enum CodingKeys: String, CodingKey { | 162 | enum CodingKeys: String, CodingKey { |
| 162 | case author, body | 163 | case author, body |
| 164 | case bodyFormat = "body_format" | ||
| 163 | case createdAt = "created_at" | 165 | case createdAt = "created_at" |
| 164 | } | 166 | } |
| 165 | 167 | ||
gitbay/Views/Issues/IssueView.swift +2 −2
| @@ -31,7 +31,7 @@ struct IssueView: View { | |||
| 31 | 31 | ||
| 32 | if let body = issue.body, !body.isEmpty { | 32 | if let body = issue.body, !body.isEmpty { |
| 33 | Section { | 33 | Section { |
| 34 | MarkdownView(markdown: body) | 34 | FormattedBodyView(text: body, format: issue.bodyFormat) |
| 35 | .padding(.vertical, 4) | 35 | .padding(.vertical, 4) |
| 36 | } | 36 | } |
| 37 | } | 37 | } |
| @@ -194,7 +194,7 @@ struct IssueView: View { | |||
| 194 | .font(.gbSans(.caption)) | 194 | .font(.gbSans(.caption)) |
| 195 | .foregroundStyle(.tertiary) | 195 | .foregroundStyle(.tertiary) |
| 196 | } | 196 | } |
| 197 | MarkdownView(markdown: comment.body) | 197 | FormattedBodyView(text: comment.body, format: comment.bodyFormat) |
| 198 | .font(.gbSans(.subheadline)) | 198 | .font(.gbSans(.subheadline)) |
| 199 | } | 199 | } |
| 200 | .padding(.vertical, 2) | 200 | .padding(.vertical, 2) |
gitbay/Views/MRs/MRView.swift +2 −2
| @@ -34,7 +34,7 @@ struct MRView: View { | |||
| 34 | 34 | ||
| 35 | if let body = mr.body, !body.isEmpty { | 35 | if let body = mr.body, !body.isEmpty { |
| 36 | Section { | 36 | Section { |
| 37 | MarkdownView(markdown: body) | 37 | FormattedBodyView(text: body, format: mr.bodyFormat) |
| 38 | .padding(.vertical, 4) | 38 | .padding(.vertical, 4) |
| 39 | } | 39 | } |
| 40 | } | 40 | } |
| @@ -397,7 +397,7 @@ struct MRView: View { | |||
| 397 | .font(.gbSans(.caption)) | 397 | .font(.gbSans(.caption)) |
| 398 | .foregroundStyle(.tertiary) | 398 | .foregroundStyle(.tertiary) |
| 399 | } | 399 | } |
| 400 | MarkdownView(markdown: comment.body) | 400 | FormattedBodyView(text: comment.body, format: comment.bodyFormat) |
| 401 | .font(.gbSans(.subheadline)) | 401 | .font(.gbSans(.subheadline)) |
| 402 | } | 402 | } |
| 403 | .padding(.vertical, 2) | 403 | .padding(.vertical, 2) |
gitbay/Views/Repos/FormattedBodyView.swift added +28
| @@ -0,0 +1,28 @@ | |||
| 1 | import OrgSwiftUI | ||
| 2 | import SwiftUI | ||
| 3 | |||
| 4 | /// Renders an issue/MR body or comment in whichever format it was stored as. | ||
| 5 | /// Mirrors `ReadmeView`'s dispatch, keyed on the stored `body_format` rather | ||
| 6 | /// than a file name. A missing format renders as markdown, matching every | ||
| 7 | /// body written before org support existed. | ||
| 8 | struct FormattedBodyView: View { | ||
| 9 | let text: String | ||
| 10 | var format: String? | ||
| 11 | |||
| 12 | @Environment(\.colorScheme) private var colorScheme | ||
| 13 | |||
| 14 | var isOrg: Bool { format == "org" } | ||
| 15 | |||
| 16 | var body: some View { | ||
| 17 | if isOrg { | ||
| 18 | // Qualified: `OrgView` here is org-mode, not the organization view. | ||
| 19 | OrgSwiftUI.OrgView( | ||
| 20 | text, | ||
| 21 | styler: OrgSyntaxStyler(colorScheme: colorScheme), | ||
| 22 | keywords: OrgKeywordStyle(todo: .gbOK, done: .gbDone) | ||
| 23 | ) | ||
| 24 | } else { | ||
| 25 | MarkdownView(markdown: text) | ||
| 26 | } | ||
| 27 | } | ||
| 28 | } | ||
gitbayTests/BodyFormatTests.swift +43 −3
| @@ -83,6 +83,45 @@ struct BodyFormatDecodingTests { | |||
| 83 | #expect(mr.checksCombined == "success") | 83 | #expect(mr.checksCombined == "success") |
| 84 | #expect(mr.unresolvedThreads == 2) | 84 | #expect(mr.unresolvedThreads == 2) |
| 85 | } | 85 | } |
| 86 | |||
| 87 | /// body_format is omitempty on a comment too; an ordinary one carries no key. | ||
| 88 | @Test func anIssueCommentWithoutAStoredFormatDecodesNil() throws { | ||
| 89 | let issue = try decodeIssue(""" | ||
| 90 | {"number":7,"title":"t","state":"open","author":"cmc",\ | ||
| 91 | "created_at":"2026-09-01T00:00:00Z","comments":[{"author":"rae",\ | ||
| 92 | "body":"hi","created_at":"2026-09-01T00:00:00Z"}]} | ||
| 93 | """) | ||
| 94 | #expect(issue.comments?.first?.bodyFormat == nil) | ||
| 95 | } | ||
| 96 | |||
| 97 | @Test func anIssueCommentStoredAsOrgSaysSo() throws { | ||
| 98 | let issue = try decodeIssue(""" | ||
| 99 | {"number":7,"title":"t","state":"open","author":"cmc",\ | ||
| 100 | "created_at":"2026-09-01T00:00:00Z","comments":[{"author":"rae",\ | ||
| 101 | "body":"hi","body_format":"org","created_at":"2026-09-01T00:00:00Z"}]} | ||
| 102 | """) | ||
| 103 | #expect(issue.comments?.first?.bodyFormat == "org") | ||
| 104 | } | ||
| 105 | |||
| 106 | @Test func aMergeRequestCommentWithoutAStoredFormatDecodesNil() throws { | ||
| 107 | let mr = try decodeMR(""" | ||
| 108 | {"number":7,"title":"t","state":"open","author":"cmc","source":"f",\ | ||
| 109 | "target_ref":"main","head_sha":"a","created_at":"2026-09-01T00:00:00Z",\ | ||
| 110 | "comments":[{"author":"rae","body":"hi",\ | ||
| 111 | "created_at":"2026-09-01T00:00:00Z"}]} | ||
| 112 | """) | ||
| 113 | #expect(mr.comments?.first?.bodyFormat == nil) | ||
| 114 | } | ||
| 115 | |||
| 116 | @Test func aMergeRequestCommentStoredAsOrgSaysSo() throws { | ||
| 117 | let mr = try decodeMR(""" | ||
| 118 | {"number":7,"title":"t","state":"open","author":"cmc","source":"f",\ | ||
| 119 | "target_ref":"main","head_sha":"a","created_at":"2026-09-01T00:00:00Z",\ | ||
| 120 | "comments":[{"author":"rae","body":"hi","body_format":"org",\ | ||
| 121 | "created_at":"2026-09-01T00:00:00Z"}]} | ||
| 122 | """) | ||
| 123 | #expect(mr.comments?.first?.bodyFormat == "org") | ||
| 124 | } | ||
| 86 | } | 125 | } |
| 87 | 126 | ||
| 88 | @MainActor | 127 | @MainActor |
| @@ -141,9 +180,10 @@ struct BodyFormatWriteTests { | |||
| 141 | stub.enqueue(.init(status: 200, json: issueJSON)) | 180 | stub.enqueue(.init(status: 200, json: issueJSON)) |
| 142 | await model.edit(title: "t2", body: "b2", format: "md") | 181 | await model.edit(title: "t2", body: "b2", format: "md") |
| 143 | 182 | ||
| 144 | let argv = try argvOf(try #require(stub.seen.first { $0.method == "POST" })) | 183 | let write = try #require(stub.seen.first { $0.method == "POST" }) |
| 145 | #expect(argv.contains("--format")) | 184 | #expect(try argvOf(write) |
| 146 | #expect(argv.contains("md")) | 185 | == ["issue", "edit", "krz/gitbay", "7", "--title", "t2", "--file", "-", "--format", "md"]) |
| 186 | #expect(try stdinOf(write) == "b2") | ||
| 147 | } | 187 | } |
| 148 | } | 188 | } |
| 149 | 189 | ||