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 {
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 @@
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 {
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