Monorepo for Tangled tangled.org

pulls: render trailing git trailers as separate lines in pull body markdown #1128

open opened by nolith.dev targeting master from nolith.dev/tangled-core: pulls-fix-git-trailers

Pull descriptions ending with git trailers (for example Co-authored-by, Reviewed-by, Ticket) were rendered as a single wrapped paragraph because markdown treats single newlines as soft breaks. Normalize only trailing trailer blocks in pull bodies by converting trailer line separators to markdown hard line breaks (" \n"), then render through the existing markdown and sanitizer pipeline.

This keeps normal markdown behavior unchanged and avoids custom HTML rendering logic. The pull header template now uses markdownPullBody, and tests cover trailer normalization plus unchanged non-trailer cases.

AI-assisted: OpenCode (Openai GPT-5.3 Codex) Signed-off-by: Alessio Caiazza code.git@caiazza.info

Labels

None yet.

assignee

None yet.

Participants 2
AT URI
at://did:plc:nzep3slobztdph3kxswzbing/sh.tangled.repo.pull/3mgnw3arzwg22
+81 -2
Diff #1
+40 -2
appview/pulls/pulls.go
··· 12 12 "log" 13 13 "log/slog" 14 14 "net/http" 15 + "regexp" 15 16 "slices" 16 17 "sort" 17 18 "strconv" ··· 52 53 53 54 const ApplicationGzip = "application/gzip" 54 55 56 + const markdownHardLineBreak = " \n" 57 + 58 + var gitTrailersRegex = regexp.MustCompile("" + 59 + // blank line separating body from trailers 60 + `\n\n` + 61 + // capture group 1: entire trailer block 62 + `(` + 63 + // trailer lines with optional continuation lines 64 + `(?:` + 65 + `[A-Za-z0-9][A-Za-z0-9-]*:\s+[^\n]+` + 66 + `(?:\n[ \t]+[^\n]+)*` + 67 + `(?:\n|$)` + 68 + `)+` + 69 + `)` + 70 + // optional trailing whitespace 71 + `[ \t\r\n]*` + 72 + // must be at end of string 73 + `$`, 74 + ) 75 + 55 76 type Pulls struct { 56 77 oauth *oauth.OAuth 57 78 repoResolver *reporesolver.RepoResolver ··· 1295 1316 title = formatPatches[0].Title 1296 1317 } 1297 1318 if body == "" { 1298 - body = formatPatches[0].Body 1319 + body = normalizeAutoPullBodyTrailers(formatPatches[0].Body) 1299 1320 } 1300 1321 } 1301 1322 ··· 2512 2533 } 2513 2534 2514 2535 title := fp.Title 2515 - body := fp.Body 2536 + body := normalizeAutoPullBodyTrailers(fp.Body) 2516 2537 rkey := tid.TID() 2517 2538 2518 2539 mentions, references := s.mentionsResolver.Resolve(ctx, body) ··· 2559 2580 } 2560 2581 2561 2582 func ptrPullState(s models.PullState) *models.PullState { return &s } 2583 + 2584 + func normalizeAutoPullBodyTrailers(text string) string { 2585 + normalized := strings.ReplaceAll(text, "\r\n", "\n") 2586 + match := gitTrailersRegex.FindStringSubmatchIndex(normalized) 2587 + if len(match) < 4 { 2588 + return text 2589 + } 2590 + 2591 + mainBody := strings.TrimRight(normalized[:match[0]], "\n") 2592 + rawTrailerBlock := strings.TrimSpace(normalized[match[2]:match[3]]) 2593 + if rawTrailerBlock == "" { 2594 + return text 2595 + } 2596 + 2597 + trailerBlock := strings.ReplaceAll(rawTrailerBlock, "\n", markdownHardLineBreak) 2598 + return mainBody + "\n\n" + trailerBlock 2599 + }
+41
appview/pulls/pulls_trailers_test.go
··· 1 + package pulls 2 + 3 + import "testing" 4 + 5 + func TestNormalizeAutoPullBodyTrailers(t *testing.T) { 6 + tests := []struct { 7 + name string 8 + input string 9 + want string 10 + }{ 11 + { 12 + name: "trailing trailers are normalized", 13 + input: "This is a placeholder commit message with multiple paragraphs\n\nThe content does not matter.\n\nCo-authored-by: Developer One <dev1@example.com>\nReviewed-by: Reviewer Two <reviewer@example.com>\nTicket: PROJ-123\n", 14 + want: "This is a placeholder commit message with multiple paragraphs\n\nThe content does not matter.\n\nCo-authored-by: Developer One <dev1@example.com> \nReviewed-by: Reviewer Two <reviewer@example.com> \nTicket: PROJ-123", 15 + }, 16 + { 17 + name: "regular prose with colons is unchanged", 18 + input: "Body\n\nThis line: has extra words and should not match\nsecond plain line", 19 + want: "Body\n\nThis line: has extra words and should not match\nsecond plain line", 20 + }, 21 + { 22 + name: "no blank line separator means unchanged", 23 + input: "Body\nTicket: PROJ-123", 24 + want: "Body\nTicket: PROJ-123", 25 + }, 26 + { 27 + name: "trailer continuation line is normalized", 28 + input: "Body\n\nReviewed-by: Reviewer\n\tAdditional context\nTicket: PROJ-123\n", 29 + want: "Body\n\nReviewed-by: Reviewer \n\tAdditional context \nTicket: PROJ-123", 30 + }, 31 + } 32 + 33 + for _, tt := range tests { 34 + t.Run(tt.name, func(t *testing.T) { 35 + got := normalizeAutoPullBodyTrailers(tt.input) 36 + if got != tt.want { 37 + t.Fatalf("normalized body = %q, want %q", got, tt.want) 38 + } 39 + }) 40 + } 41 + }

History

2 rounds 4 comments
sign up or login to add to the discussion
1 commit
expand
pulls: normalize auto-generated trailer blocks in PR bodies
no conflicts, ready to merge
expand 1 comment

Unfortunately the pull description is still the old one :(

1 commit
expand
pulls: render trailing git trailers as separate lines in pull body markdown
expand 3 comments

The pull body shows exactly the problem I'm fixing.

gitTrailersRegex could be written as online, but even the complexity of the regex I went for a documented multiline version of it.

Thanks for the PR! Can we add that extra newline on auto PR description creation? Then we won't need special markdownPullBody.

Ideally I think we shouldn't even try to parse comment body as markdown, but that's another story (working on it!)

sure, I went for rendering time to fix the existing pulls too, but either options are fine.