vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Use the rodent emblem as the logo and the favicon
6416cab19d8c205d4ed643399f9a78596f5a31ca · Keenan Nemetz · 13d ago · parent 13cff33f
Use the rodent emblem as the logo and the favicon The header carried a letter in an accent chip and the favicon was a separate "v" glyph, so the forge had two marks that could drift apart. They are now one piece of artwork, served from the binary like every other static asset: a dark rounded tile with a white rodent face, named by the shell as both the icons and the header mark. The source drawing is a tile on a white page. Only the page is removed — a fuzz flood fill from the corner, which leaves the face opaque because the tile encloses it — so the mark has transparent corners and reads on either theme without a second set of assets. Sizes are 128 for the header (22 CSS px, so it survives a high-DPI screen), 32 and 16 for the tab, each square and centred because a favicon that is 31 pixels tall is a favicon some browser will letterbox. favicon.svg is removed; nothing else referenced it. A test now asserts that the three files are served as PNG and that the shell names them, so the next rename cannot leave every page pointing at a missing icon.
server/ui_test.go +50
| @@ -4,6 +4,8 @@ package server_test | |||
| 4 | 4 | // normal page must all come back as Vole HTML, not as a bare Go error string. | |
| 5 | 5 | ||
| 6 | 6 | import ( | |
| 7 | "io" | ||
| 8 | "net/http" | ||
| 7 | 9 | "strings" | |
| 8 | 10 | "testing" | |
| 9 | 11 | "time" | |
| @@ -84,6 +86,54 @@ func TestStaticAssetsAreServedFromTheBinary(t *testing.T) { | |||
| 84 | 86 | } | |
| 85 | 87 | } | |
| 86 | 88 | ||
| 89 | // TestTheEmblemShipsAndTheShellNamesIt: the mark and the favicon are one file, | ||
| 90 | // so a page that points at a name nobody ships is a broken tab icon on every | ||
| 91 | // page. This is the test that would have caught the rename. | ||
| 92 | func TestTheEmblemShipsAndTheShellNamesIt(t *testing.T) { | ||
| 93 | t.Parallel() | ||
| 94 | f := newFixture(t) | ||
| 95 | |||
| 96 | for _, name := range []string{"logo.png", "favicon-32.png", "favicon-16.png"} { | ||
| 97 | resp, err := http.Get(f.http.URL + "/static/" + name) | ||
| 98 | if err != nil { | ||
| 99 | t.Fatal(err) | ||
| 100 | } | ||
| 101 | if resp.StatusCode != http.StatusOK { | ||
| 102 | resp.Body.Close() | ||
| 103 | t.Fatalf("/static/%s = %d", name, resp.StatusCode) | ||
| 104 | } | ||
| 105 | if ct := resp.Header.Get("Content-Type"); !strings.Contains(ct, "png") { | ||
| 106 | resp.Body.Close() | ||
| 107 | t.Fatalf("%s Content-Type = %q", name, ct) | ||
| 108 | } | ||
| 109 | head := make([]byte, 8) | ||
| 110 | if _, err := io.ReadFull(resp.Body, head); err != nil { | ||
| 111 | resp.Body.Close() | ||
| 112 | t.Fatal(err) | ||
| 113 | } | ||
| 114 | resp.Body.Close() | ||
| 115 | if string(head) != "\x89PNG\r\n\x1a\n" { | ||
| 116 | t.Errorf("%s is not a PNG: %x", name, head) | ||
| 117 | } | ||
| 118 | } | ||
| 119 | |||
| 120 | code, shell := f.get("/") | ||
| 121 | if code != http.StatusOK { | ||
| 122 | t.Fatalf("landing page = %d", code) | ||
| 123 | } | ||
| 124 | if !strings.Contains(shell, `href="/static/favicon-32.png"`) { | ||
| 125 | t.Error("the shell does not name the emblem as its icon") | ||
| 126 | } | ||
| 127 | if !strings.Contains(shell, `src="/static/logo.png"`) { | ||
| 128 | t.Error("the header does not use the emblem") | ||
| 129 | } | ||
| 130 | for _, gone := range []string{"logo.svg", "favicon.svg"} { | ||
| 131 | if strings.Contains(shell, gone) { | ||
| 132 | t.Errorf("the shell still points at the removed %s", gone) | ||
| 133 | } | ||
| 134 | } | ||
| 135 | } | ||
| 136 | |||
| 87 | 137 | // The celebration library is a static asset like any other, but it is not in the | |
| 88 | 138 | // shell: the shell would make every page load fetch it. app.js names it, and names | |
| 89 | 139 | // it once, so the only request is the one a landing asks for. | |