deps: pad the issue table so the mail is legible !132
2 files changed, +72 −2
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. | ||
| 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 | return b.String() | 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 | } | ||