Commit 63507dba22

63507dba22acff4fee2b2a7e19bf3227c1ea498a

parent: d036624920

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-07 03:26 UTC

Render issue/MR bodies and comments in their stored format

IssueView and MRView rendered every body and comment through
MarkdownView unconditionally, so an org-authored body or comment
flattened to plain text on the same screen that wrote it. Add
FormattedBodyView, which dispatches on a stored format the way
ReadmeView dispatches on file extension, and use it for both bodies
and comments. IssueDetail.Comment and MRDetail.MRComment gained
body_format decoding, which the server already emits but nothing
read; a comment with no stored format still renders as markdown.

Also tighten anIssueEditCarriesItsFormat to assert full argv equality
instead of two independent contains() checks, matching its MR sibling.

Layout: unified · split

gitbay/Issues/IssueModels.swift +2
@@ -46,10 +46,12 @@ nonisolated struct IssueDetail: Decodable, Sendable, Hashable {
4646 nonisolated struct Comment: Decodable, Sendable, Hashable, Identifiable {
4747 let author: String
4848 let body: String
49 let bodyFormat: String?
4950 let createdAt: Date
5051
5152 enum CodingKeys: String, CodingKey {
5253 case author, body
54 case bodyFormat = "body_format"
5355 case createdAt = "created_at"
5456 }
5557
gitbay/MRs/MRModels.swift +2
@@ -156,10 +156,12 @@ nonisolated struct MRDetail: Decodable, Sendable, Hashable {
156156 nonisolated struct MRComment: Decodable, Sendable, Hashable, Identifiable {
157157 let author: String
158158 let body: String
159 let bodyFormat: String?
159160 let createdAt: Date
160161
161162 enum CodingKeys: String, CodingKey {
162163 case author, body
164 case bodyFormat = "body_format"
163165 case createdAt = "created_at"
164166 }
165167
gitbay/Views/Issues/IssueView.swift +2 −2
@@ -31,7 +31,7 @@ struct IssueView: View {
3131
3232 if let body = issue.body, !body.isEmpty {
3333 Section {
34 MarkdownView(markdown: body)
34 FormattedBodyView(text: body, format: issue.bodyFormat)
3535 .padding(.vertical, 4)
3636 }
3737 }
@@ -194,7 +194,7 @@ struct IssueView: View {
194194 .font(.gbSans(.caption))
195195 .foregroundStyle(.tertiary)
196196 }
197 MarkdownView(markdown: comment.body)
197 FormattedBodyView(text: comment.body, format: comment.bodyFormat)
198198 .font(.gbSans(.subheadline))
199199 }
200200 .padding(.vertical, 2)
gitbay/Views/MRs/MRView.swift +2 −2
@@ -34,7 +34,7 @@ struct MRView: View {
3434
3535 if let body = mr.body, !body.isEmpty {
3636 Section {
37 MarkdownView(markdown: body)
37 FormattedBodyView(text: body, format: mr.bodyFormat)
3838 .padding(.vertical, 4)
3939 }
4040 }
@@ -397,7 +397,7 @@ struct MRView: View {
397397 .font(.gbSans(.caption))
398398 .foregroundStyle(.tertiary)
399399 }
400 MarkdownView(markdown: comment.body)
400 FormattedBodyView(text: comment.body, format: comment.bodyFormat)
401401 .font(.gbSans(.subheadline))
402402 }
403403 .padding(.vertical, 2)
gitbay/Views/Repos/FormattedBodyView.swift added +28
@@ -0,0 +1,28 @@
1import OrgSwiftUI
2import 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.
8struct 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 {
8383 #expect(mr.checksCombined == "success")
8484 #expect(mr.unresolvedThreads == 2)
8585 }
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 }
86125}
87126
88127@MainActor
@@ -141,9 +180,10 @@ struct BodyFormatWriteTests {
141180 stub.enqueue(.init(status: 200, json: issueJSON))
142181 await model.edit(title: "t2", body: "b2", format: "md")
143182
144 let argv = try argvOf(try #require(stub.seen.first { $0.method == "POST" }))
145 #expect(argv.contains("--format"))
146 #expect(argv.contains("md"))
183 let write = try #require(stub.seen.first { $0.method == "POST" })
184 #expect(try argvOf(write)
185 == ["issue", "edit", "krz/gitbay", "7", "--title", "t2", "--file", "-", "--format", "md"])
186 #expect(try stdinOf(write) == "b2")
147187 }
148188}
149189