Commit a997bedea1

a997bedea13d1e8a109271c3966bc0d0e7c0e477

parent: 768fd2e0ca

Verified · cmc ci/build: success

cmc <hello@cleberg.net> · 2026-08-31 15:29 UTC

deps: pad the issue table so the mail is legible

The body is read twice: as a rendered table on the web, and as the plain
text of the notification mail. Padding every cell to its column width is
ignored by the renderer and is the only thing keeping the mail's columns
readable, so one body serves both.

Closes #61

Layout: unified · split

internal/deps/report.go +29 −2
@@ -39,10 +39,37 @@ func Body(branch string, reports []store.DepReport) string {
39 continue 39 continue
40 } 40 }
41 sort.Slice(rows, func(i, j int) bool { return rows[i].Name < rows[j].Name }) 41 sort.Slice(rows, func(i, j int) bool { return rows[i].Name < rows[j].Name })
42 fmt.Fprintf(&b, "\n### %s\n\n| Package | Current | Latest |\n| --- | --- | --- |\n", ecosystemNames[eco]) 42 fmt.Fprintf(&b, "\n### %s\n\n", ecosystemNames[eco])
43 cells := [][3]string{{"Package", "Current", "Latest"}}
43 for _, r := range rows { 44 for _, r := range rows {
44 fmt.Fprintf(&b, "| `%s` | %s | %s |\n", r.Name, r.Current, r.Latest) 45 cells = append(cells, [3]string{"`" + r.Name + "`", r.Current, r.Latest})
45 } 46 }
47 b.WriteString(table(cells))
48 }
49 return b.String()
50}
51
52// table pads every cell to its column, because the body is read twice: as
53// a rendered table on the web, where the padding is ignored, and as the
54// plain text of the notification mail, where it is the only thing keeping
55// the columns readable. Row 0 is the header.
56func table(cells [][3]string) string {
57 var w [3]int
58 for _, c := range cells {
59 for i := range w {
60 if n := len(c[i]); n > w[i] {
61 w[i] = n
62 }
63 }
64 }
65 var b strings.Builder
66 row := func(c [3]string) {
67 fmt.Fprintf(&b, "| %-*s | %-*s | %-*s |\n", w[0], c[0], w[1], c[1], w[2], c[2])
68 }
69 row(cells[0])
70 row([3]string{strings.Repeat("-", w[0]), strings.Repeat("-", w[1]), strings.Repeat("-", w[2])})
71 for _, c := range cells[1:] {
72 row(c)
46 } 73 }
47 return b.String() 74 return b.String()
48} 75}
internal/deps/report_test.go added +43
@@ -0,0 +1,43 @@
1package deps
2
3import (
4 "strings"
5 "testing"
6
7 "gitbay.org/gitbay/internal/store"
8)
9
10// The body is read as a rendered table on the web and as the plain text of
11// the notification mail, so every row has to line up on its own.
12func TestBodyAlignsColumns(t *testing.T) {
13 body := Body("main", []store.DepReport{
14 {Ecosystem: EcoGo, Name: "go.yaml.in/yaml/v3", Current: "v3.0.4", Latest: "v3.0.5"},
15 {Ecosystem: EcoGo, Name: "golang.org/x/net", Current: "v0.57.0", Latest: "v0.58.0"},
16 {Ecosystem: EcoNPM, Name: "react", Current: "18.2.0", Latest: "19.0.0"},
17 })
18 var pipes []int
19 for _, line := range strings.Split(body, "\n") {
20 if !strings.HasPrefix(line, "|") {
21 pipes = append(pipes, -1) // section break
22 continue
23 }
24 pipes = append(pipes, len(line))
25 }
26 // Within a run of table lines every line is the same length.
27 var run []int
28 for _, n := range append(pipes, -1) {
29 if n == -1 {
30 for _, w := range run {
31 if w != run[0] {
32 t.Fatalf("ragged table in:\n%s", body)
33 }
34 }
35 run = nil
36 continue
37 }
38 run = append(run, n)
39 }
40 if !strings.Contains(body, "| `golang.org/x/net` | v0.57.0 | v0.58.0 |") {
41 t.Errorf("row not padded to its column:\n%s", body)
42 }
43}