Commit e5dee0c2a4
Verified · cmc ci/build: success ci/test: success
.gitbay/wiki/Users.org +7 −8
| @@ -372,11 +372,10 @@ gitbay milestone close v1.0 / reopen v1.0 | ||
| 372 | 372 | An org holds labels and milestones every repository under it sees |
| 373 | 373 | beside its own. =issue label --add=, =mr label --add=, =issue |
| 374 | 374 | milestone= and =mr milestone= resolve the org's row first; a repository |
| 375 | cannot create a | |
| 376 | label or milestone with a name its org holds. Creating an org label or | |
| 377 | milestone whose name repositories under the org already use folds them | |
| 378 | in: their issues and merge requests move to the org's row. Org admins | |
| 379 | manage them; counts span the repositories you can read. | |
| 375 | cannot create a label or milestone with a name its org holds. Creating | |
| 376 | an org label or milestone whose name repositories under the org already | |
| 377 | use folds them in: their issues and merge requests move to the org's | |
| 378 | row. Org admins manage them; counts span the repositories you can read. | |
| 380 | 379 | |
| 381 | 380 | #+begin_src sh |
| 382 | 381 | gitbay org label set acme bug --color cf222e |
| @@ -395,9 +394,9 @@ form prefills its textarea, and =gitbay issue templates= lists them. | ||
| 395 | 394 | |
| 396 | 395 | Lists narrow the same way on every surface: =issue list --label bug |
| 397 | 396 | --assignee bob --author alice --milestone v1= (or =--milestone none=), |
| 398 | =mr list --label bug --author bob --milestone v1=; the web's issue and merge request | |
| 399 | lists take the same names as query parameters, and each active filter | |
| 400 | shows with a link that drops it. | |
| 397 | =mr list --label bug --author bob --milestone v1=; the web's issue and | |
| 398 | merge request lists take the same names as query parameters, and each | |
| 399 | active filter shows with a link that drops it. | |
| 401 | 400 | |
| 402 | 401 | Labels take a colour: =gitbay label set bug --color cf222e=; =label |
| 403 | 402 | list= shows each with its colour and how many issues and merge requests |
e2e/label_test.go +3 −2
| @@ -23,7 +23,7 @@ func TestLabelColors(t *testing.T) { | ||
| 23 | 23 | if _, _, code := inst.ssh(t, aliceKey, "", "issue", "label", "alice/app", "1", "--add", "bug"); code != 0 { |
| 24 | 24 | t.Fatal("issue label failed") |
| 25 | 25 | } |
| 26 | if out, _, _ := inst.ssh(t, aliceKey, "", "label", "list", "alice/app"); !strings.Contains(out, "bug\t\t1") { | |
| 26 | if out, _, _ := inst.ssh(t, aliceKey, "", "label", "list", "alice/app"); !strings.Contains(out, "bug\t\t1\t0") { | |
| 27 | 27 | t.Fatalf("list after issue label:\n%s", out) |
| 28 | 28 | } |
| 29 | 29 | if _, _, code := inst.ssh(t, bobKey, "", "label", "set", "alice/app", "bug", "--color", "cf222e"); code != 4 { |
| @@ -39,7 +39,8 @@ func TestLabelColors(t *testing.T) { | ||
| 39 | 39 | t.Fatal("create without colour failed") |
| 40 | 40 | } |
| 41 | 41 | out, _, _ := inst.ssh(t, aliceKey, "", "label", "list", "alice/app", "--json") |
| 42 | if !strings.Contains(out, `{"name":"bug","color":"#cf222e","issues":1}`) || !strings.Contains(out, `{"name":"docs","issues":0}`) { | |
| 42 | if !strings.Contains(out, `{"name":"bug","color":"#cf222e","issues":1,"mrs":0}`) || | |
| 43 | !strings.Contains(out, `{"name":"docs","issues":0,"mrs":0}`) { | |
| 43 | 44 | t.Fatalf("list json:\n%s", out) |
| 44 | 45 | } |
| 45 | 46 | // The web paints the chip with the stored colour. |
internal/control/label.go +3 −3
| @@ -14,13 +14,13 @@ import ( | ||
| 14 | 14 | |
| 15 | 15 | func init() { |
| 16 | 16 | register(Command{Path: []string{"label", "list"}, |
| 17 | Summary: "list a repository's issue labels with colour and use", | |
| 17 | Summary: "list a repository's labels with colour and use", | |
| 18 | 18 | Usage: "label list <owner/name>", ReadOnly: true, Run: runLabelList}) |
| 19 | 19 | register(Command{Path: []string{"label", "set"}, |
| 20 | 20 | Summary: "create a label or set its colour", |
| 21 | 21 | Usage: "label set <owner/name> <label> [--color rrggbb|'']", Run: runLabelSet}) |
| 22 | 22 | register(Command{Path: []string{"label", "remove"}, |
| 23 | Summary: "remove a label from the repository and from every issue", | |
| 23 | Summary: "remove a label from the repository and from every issue and merge request", | |
| 24 | 24 | Usage: "label remove <owner/name> <label>", Run: runLabelRemove}) |
| 25 | 25 | } |
| 26 | 26 | |
| @@ -47,7 +47,7 @@ func runLabelList(c *Ctx, args []string) int { | ||
| 47 | 47 | } |
| 48 | 48 | return c.emit(labels, func(w io.Writer) { |
| 49 | 49 | for _, l := range labels { |
| 50 | fmt.Fprintf(w, "%s\t%s\t%d%s\n", l.Name, l.Color, l.Issues, map[bool]string{true: "\torg"}[l.Org]) | |
| 50 | fmt.Fprintf(w, "%s\t%s\t%d\t%d%s\n", l.Name, l.Color, l.Issues, l.MRs, map[bool]string{true: "\torg"}[l.Org]) | |
| 51 | 51 | } |
| 52 | 52 | }) |
| 53 | 53 | } |
internal/control/mr.go +41 −39
| @@ -881,45 +881,6 @@ func runMRReview(c *Ctx, args []string) int { | ||
| 881 | 881 | // runMRReviewRequest is issue assign's counterpart for merge requests: it |
| 882 | 882 | // pushes a merge request into a specific person's review queue and inbox |
| 883 | 883 | // directly, rather than waiting for them to be otherwise involved (#145). |
| 884 | func runMRLabel(c *Ctx, args []string) int { | |
| 885 | rest, adds, removes, err := addRemoveFlags(args) | |
| 886 | if err != nil { | |
| 887 | return c.failInput(err) | |
| 888 | } | |
| 889 | if len(adds)+len(removes) == 0 { | |
| 890 | return c.usage() | |
| 891 | } | |
| 892 | repo, mr, code := mrRef(c, rest, policy.CanWrite) | |
| 893 | if code >= 0 { | |
| 894 | return code | |
| 895 | } | |
| 896 | if code := refuseArchived(c, repo); code >= 0 { | |
| 897 | return code | |
| 898 | } | |
| 899 | for _, l := range adds { | |
| 900 | if err := c.Store.SetMRLabel(repo, mr.ID, l, true); err != nil { | |
| 901 | return c.fail(protocol.ExitFailure, "%v", err) | |
| 902 | } | |
| 903 | } | |
| 904 | for _, l := range removes { | |
| 905 | if err := c.Store.SetMRLabel(repo, mr.ID, l, false); err != nil { | |
| 906 | if errors.Is(err, store.ErrNotFound) { | |
| 907 | return c.fail(protocol.ExitNotFound, "%v", err) | |
| 908 | } | |
| 909 | return c.fail(protocol.ExitFailure, "%v", err) | |
| 910 | } | |
| 911 | } | |
| 912 | updated, err := c.Store.MRByNumber(repo.ID, mr.Number) | |
| 913 | if err != nil { | |
| 914 | return c.fail(protocol.ExitFailure, "%v", err) | |
| 915 | } | |
| 916 | c.Store.RecordEvent(repo.ID, c.User.ID, "mr.labeled", | |
| 917 | fmt.Sprintf(`{"number":%d,"labels":%s}`, mr.Number, jsonStrings(updated.Labels))) | |
| 918 | return c.emit(map[string]any{"number": mr.Number, "labels": updated.Labels}, func(w io.Writer) { | |
| 919 | fmt.Fprintf(w, "labels on %s!%d: %s\n", repo.Path(), mr.Number, strings.Join(updated.Labels, ", ")) | |
| 920 | }) | |
| 921 | } | |
| 922 | ||
| 923 | 884 | func runMRReviewRequest(c *Ctx, args []string) int { |
| 924 | 885 | rest, adds, removes, err := addRemoveFlags(args) |
| 925 | 886 | if err != nil { |
| @@ -1004,6 +965,47 @@ func runMRReviewRequest(c *Ctx, args []string) int { | ||
| 1004 | 965 | }) |
| 1005 | 966 | } |
| 1006 | 967 | |
| 968 | // runMRLabel is issue label's counterpart for merge requests: the label | |
| 969 | // set is the repository's (or its org's), shared with the issues (#231). | |
| 970 | func runMRLabel(c *Ctx, args []string) int { | |
| 971 | rest, adds, removes, err := addRemoveFlags(args) | |
| 972 | if err != nil { | |
| 973 | return c.failInput(err) | |
| 974 | } | |
| 975 | if len(adds)+len(removes) == 0 { | |
| 976 | return c.usage() | |
| 977 | } | |
| 978 | repo, mr, code := mrRef(c, rest, policy.CanWrite) | |
| 979 | if code >= 0 { | |
| 980 | return code | |
| 981 | } | |
| 982 | if code := refuseArchived(c, repo); code >= 0 { | |
| 983 | return code | |
| 984 | } | |
| 985 | for _, l := range adds { | |
| 986 | if err := c.Store.SetMRLabel(repo, mr.ID, l, true); err != nil { | |
| 987 | return c.fail(protocol.ExitFailure, "%v", err) | |
| 988 | } | |
| 989 | } | |
| 990 | for _, l := range removes { | |
| 991 | if err := c.Store.SetMRLabel(repo, mr.ID, l, false); err != nil { | |
| 992 | if errors.Is(err, store.ErrNotFound) { | |
| 993 | return c.fail(protocol.ExitNotFound, "%v", err) | |
| 994 | } | |
| 995 | return c.fail(protocol.ExitFailure, "%v", err) | |
| 996 | } | |
| 997 | } | |
| 998 | updated, err := c.Store.MRByNumber(repo.ID, mr.Number) | |
| 999 | if err != nil { | |
| 1000 | return c.fail(protocol.ExitFailure, "%v", err) | |
| 1001 | } | |
| 1002 | c.Store.RecordEvent(repo.ID, c.User.ID, "mr.labeled", | |
| 1003 | fmt.Sprintf(`{"number":%d,"labels":%s}`, mr.Number, jsonStrings(updated.Labels))) | |
| 1004 | return c.emit(map[string]any{"number": mr.Number, "labels": updated.Labels}, func(w io.Writer) { | |
| 1005 | fmt.Fprintf(w, "labels on %s!%d: %s\n", repo.Path(), mr.Number, strings.Join(updated.Labels, ", ")) | |
| 1006 | }) | |
| 1007 | } | |
| 1008 | ||
| 1007 | 1009 | func runMRMerge(c *Ctx, args []string) int { |
| 1008 | 1010 | f, err := parseFlags(args, flagSpec{Values: []string{"--strategy"}, MaxPos: -1, Usage: "mr merge <owner/name> <n> [--strategy ff|merge|squash|rebase]"}) |
| 1009 | 1011 | if err != nil { |
internal/control/orglabel.go +1 −1
| @@ -129,7 +129,7 @@ func runOrgLabelList(c *Ctx, args []string) int { | ||
| 129 | 129 | } |
| 130 | 130 | return c.emit(labels, func(w io.Writer) { |
| 131 | 131 | for _, l := range labels { |
| 132 | fmt.Fprintf(w, "%s\t%s\t%d\n", l.Name, l.Color, l.Issues) | |
| 132 | fmt.Fprintf(w, "%s\t%s\t%d\t%d\n", l.Name, l.Color, l.Issues, l.MRs) | |
| 133 | 133 | } |
| 134 | 134 | }) |
| 135 | 135 | } |
internal/store/labels_test.go +10
| @@ -260,6 +260,16 @@ func TestSetOrgLabelFoldsMRLabels(t *testing.T) { | ||
| 260 | 260 | if len(rows) != 1 || rows[0].MRs != 2 { |
| 261 | 261 | t.Fatalf("org label rows = %+v", rows) |
| 262 | 262 | } |
| 263 | // Deleting the label takes it off every merge request: mr_labels | |
| 264 | // cascades from labels, nothing unlinks them by hand. | |
| 265 | if err := f.s.DeleteOrgLabel(f.org, "bug"); err != nil { | |
| 266 | t.Fatal(err) | |
| 267 | } | |
| 268 | for _, repo := range []Repo{f.core, f.site} { | |
| 269 | if got, err := f.s.ListMRLabels(repo); err != nil || len(got) != 0 { | |
| 270 | t.Fatalf("%s MR labels after delete = %v, %v", repo.Name, got, err) | |
| 271 | } | |
| 272 | } | |
| 263 | 273 | } |
| 264 | 274 | |
| 265 | 275 | func TestRepoLabelRefusedWhenOrgHoldsName(t *testing.T) { |
internal/store/migrations/0056_mr_labels.up.sql +5
| @@ -1,5 +1,10 @@ | ||
| 1 | 1 | -- Labels on merge requests, carried the same way issues carry them |
| 2 | 2 | -- (#231). The label rows themselves are shared: repo or org scoped. |
| 3 | -- | |
| 4 | -- labels now has two child tables, issue_labels and mr_labels: a future | |
| 5 | -- rebuild of labels must carry both. With foreign keys on, rebuilding a | |
| 6 | -- parent table drops its children's rows, which is what the first-line | |
| 7 | -- "-- foreign_keys: off" directive in 0052 is for. | |
| 3 | 8 | CREATE TABLE mr_labels ( |
| 4 | 9 | mr_id INTEGER NOT NULL REFERENCES merge_requests(id) ON DELETE CASCADE, |
| 5 | 10 | label_id INTEGER NOT NULL REFERENCES labels(id) ON DELETE CASCADE, |
internal/web/templates/orglabels.html +2 −1
| @@ -3,11 +3,12 @@ | ||
| 3 | 3 | <h1><a href="/{{.Org}}">{{.Org}}</a> labels</h1> |
| 4 | 4 | <p class="meta">Every repository under {{.Org}} sees these beside its own. Managed with <code>gitbay org label set {{.Org}} <label></code>; counts span the repositories you can read.</p> |
| 5 | 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></tr> | |
| 6 | <tr class="cols"><th scope="col">label</th><th scope="col">colour</th><th scope="col">issues</th><th scope="col">merge requests</th></tr> | |
| 7 | 7 | {{range .Labels}}<tr> |
| 8 | 8 | <td><span class="chip label" style="{{index $.LabelColors .Name}}">{{.Name}}</span></td> |
| 9 | 9 | <td><span class="mono">{{if .Color}}{{.Color}}{{else}}—{{end}}</span></td> |
| 10 | 10 | <td>{{.Issues}}</td> |
| 11 | <td>{{.MRs}}</td> | |
| 11 | 12 | </tr> |
| 12 | 13 | {{end}}</table></div> |
| 13 | 14 | {{else}}<p class="none">No org labels yet.</p>{{end}} |