Commit 4e7f0573d0
Verified · cmc
e2e/design_test.go +34 −7
| @@ -139,15 +139,42 @@ func TestReadmeRelativeLinks(t *testing.T) { | ||
| 139 | 139 | t.Errorf("explore row missing %q", want) |
| 140 | 140 | } |
| 141 | 141 | } |
| 142 | // The repo header renders the same on every tab of the repo. It sits | |
| 143 | // above the tab bar, so anything that appears on one tab and not | |
| 144 | // another moves the navigation between clicks of that navigation. | |
| 145 | for _, page := range []string{"", "/issues", "/mrs", "/releases"} { | |
| 146 | _, body := inst.get(t, "/alice/site"+page) | |
| 147 | if !strings.Contains(body, `href="/explore?q=web"`) { | |
| 148 | t.Errorf("repo header topics missing on /alice/site%s", page) | |
| 142 | // The description and topics belong to the code tab; task tabs render | |
| 143 | // only the identity row and the tab bar. | |
| 144 | for _, p := range []string{"/alice/site/issues", "/alice/site/mrs", "/alice/site/releases"} { | |
| 145 | if _, body := inst.get(t, p); strings.Contains(body, `class="chip topic"`) { | |
| 146 | t.Errorf("%s: header still carries topics on a task tab", p) | |
| 149 | 147 | } |
| 150 | 148 | } |
| 149 | if _, body := inst.get(t, "/alice/site"); !strings.Contains(body, `class="chip topic"`) { | |
| 150 | t.Error("repo home lost its topics") | |
| 151 | } | |
| 152 | } | |
| 153 | ||
| 154 | // TestLandingRoutes checks the landing page's copy and the two routes. | |
| 155 | func TestLandingRoutes(t *testing.T) { | |
| 156 | smtp := startFakeSMTP(t) | |
| 157 | inst := startInstanceWith(t, fmt.Sprintf( | |
| 158 | "[web]\nmode = \"accounts\"\n[registration]\nmode = \"open\"\n[mail]\nsmtp_host = %q\nfrom = \"noreply@gitbay.test\"\n", | |
| 159 | smtp.addr)) | |
| 160 | _, body := inst.get(t, "/") | |
| 161 | for _, want := range []string{ | |
| 162 | "A git forge you drive from the terminal.", | |
| 163 | `class="button primary" href="/explore">Explore repositories</a>`, | |
| 164 | `class="button btn" href="/register">Create an account</a>`, | |
| 165 | "web login</code>", | |
| 166 | } { | |
| 167 | if !strings.Contains(body, want) { | |
| 168 | t.Errorf("landing lacks %q", want) | |
| 169 | } | |
| 170 | } | |
| 171 | if strings.Contains(body, "is the whole onboarding") { | |
| 172 | t.Error("landing still calls repo create the whole onboarding") | |
| 173 | } | |
| 174 | _, reg := inst.get(t, "/register") | |
| 175 | if !strings.Contains(reg, "Paste the contents of your public key file") || !strings.Contains(reg, "/krz/gitbay/wiki/SSH-keys") { | |
| 176 | t.Error("register page lacks the key hint or the wiki link") | |
| 177 | } | |
| 151 | 178 | } |
| 152 | 179 | |
| 153 | 180 | func TestWebInteractions(t *testing.T) { |
internal/httpd/web.go +11 −4
| @@ -161,13 +161,20 @@ func (s *Server) index(w http.ResponseWriter, r *http.Request) { | ||
| 161 | 161 | s.cfg.Server.SiteURL, "https://"), "http://"), "/") |
| 162 | 162 | s.render(w, "landing.html", struct { |
| 163 | 163 | basePage |
| 164 | Host string | |
| 165 | Accounts bool | |
| 166 | Signup bool | |
| 164 | Host string | |
| 165 | Accounts bool | |
| 166 | Signup bool | |
| 167 | Picture bool | |
| 168 | EmailLogin bool | |
| 167 | 169 | }{basePage{Site: s.siteName(), Host: s.cfg.SiteHost()}, host, s.cfg.Web.Mode == "accounts", |
| 168 | s.cfg.Web.Mode == "accounts" && s.cfg.Registration.Mode != "closed"}) | |
| 170 | s.cfg.Web.Mode == "accounts" && s.cfg.Registration.Mode != "closed", | |
| 171 | landingPicture, s.emailLoginEnabled()}) | |
| 169 | 172 | } |
| 170 | 173 | |
| 174 | // landingPicture says whether the landing page's screenshot images exist to | |
| 175 | // show. Task 15 replaces this with a check of the embedded images. | |
| 176 | var landingPicture = false | |
| 177 | ||
| 171 | 178 | func (s *Server) dashboard(w http.ResponseWriter, r *http.Request, viewer store.User) { |
| 172 | 179 | pinned, _ := s.st.PinnedRepos(viewer.ID) |
| 173 | 180 | var visible []store.Repo |
internal/web/static/style.css +1 −2
| @@ -417,7 +417,6 @@ footer a:hover { color: var(--link); } | ||
| 417 | 417 | font-size: var(--fs-2); |
| 418 | 418 | display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; |
| 419 | 419 | } |
| 420 | .repohead .topics { margin: var(--sp-2) 0 0; } | |
| 421 | 420 | .repometa { margin: var(--sp-1) 0 0; color: var(--muted); font-size: var(--fs-1); } |
| 422 | 421 | .toggles { margin: var(--sp-2) 0 0; color: var(--muted); font-size: var(--fs-1); } |
| 423 | 422 | |
| @@ -1369,6 +1368,7 @@ details.refmenu .refdrop a.allrefs { | ||
| 1369 | 1368 | readable on the templates that do not set one yet */ |
| 1370 | 1369 | .landing { max-width: 48rem; padding: var(--sp-6) 0; } |
| 1371 | 1370 | .landing h1 { font-size: var(--fs-6); display: flex; align-items: center; gap: var(--sp-3); margin-bottom: var(--sp-3); } |
| 1371 | .landing h1 .mark { width: 32px; height: 32px; } | |
| 1372 | 1372 | .lede { font-size: var(--fs-4); color: var(--muted); line-height: 1.5; max-width: 40rem; margin: 0 0 var(--sp-5); } |
| 1373 | 1373 | pre.quickstart { margin: 0 0 var(--sp-4); } |
| 1374 | 1374 | .shot { border: 1px solid var(--line); border-radius: var(--r-card); overflow: hidden; margin: var(--sp-5) 0; } |
| @@ -1377,7 +1377,6 @@ pre.quickstart { margin: 0 0 var(--sp-4); } | ||
| 1377 | 1377 | .facets h2 { font-size: var(--fs-3); margin: 0 0 var(--sp-1); } |
| 1378 | 1378 | .facets p { color: var(--muted); font-size: var(--fs-2); margin: 0; } |
| 1379 | 1379 | .routes { display: flex; gap: var(--sp-3); align-items: center; margin: var(--sp-5) 0 var(--sp-3); } |
| 1380 | .explorelink { font-weight: 500; } | |
| 1381 | 1380 | |
| 1382 | 1381 | /* ---- profile ---- */ |
| 1383 | 1382 | .profilehead { border-bottom: 1px solid var(--faint); padding-bottom: var(--sp-4); margin-bottom: var(--sp-4); } |
internal/web/templates/landing.html +13 −11
| @@ -1,19 +1,21 @@ | ||
| 1 | {{define "width"}}bounded{{end}} | |
| 2 | 1 | {{define "title"}}{{.Site}}{{end}} |
| 2 | {{define "width"}}bounded{{end}} | |
| 3 | 3 | {{define "content"}} |
| 4 | 4 | <div class="landing"> |
| 5 | <h1>{{.Site}}</h1> | |
| 6 | <p class="lede">A CLI-first git forge. SSH is the API: repositories, issues, and | |
| 7 | merge requests are managed from your terminal with nothing but OpenSSH — | |
| 8 | the web is a fast, readable rendering of that state.</p> | |
| 9 | <pre class="quickstart">ssh git@{{.Host}} help # every command, no client needed | |
| 5 | <h1>{{template "mark"}}{{.Site}}</h1> | |
| 6 | <p class="lede">A git forge you drive from the terminal. Repositories, issues, merge requests and CI over SSH, with a fast, readable web view of the same state.</p> | |
| 7 | <pre class="quickstart">ssh git@{{.Host}} help # every command, no client to install | |
| 10 | 8 | git clone ssh://git@{{.Host}}/owner/repo.git</pre> |
| 9 | {{if .Picture}}<div class="shot"><picture> | |
| 10 | <source srcset="/static/img/mr-dark.png" media="(prefers-color-scheme: dark)"> | |
| 11 | <img src="/static/img/mr-light.png" width="1280" height="900" alt="A merge request page: the conversation on the left, checks and reviewers on the right."> | |
| 12 | </picture></div>{{end}} | |
| 11 | 13 | <div class="facets"> |
| 12 | <section><h2>Read</h2><p>Browse and clone any public repository, over HTTPS or <code>git://</code>, with no account. Every commit shows whether its signature was verified.</p></section> | |
| 13 | <section><h2>Write</h2><p>Push over SSH with the key you already have. <code>ssh git@{{.Host}} repo create you/project</code> is the whole onboarding; issues, merge requests and reviews are commands too.</p></section> | |
| 14 | <section><h2>Review</h2><p>The web is a rendering of the same state: read a diff, comment on a line, approve, merge. Everything it can do, the terminal can do first.</p></section> | |
| 14 | <section><h2>Read</h2><p>Browse and clone any public repository over HTTPS or <code>git://</code>, no account. Every commit shows whether its signature verified.</p></section> | |
| 15 | <section><h2>Write</h2><p>Push over SSH with the key you already have. Create a repository, file an issue, open and merge a request, all as commands.</p></section> | |
| 16 | <section><h2>Review</h2><p>Read a diff, comment on a line, approve, merge, in the browser or the terminal.</p></section> | |
| 15 | 17 | </div> |
| 16 | <p><a class="explorelink" href="/explore">explore public repositories →</a></p> | |
| 17 | {{if .Accounts}}<p class="meta">have an account? log in from your terminal: <code>gitbay web login</code>{{if .Signup}} · new here? <a href="/register">create an account</a>{{end}}</p>{{end}} | |
| 18 | <div class="routes"><a class="button primary" href="/explore">Explore repositories</a>{{if .Signup}}<a class="button btn" href="/register">Create an account</a>{{end}}</div> | |
| 19 | {{if .Accounts}}<p class="meta">Have an account? {{if .EmailLogin}}<a href="/login">Sign in with an emailed link</a>, or run{{else}}Run{{end}} <code>ssh git@{{.Host}} web login</code>.</p>{{end}} | |
| 18 | 20 | </div> |
| 19 | 21 | {{end}} |
internal/web/templates/layout.html +7 −7
| @@ -71,13 +71,13 @@ | ||
| 71 | 71 | <form method="post" action="/{{.OwnerName}}/{{.Name}}/fork" class="inline"><button type="submit" class="btn">Fork</button></form>{{end}} |
| 72 | 72 | </div> |
| 73 | 73 | {{$top := topTab (str $ "Tab")}} |
| 74 | {{/* The header is the same on every tab: it sits above the tab bar, | |
| 75 | and anything shown on one tab and not another would move the | |
| 76 | navigation between clicks of that navigation. */}} | |
| 77 | {{with field $ "Desc"}}<p class="repodesc">{{.}}</p>{{end}} | |
| 78 | {{with field $ "Topics"}}<p class="topics">{{range .}}<a class="chip topic" href="/explore?q={{.}}">{{.}}</a> {{end}}</p>{{end}} | |
| 79 | {{with $.Repo.Settings.Website}}<p class="repometa"><a href="{{.}}" rel="nofollow">{{.}}</a></p>{{end}} | |
| 80 | {{range field $ "Mirrors"}}<p class="repometa">{{if eq .Direction "push"}}Mirrors to{{else}}Mirrors from{{end}} <a href="{{.URL}}" rel="nofollow">{{.Target}}</a>{{if .Error}} · <span class="bad">sync error: {{.Error}}</span>{{else if .Synced}} · synced {{.Synced}}{{end}}</p> | |
| 74 | {{/* Description, topics and metadata belong to the code tab. On task | |
| 75 | tabs only the identity row and the tab bar render, and the header | |
| 76 | is identical on every page within a tab. */}} | |
| 77 | {{if eq $top "code"}} | |
| 78 | <p class="repodesc">{{with field $ "Desc"}}{{.}}{{end}} {{with field $ "Topics"}}{{range .}}<a class="chip topic" href="/explore?q={{.}}">{{.}}</a> {{end}}{{end}}</p> | |
| 79 | {{if or $.Repo.Settings.Website (field $ "Mirrors")}}<p class="repometa">{{with $.Repo.Settings.Website}}<a href="{{.}}" rel="nofollow">{{.}}</a>{{end}}{{range field $ "Mirrors"}} · {{if eq .Direction "push"}}mirrors to{{else}}mirrors from{{end}} <a href="{{.URL}}" rel="nofollow">{{.Target}}</a>{{if .Error}}, <span class="bad">sync error: {{.Error}}</span>{{else if .Synced}}, synced {{.Synced}}{{end}}{{end}}</p>{{end}} | |
| 80 | {{if $.Viewer}}<p class="toggles">Pinned shows in your rail. Watching sends every issue, request and build to your inbox. Bookmarked lists it under Bookmarks.</p>{{end}} | |
| 81 | 81 | {{end}} |
| 82 | 82 | <nav class="tabs" aria-label="Repository"> |
| 83 | 83 | <a {{if eq $top "code"}}aria-current="page" {{end}}href="/{{.OwnerName}}/{{.Name}}">Code</a> |
internal/web/templates/login.html +2 −2
| @@ -10,8 +10,8 @@ expires in fifteen minutes.</p> | ||
| 10 | 10 | {{else}} |
| 11 | 11 | {{if .EmailLogin}} |
| 12 | 12 | <form method="post" action="/login"> |
| 13 | <label for="identifier">Username or email address</label> | |
| 14 | <input type="text" id="identifier" name="identifier" autocomplete="username" required> | |
| 13 | <div class="field"><label for="identifier">Username or email address</label> | |
| 14 | <input type="text" id="identifier" name="identifier" autocomplete="username" required></div> | |
| 15 | 15 | <button type="submit">Email me a link</button> |
| 16 | 16 | </form> |
| 17 | 17 | <p>Or, from a machine with your registered key:</p> |
internal/web/templates/register.html +9 −10
| @@ -1,19 +1,18 @@ | ||
| 1 | {{define "width"}}bounded{{end}} | |
| 2 | 1 | {{define "title"}}register · {{.Site}}{{end}} |
| 2 | {{define "width"}}bounded{{end}} | |
| 3 | 3 | {{define "content"}} |
| 4 | <div class="landing"> | |
| 5 | 4 | <h1>Create an account</h1> |
| 6 | {{if eq .Mode "invite"}}<p class="lede">This instance is invite-only: you need an invite code from an admin.</p> | |
| 7 | {{else}}<p class="lede">Open registration — your account activates once you verify your email.</p>{{end}} | |
| 5 | {{if eq .Mode "invite"}}<p class="lede">This instance is invite-only. You need an invite code from an admin.</p> | |
| 6 | {{else}}<p class="lede">Open registration. Your account activates once you verify your email.</p>{{end}} | |
| 8 | 7 | {{if .Error}}<p class="error" role="alert">{{.Error}}</p>{{end}} |
| 9 | 8 | <form method="post" action="/register" class="signupform"> |
| 10 | <p><label>Username<br><input type="text" name="username" value="{{.Username}}" required autofocus></label></p> | |
| 11 | {{if eq .Mode "invite"}}<p><label>invite code<br><input type="text" name="invite" required></label></p> | |
| 12 | {{else}}<p><label>Email<br><input type="text" name="email" required></label></p>{{end}} | |
| 13 | <p><label>SSH public key — your key is your identity; paste e.g. <code>~/.ssh/id_ed25519.pub</code><br> | |
| 14 | <textarea name="key" aria-label="SSH public key" rows="3" required placeholder="ssh-ed25519 AAAA... you@host"></textarea></label></p> | |
| 9 | <div class="field"><label for="username">Username</label><input type="text" id="username" name="username" value="{{.Username}}" required autofocus></div> | |
| 10 | {{if eq .Mode "invite"}}<div class="field"><label for="invite">Invite code</label><input type="text" id="invite" name="invite" required></div> | |
| 11 | {{else}}<div class="field"><label for="email">Email</label><input type="text" id="email" name="email" required></div>{{end}} | |
| 12 | <div class="field"><label for="key">SSH public key</label> | |
| 13 | <p class="hint">Paste the contents of your public key file, usually <code>~/.ssh/id_ed25519.pub</code>. It starts with <code>ssh-ed25519</code> or <code>ssh-rsa</code>. No key yet? <a href="/krz/gitbay/wiki/SSH-keys">Make one</a>.</p> | |
| 14 | <textarea id="key" name="key" rows="3" required placeholder="ssh-ed25519 AAAA... you@host"></textarea></div> | |
| 15 | 15 | <p><button type="submit">Create account</button></p> |
| 16 | 16 | </form> |
| 17 | 17 | <p class="meta">Prefer the terminal? <code>ssh git@{{.Host}} register --username you {{if eq .Mode "invite"}}--invite <code>{{else}}--email you@example.org{{end}}</code></p> |
| 18 | </div> | |
| 19 | 18 | {{end}} |