Commit a997bedea1
a997bedea13d1e8a109271c3966bc0d0e7c0e477
parent: 768fd2e0ca
Verified · cmc ci/build: success
cmc <hello@cleberg.net> · 2026-08-31T15:29:14Z
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
internal/deps/report.go
+29 −2
| @@ -39,10 +39,37 @@ func Body(branch string, reports []store.DepReport) string { |
| 39 | 39 | continue |
| 40 | 40 | } |
| 41 | 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 | 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. |
| 56 | func 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 | 74 | return b.String() |
| 48 | 75 | } |
internal/deps/report_test.go
added
+43
| @@ -0,0 +1,43 @@ |
| 1 | package deps |
| 2 | |
| 3 | import ( |
| 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. |
| 12 | func 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 | } |