Commit ca02f30bc0
Verified · cmc ci/build: success ci/sonar: success ci/test: success ci/vuln: success
Layout: unified · split
.gitbay/wiki/Parity.org +4 −3
| @@ -96,14 +96,15 @@ reviews, since an approval was of the diff against the old branch. | ||
| 96 | 96 | | labels, assignees | yes | yes | yes | |
| 97 | 97 | | milestone | yes | yes | yes | |
| 98 | 98 | | milestone list | yes | yes | yes | |
| 99 | | labels: list, colour | yes | no | no | | |
| 99 | | labels: list, colour | yes | yes | no | | |
| 100 | 100 | | choose body markup | yes | no | no | |
| 101 | 101 | |
| 102 | 102 | Labels are created on the fly by =issue label --add= and managed by |
| 103 | 103 | =label list=, =label set <label> --color rrggbb= and =label remove=, |
| 104 | 104 | which takes the label off every issue. The web paints the stored colour |
| 105 | on every chip and derives one from the name when none is set; it has no | |
| 106 | form for setting one yet. | |
| 105 | on every chip and derives one from the name when none is set. The set | |
| 106 | itself is at =/<owner>/<repo>/labels=, linked from the issue list: | |
| 107 | create, recolour and remove, dispatching the same commands. | |
| 107 | 108 | |
| 108 | 109 | Issue, MR and release bodies, and their comments, carry the markup they |
| 109 | 110 | were written in — =--format md|org= on create, comment and edit, stored |
e2e/isolation_test.go +1
| @@ -67,6 +67,7 @@ func TestPrivateRepoIsInvisible(t *testing.T) { | ||
| 67 | 67 | "/owner/secret/tree/main/", "/owner/secret/blob/main/notes.txt", |
| 68 | 68 | "/owner/secret/raw/main/notes.txt", "/owner/secret/log", |
| 69 | 69 | "/owner/secret/releases", "/owner/secret/builds", "/owner/secret/wiki", |
| 70 | "/owner/secret/labels", | |
| 70 | 71 | "/owner/secret/compare/main...feat", "/owner/secret/milestones", |
| 71 | 72 | "/owner/secret/archive/main.tar.gz", "/owner/secret/badge/build.svg", |
| 72 | 73 | "/owner/secret/search?q=" + fileWord, |
e2e/labelweb_test.go added +76
| @@ -0,0 +1,76 @@ | ||
| 1 | package e2e | |
| 2 | ||
| 3 | import ( | |
| 4 | "net/url" | |
| 5 | "strings" | |
| 6 | "testing" | |
| 7 | ) | |
| 8 | ||
| 9 | // The label set itself is managed from the browser: create, recolour and | |
| 10 | // remove, each through the label command the CLI runs (#163). | |
| 11 | func TestLabelsWeb(t *testing.T) { | |
| 12 | inst := startInstanceWith(t, "[web]\nmode = \"accounts\"\n") | |
| 13 | aliceKey := inst.newKey(t, "alice") | |
| 14 | bobKey := inst.newKey(t, "bob") | |
| 15 | inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub") | |
| 16 | inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub") | |
| 17 | if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/app"); code != 0 { | |
| 18 | t.Fatalf("repo create: %s", errOut) | |
| 19 | } | |
| 20 | inst.ssh(t, aliceKey, "", "issue", "create", "alice/app", "--title", "one") | |
| 21 | inst.ssh(t, aliceKey, "", "issue", "label", "alice/app", "1", "--add", "bug") | |
| 22 | ||
| 23 | alice := inst.login(t, aliceKey) | |
| 24 | base := inst.base() + "/alice/app" | |
| 25 | ||
| 26 | // The existing label is listed with its use count. | |
| 27 | status, page := browserGet(t, alice, base+"/labels") | |
| 28 | if status != 200 || !strings.Contains(page, "bug") { | |
| 29 | t.Fatalf("labels page: %d\n%s", status, page) | |
| 30 | } | |
| 31 | ||
| 32 | // Creating one with a colour, and recolouring the existing one. | |
| 33 | if status, _ := browserPost(t, alice, base+"/labels", url.Values{ | |
| 34 | "name": {"docs"}, "color": {"1f6feb"}}); status != 200 { | |
| 35 | t.Fatal("label create failed") | |
| 36 | } | |
| 37 | if status, _ := browserPost(t, alice, base+"/labels", url.Values{ | |
| 38 | "name": {"bug"}, "color": {"cf222e"}}); status != 200 { | |
| 39 | t.Fatal("label recolour failed") | |
| 40 | } | |
| 41 | out, _, _ := inst.ssh(t, aliceKey, "", "label", "list", "alice/app", "--json") | |
| 42 | if !strings.Contains(out, `{"name":"bug","color":"#cf222e","issues":1}`) || | |
| 43 | !strings.Contains(out, `{"name":"docs","color":"#1f6feb","issues":0}`) { | |
| 44 | t.Fatalf("labels not as posted:\n%s", out) | |
| 45 | } | |
| 46 | ||
| 47 | // A bad colour comes back as an error on the page, not a silent no-op. | |
| 48 | _, body := browserPost(t, alice, base+"/labels", url.Values{ | |
| 49 | "name": {"docs"}, "color": {"red"}}) | |
| 50 | if !strings.Contains(body, `class="error"`) { | |
| 51 | t.Errorf("bad colour accepted:\n%s", body) | |
| 52 | } | |
| 53 | ||
| 54 | // Removing takes the label off the issue too. | |
| 55 | if status, _ := browserPost(t, alice, base+"/labels", url.Values{ | |
| 56 | "action": {"remove"}, "name": {"bug"}}); status != 200 { | |
| 57 | t.Fatal("label remove failed") | |
| 58 | } | |
| 59 | if out, _, _ := inst.ssh(t, aliceKey, "", "issue", "show", "alice/app", "1", "--json"); strings.Contains(out, `"bug"`) { | |
| 60 | t.Fatalf("issue still carries the removed label:\n%s", out) | |
| 61 | } | |
| 62 | ||
| 63 | // A reader sees the set and none of the controls. | |
| 64 | bob := inst.login(t, bobKey) | |
| 65 | _, p := browserGet(t, bob, base+"/labels") | |
| 66 | if !strings.Contains(p, "docs") { | |
| 67 | t.Fatalf("reader cannot see the labels:\n%s", p) | |
| 68 | } | |
| 69 | if strings.Contains(p, "New label") || strings.Contains(p, "Remove") { | |
| 70 | t.Fatal("reader sees a label control") | |
| 71 | } | |
| 72 | browserPost(t, bob, base+"/labels", url.Values{"name": {"sneak"}}) | |
| 73 | if out, _, _ := inst.ssh(t, aliceKey, "", "label", "list", "alice/app", "--json"); strings.Contains(out, "sneak") { | |
| 74 | t.Fatal("reader created a label") | |
| 75 | } | |
| 76 | } | |
internal/httpd/labels.go added +50
| @@ -0,0 +1,50 @@ | ||
| 1 | package httpd | |
| 2 | ||
| 3 | import ( | |
| 4 | "html/template" | |
| 5 | "net/http" | |
| 6 | "strings" | |
| 7 | ||
| 8 | "gitbay.org/gitbay/internal/store" | |
| 9 | ) | |
| 10 | ||
| 11 | // labels lists a repository's label set with its colour and how many | |
| 12 | // issues carry it. Applying a label is on the issue page; this is where | |
| 13 | // the set itself is kept (#163). | |
| 14 | func (s *Server) labels(w http.ResponseWriter, r *http.Request) { | |
| 15 | p, ok := s.repoFor(w, r, "") | |
| 16 | if !ok { | |
| 17 | return | |
| 18 | } | |
| 19 | p.Tab = "issues" | |
| 20 | labels, err := s.st.ListLabels(p.Repo.ID) | |
| 21 | if err != nil { | |
| 22 | http.Error(w, "internal error", http.StatusInternalServerError) | |
| 23 | return | |
| 24 | } | |
| 25 | s.render(w, "labels.html", struct { | |
| 26 | repoPage | |
| 27 | Labels []store.Label | |
| 28 | LabelColors map[string]template.CSS | |
| 29 | CanWrite bool | |
| 30 | Notice string | |
| 31 | }{p, labels, s.labelColors(p.Repo.ID), s.canWriteRepo(r, p.Repo), s.takeFlash(w, r)}) | |
| 32 | } | |
| 33 | ||
| 34 | // labelSubmit creates a label, sets its colour, or removes it, through | |
| 35 | // the label commands the CLI runs. | |
| 36 | func (s *Server) labelSubmit(w http.ResponseWriter, r *http.Request, u store.User) { | |
| 37 | repo := r.PathValue("owner") + "/" + r.PathValue("repo") | |
| 38 | name := strings.TrimSpace(r.FormValue("name")) | |
| 39 | back := func(w http.ResponseWriter, r *http.Request, msg string) { s.backTo(w, r, "labels", msg) } | |
| 40 | if name == "" { | |
| 41 | s.backTo(w, r, "labels", "name the label") | |
| 42 | return | |
| 43 | } | |
| 44 | argv := []string{"label", "set", repo, name, "--color", strings.TrimSpace(r.FormValue("color"))} | |
| 45 | if r.FormValue("action") == "remove" { | |
| 46 | argv = []string{"label", "remove", repo, name} | |
| 47 | } | |
| 48 | _, msg, code := s.runControlCode(u, argv) | |
| 49 | s.done(w, r, code, msg, back) | |
| 50 | } | |
internal/httpd/routes.go +3
| @@ -62,6 +62,7 @@ func (s *Server) Routes() []Route { | ||
| 62 | 62 | Route{Method: "GET", Pattern: "/{owner}/{repo}/blame/{ref}/{path...}", Handler: s.blame}, |
| 63 | 63 | Route{Method: "GET", Pattern: "/{owner}/{repo}/search", Handler: s.search}, |
| 64 | 64 | Route{Method: "GET", Pattern: "/{owner}/{repo}/milestones", Handler: s.milestones}, |
| 65 | Route{Method: "GET", Pattern: "/{owner}/{repo}/labels", Handler: s.labels}, | |
| 65 | 66 | Route{Method: "GET", Pattern: "/{owner}/{repo}/wiki", Handler: s.wiki}, |
| 66 | 67 | Route{Method: "GET", Pattern: "/{owner}/{repo}/wiki/_raw/{path...}", Handler: s.wikiRaw}, |
| 67 | 68 | Route{Method: "GET", Pattern: "/{owner}/{repo}/wiki/{page}", Handler: s.wiki}, |
| @@ -149,6 +150,8 @@ func (s *Server) Routes() []Route { | ||
| 149 | 150 | Handler: s.checkOrigin(s.requireUser(s.issueMilestoneSubmit))}, |
| 150 | 151 | Route{Method: "POST", Pattern: "/{owner}/{repo}/releases", Mutating: true, |
| 151 | 152 | Handler: s.checkOrigin(s.requireUser(s.releaseSubmit))}, |
| 153 | Route{Method: "POST", Pattern: "/{owner}/{repo}/labels", Mutating: true, | |
| 154 | Handler: s.checkOrigin(s.requireUser(s.labelSubmit))}, | |
| 152 | 155 | Route{Method: "POST", Pattern: "/{owner}/{repo}/builds", Mutating: true, |
| 153 | 156 | Handler: s.checkOrigin(s.requireUser(s.buildTriggerSubmit))}, |
| 154 | 157 | Route{Method: "POST", Pattern: "/{owner}/{repo}/builds/{n}/cancel", Mutating: true, |
internal/web/templates/issues.html +1 −1
| @@ -13,7 +13,7 @@ | ||
| 13 | 13 | <input type="hidden" name="state" value="{{.State}}"> |
| 14 | 14 | </form> |
| 15 | 15 | <span class="spacer"></span> |
| 16 | <p class="meta"><a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/milestones">milestones</a>{{if .Viewer}} · <a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/issues/new">new issue</a>{{end}}</p> | |
| 16 | <p class="meta"><a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/milestones">milestones</a> · <a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/labels">labels</a>{{if .Viewer}} · <a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/issues/new">new issue</a>{{end}}</p> | |
| 17 | 17 | </div> |
| 18 | 18 | <ul class="issuelist"> |
| 19 | 19 | {{range .Issues}}<li> |
internal/web/templates/labels.html added +36
| @@ -0,0 +1,36 @@ | ||
| 1 | {{define "title"}}labels · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}} | |
| 2 | {{define "content"}} | |
| 3 | <h1>Labels</h1> | |
| 4 | {{if .Notice}}<p class="error" role="alert">{{.Notice}}</p>{{end}} | |
| 5 | {{if .Labels}}<div class="tablewrap"><table class="keys"> | |
| 6 | <tr class="cols"><th scope="col">label</th><th scope="col">colour</th><th scope="col">issues</th><th scope="col"></th></tr> | |
| 7 | {{range .Labels}}<tr> | |
| 8 | <td><a class="chip label" style="{{index $.LabelColors .Name}}" href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/issues?label={{.Name}}">{{.Name}}</a></td> | |
| 9 | <td>{{if $.CanWrite}}<form method="post" action="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/labels" class="inline"> | |
| 10 | <input type="hidden" name="name" value="{{.Name}}"> | |
| 11 | <input type="text" name="color" value="{{.Color}}" aria-label="Colour for {{.Name}}" placeholder="rrggbb" size="8"> | |
| 12 | <button type="submit" class="btn">Save</button> | |
| 13 | </form>{{else}}<span class="mono">{{if .Color}}{{.Color}}{{else}}—{{end}}</span>{{end}}</td> | |
| 14 | <td>{{.Issues}}</td> | |
| 15 | <td class="act">{{if $.CanWrite}}<form method="post" action="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/labels" class="inline"> | |
| 16 | <input type="hidden" name="action" value="remove"> | |
| 17 | <input type="hidden" name="name" value="{{.Name}}"> | |
| 18 | <button type="submit" class="linklike">Remove</button> | |
| 19 | </form>{{end}}</td> | |
| 20 | </tr> | |
| 21 | {{end}}</table></div> | |
| 22 | {{else}}<p class="none">No labels yet.</p>{{end}} | |
| 23 | {{if .CanWrite}} | |
| 24 | <details class="editbox"> | |
| 25 | <summary>New label</summary> | |
| 26 | <form method="post" action="/{{.Repo.OwnerName}}/{{.Repo.Name}}/labels" class="setform stack"> | |
| 27 | <label for="labelname">Name</label> | |
| 28 | <input type="text" id="labelname" name="name" maxlength="50" required> | |
| 29 | <label for="labelcolor">Colour</label> | |
| 30 | <input type="text" id="labelcolor" name="color" placeholder="rrggbb, or blank for one picked from the name"> | |
| 31 | <button type="submit">Create label</button> | |
| 32 | </form> | |
| 33 | </details> | |
| 34 | <p class="meta">Removing a label takes it off every issue that carries it.</p> | |
| 35 | {{end}} | |
| 36 | {{end}} | |