Commit 784b5dfad3
Verified · cmc
internal/control/ghimport.go +1 −1
| @@ -256,7 +256,7 @@ func runImportIssues(c *Ctx, args []string) int { | ||
| 256 | 256 | return c.fail(protocol.ExitFailure, "%v", err) |
| 257 | 257 | } |
| 258 | 258 | for _, l := range it.Labels { |
| 259 | c.Store.SetIssueLabel(repo.ID, iss.ID, l.Name, true) | |
| 259 | c.Store.SetIssueLabel(repo, iss.ID, l.Name, true) | |
| 260 | 260 | } |
| 261 | 261 | if it.State != "open" { |
| 262 | 262 | c.Store.SetIssueState(iss.ID, "closed") |
internal/control/issue.go +2 −2
| @@ -388,12 +388,12 @@ func runIssueLabel(c *Ctx, args []string) int { | ||
| 388 | 388 | return code |
| 389 | 389 | } |
| 390 | 390 | for _, l := range adds { |
| 391 | if err := c.Store.SetIssueLabel(repo.ID, issue.ID, l, true); err != nil { | |
| 391 | if err := c.Store.SetIssueLabel(repo, issue.ID, l, true); err != nil { | |
| 392 | 392 | return c.fail(protocol.ExitFailure, "%v", err) |
| 393 | 393 | } |
| 394 | 394 | } |
| 395 | 395 | for _, l := range removes { |
| 396 | if err := c.Store.SetIssueLabel(repo.ID, issue.ID, l, false); err != nil { | |
| 396 | if err := c.Store.SetIssueLabel(repo, issue.ID, l, false); err != nil { | |
| 397 | 397 | if errors.Is(err, store.ErrNotFound) { |
| 398 | 398 | return c.fail(protocol.ExitNotFound, "%v", err) |
| 399 | 399 | } |
internal/control/label.go +16 −10
| @@ -37,13 +37,17 @@ func runLabelList(c *Ctx, args []string) int { | ||
| 37 | 37 | if code >= 0 { |
| 38 | 38 | return code |
| 39 | 39 | } |
| 40 | labels, err := c.Store.ListLabels(repo.ID) | |
| 40 | readable, err := ReadableScope(c.Store, c.User, repo) | |
| 41 | if err != nil { | |
| 42 | return c.fail(protocol.ExitFailure, "%v", err) | |
| 43 | } | |
| 44 | labels, err := c.Store.ListLabels(repo, readable) | |
| 41 | 45 | if err != nil { |
| 42 | 46 | return c.fail(protocol.ExitFailure, "%v", err) |
| 43 | 47 | } |
| 44 | 48 | return c.emit(labels, func(w io.Writer) { |
| 45 | 49 | for _, l := range labels { |
| 46 | fmt.Fprintf(w, "%s\t%s\t%d\n", l.Name, l.Color, l.Issues) | |
| 50 | fmt.Fprintf(w, "%s\t%s\t%d%s\n", l.Name, l.Color, l.Issues, map[bool]string{true: "\torg"}[l.Org]) | |
| 47 | 51 | } |
| 48 | 52 | }) |
| 49 | 53 | } |
| @@ -78,15 +82,14 @@ func runLabelSet(c *Ctx, args []string) int { | ||
| 78 | 82 | } |
| 79 | 83 | if !colorSet { |
| 80 | 84 | // Keep the colour it has, if any; this is "make sure it exists". |
| 81 | if labels, err := c.Store.ListLabels(repo.ID); err == nil { | |
| 82 | for _, l := range labels { | |
| 83 | if l.Name == name { | |
| 84 | color = l.Color | |
| 85 | } | |
| 86 | } | |
| 85 | if l, err := c.Store.LabelByName(repo, name); err == nil && !l.Org { | |
| 86 | color = l.Color | |
| 87 | 87 | } |
| 88 | 88 | } |
| 89 | if err := c.Store.SetLabel(repo.ID, name, color); err != nil { | |
| 89 | if err := c.Store.SetLabel(repo, name, color); err != nil { | |
| 90 | if errors.Is(err, store.ErrOrgScoped) { | |
| 91 | return c.fail(protocol.ExitFailure, "%s", orgScopedMsg(repo, "label", name, "set")) | |
| 92 | } | |
| 90 | 93 | return c.fail(protocol.ExitFailure, "%v", err) |
| 91 | 94 | } |
| 92 | 95 | return c.emit(store.Label{Name: name, Color: color}, func(w io.Writer) { |
| @@ -109,7 +112,10 @@ func runLabelRemove(c *Ctx, args []string) int { | ||
| 109 | 112 | if code := refuseArchived(c, repo); code >= 0 { |
| 110 | 113 | return code |
| 111 | 114 | } |
| 112 | if err := c.Store.DeleteLabel(repo.ID, args[1]); err != nil { | |
| 115 | if err := c.Store.DeleteLabel(repo, args[1]); err != nil { | |
| 116 | if errors.Is(err, store.ErrOrgScoped) { | |
| 117 | return c.fail(protocol.ExitFailure, "%s", orgScopedMsg(repo, "label", args[1], "remove")) | |
| 118 | } | |
| 113 | 119 | if errors.Is(err, store.ErrNotFound) { |
| 114 | 120 | return c.fail(protocol.ExitNotFound, "no label %q in %s", args[1], repo.Path()) |
| 115 | 121 | } |
internal/control/migrate.go +1 −1
| @@ -247,7 +247,7 @@ func runAccountImportBundle(c *Ctx, args []string) int { | ||
| 247 | 247 | return c.fail(protocol.ExitFailure, "%v", err) |
| 248 | 248 | } |
| 249 | 249 | for _, l := range bi.Labels { |
| 250 | c.Store.SetIssueLabel(repo.ID, iss.ID, l, true) | |
| 250 | c.Store.SetIssueLabel(repo, iss.ID, l, true) | |
| 251 | 251 | } |
| 252 | 252 | if bi.State != "open" { |
| 253 | 253 | c.Store.SetIssueState(iss.ID, "closed") |
internal/control/milestone.go +21 −6
| @@ -60,7 +60,10 @@ func runMilestoneCreate(c *Ctx, args []string) int { | ||
| 60 | 60 | if code := refuseArchived(c, repo); code >= 0 { |
| 61 | 61 | return code |
| 62 | 62 | } |
| 63 | if _, err := c.Store.CreateMilestone(repo.ID, title, description, due); err != nil { | |
| 63 | if _, err := c.Store.CreateMilestone(repo, title, description, due); err != nil { | |
| 64 | if errors.Is(err, store.ErrOrgScoped) { | |
| 65 | return c.fail(protocol.ExitFailure, "%s", orgScopedMsg(repo, "milestone", title, "create")) | |
| 66 | } | |
| 64 | 67 | return c.failErr(err) |
| 65 | 68 | } |
| 66 | 69 | return c.emit(map[string]string{"milestone": title}, func(w io.Writer) { |
| @@ -84,7 +87,11 @@ func runMilestoneList(c *Ctx, args []string) int { | ||
| 84 | 87 | if code >= 0 { |
| 85 | 88 | return code |
| 86 | 89 | } |
| 87 | ms, err := c.Store.ListMilestones(repo.ID, state) | |
| 90 | readable, err := ReadableScope(c.Store, c.User, repo) | |
| 91 | if err != nil { | |
| 92 | return c.fail(protocol.ExitFailure, "%v", err) | |
| 93 | } | |
| 94 | ms, err := c.Store.ListMilestones(repo, state, readable) | |
| 88 | 95 | if err != nil { |
| 89 | 96 | return c.fail(protocol.ExitFailure, "%v", err) |
| 90 | 97 | } |
| @@ -93,12 +100,13 @@ func runMilestoneList(c *Ctx, args []string) int { | ||
| 93 | 100 | Description string `json:"description,omitempty"` |
| 94 | 101 | Due string `json:"due,omitempty"` |
| 95 | 102 | State string `json:"state"` |
| 103 | Org bool `json:"org,omitempty"` | |
| 96 | 104 | Open int `json:"open"` |
| 97 | 105 | Closed int `json:"closed"` |
| 98 | 106 | } |
| 99 | 107 | var ds []out |
| 100 | 108 | for _, m := range ms { |
| 101 | ds = append(ds, out{m.Title, m.Description, m.DueDate, m.State, m.OpenItems, m.ClosedItems}) | |
| 109 | ds = append(ds, out{m.Title, m.Description, m.DueDate, m.State, m.OrgID != 0, m.OpenItems, m.ClosedItems}) | |
| 102 | 110 | } |
| 103 | 111 | return c.emit(ds, func(w io.Writer) { |
| 104 | 112 | for _, d := range ds { |
| @@ -106,7 +114,11 @@ func runMilestoneList(c *Ctx, args []string) int { | ||
| 106 | 114 | if due == "" { |
| 107 | 115 | due = "-" |
| 108 | 116 | } |
| 109 | fmt.Fprintf(w, "%s\t%s\tdue %s\t%d open, %d closed\n", d.Title, d.State, due, d.Open, d.Closed) | |
| 117 | mark := "" | |
| 118 | if d.Org { | |
| 119 | mark = "\torg" | |
| 120 | } | |
| 121 | fmt.Fprintf(w, "%s\t%s\tdue %s\t%d open, %d closed%s\n", d.Title, d.State, due, d.Open, d.Closed, mark) | |
| 110 | 122 | } |
| 111 | 123 | }) |
| 112 | 124 | } |
| @@ -129,10 +141,13 @@ func setMilestoneState(c *Ctx, args []string, state string) int { | ||
| 129 | 141 | if code := refuseArchived(c, repo); code >= 0 { |
| 130 | 142 | return code |
| 131 | 143 | } |
| 132 | m, err := c.Store.MilestoneByTitle(repo.ID, args[1]) | |
| 144 | m, err := c.Store.MilestoneByTitle(repo, args[1]) | |
| 133 | 145 | if err != nil { |
| 134 | 146 | return milestoneErr(c, repo, args[1], err) |
| 135 | 147 | } |
| 148 | if m.OrgID != 0 { | |
| 149 | return c.fail(protocol.ExitFailure, "%s", orgScopedMsg(repo, "milestone", m.Title, verb)) | |
| 150 | } | |
| 136 | 151 | if err := c.Store.SetMilestoneState(m.ID, state); err != nil { |
| 137 | 152 | return c.fail(protocol.ExitFailure, "%v", err) |
| 138 | 153 | } |
| @@ -184,7 +199,7 @@ func runMRMilestone(c *Ctx, args []string) int { | ||
| 184 | 199 | func setItemMilestone(c *Ctx, repo store.Repo, noun string, number int64, title string, set func(int64) error) int { |
| 185 | 200 | var id int64 |
| 186 | 201 | if title != "none" { |
| 187 | m, err := c.Store.MilestoneByTitle(repo.ID, title) | |
| 202 | m, err := c.Store.MilestoneByTitle(repo, title) | |
| 188 | 203 | if err != nil { |
| 189 | 204 | return milestoneErr(c, repo, title, err) |
| 190 | 205 | } |
internal/control/orgscope_test.go added +154
| @@ -0,0 +1,154 @@ | ||
| 1 | package control | |
| 2 | ||
| 3 | import ( | |
| 4 | "bytes" | |
| 5 | "strings" | |
| 6 | "testing" | |
| 7 | ||
| 8 | "gitbay.org/gitbay/internal/config" | |
| 9 | "gitbay.org/gitbay/internal/protocol" | |
| 10 | "gitbay.org/gitbay/internal/store" | |
| 11 | ) | |
| 12 | ||
| 13 | // orgFixture: alice admins org acme with acme/core (public) and acme/priv | |
| 14 | // (private); bob is a plain member; carol is outside. alice also owns | |
| 15 | // alice/app. | |
| 16 | type orgFixture struct { | |
| 17 | st *store.Store | |
| 18 | alice, bob, carol int64 | |
| 19 | org int64 | |
| 20 | core, priv, app store.Repo | |
| 21 | } | |
| 22 | ||
| 23 | func newOrgFixture(t *testing.T) orgFixture { | |
| 24 | t.Helper() | |
| 25 | st, err := store.Open(":memory:") | |
| 26 | if err != nil { | |
| 27 | t.Fatal(err) | |
| 28 | } | |
| 29 | t.Cleanup(func() { st.Close() }) | |
| 30 | if err := st.MigrateUp(); err != nil { | |
| 31 | t.Fatal(err) | |
| 32 | } | |
| 33 | var f orgFixture | |
| 34 | f.st = st | |
| 35 | user := func(name string) int64 { | |
| 36 | id, err := st.CreateUser(name, false) | |
| 37 | if err != nil { | |
| 38 | t.Fatal(err) | |
| 39 | } | |
| 40 | return id | |
| 41 | } | |
| 42 | f.alice, f.bob, f.carol = user("alice"), user("bob"), user("carol") | |
| 43 | if f.org, err = st.CreateOrg("acme", f.alice); err != nil { | |
| 44 | t.Fatal(err) | |
| 45 | } | |
| 46 | if err := st.SetOrgMember(f.org, f.bob, "member"); err != nil { | |
| 47 | t.Fatal(err) | |
| 48 | } | |
| 49 | repo := func(kind string, owner int64, name, vis string) store.Repo { | |
| 50 | id, err := st.CreateRepo(kind, owner, name, vis) | |
| 51 | if err != nil { | |
| 52 | t.Fatal(err) | |
| 53 | } | |
| 54 | r, _ := st.RepoByID(id) | |
| 55 | return r | |
| 56 | } | |
| 57 | f.core = repo("org", f.org, "core", "public") | |
| 58 | f.priv = repo("org", f.org, "priv", "private") | |
| 59 | f.app = repo("user", f.alice, "app", "public") | |
| 60 | return f | |
| 61 | } | |
| 62 | ||
| 63 | func (f orgFixture) ctx(uid int64) (*Ctx, *bytes.Buffer) { | |
| 64 | var out bytes.Buffer | |
| 65 | name := map[int64]string{f.alice: "alice", f.bob: "bob", f.carol: "carol"}[uid] | |
| 66 | return &Ctx{ | |
| 67 | User: store.User{ID: uid, Username: name}, | |
| 68 | Scope: "full", | |
| 69 | Source: "SHA256:session", | |
| 70 | Store: f.st, | |
| 71 | Cfg: config.Config{Server: config.Server{SiteURL: "https://x.test"}}, | |
| 72 | Stdin: strings.NewReader(""), | |
| 73 | Stdout: &out, | |
| 74 | Stderr: &out, | |
| 75 | JSON: true, | |
| 76 | }, &out | |
| 77 | } | |
| 78 | ||
| 79 | func TestReadableOrgRepoIDs(t *testing.T) { | |
| 80 | f := newOrgFixture(t) | |
| 81 | ids, err := ReadableOrgRepoIDs(f.st, store.User{ID: f.bob, Username: "bob"}, f.org) | |
| 82 | if err != nil || len(ids) != 2 { | |
| 83 | t.Fatalf("member reads %v, %v; want both", ids, err) | |
| 84 | } | |
| 85 | ids, _ = ReadableOrgRepoIDs(f.st, store.User{ID: f.carol, Username: "carol"}, f.org) | |
| 86 | if len(ids) != 1 || ids[0] != f.core.ID { | |
| 87 | t.Fatalf("outsider reads %v; want core only", ids) | |
| 88 | } | |
| 89 | ids, _ = ReadableOrgRepoIDs(f.st, store.User{}, f.org) | |
| 90 | if len(ids) != 1 || ids[0] != f.core.ID { | |
| 91 | t.Fatalf("anonymous reads %v; want core only", ids) | |
| 92 | } | |
| 93 | ids, _ = ReadableScope(f.st, store.User{ID: f.alice, Username: "alice"}, f.app) | |
| 94 | if len(ids) != 1 || ids[0] != f.app.ID { | |
| 95 | t.Fatalf("user repo scope %v; want itself", ids) | |
| 96 | } | |
| 97 | } | |
| 98 | ||
| 99 | func TestRepoLabelCommandsRefuseOrgNames(t *testing.T) { | |
| 100 | f := newOrgFixture(t) | |
| 101 | if _, err := f.st.SetOrgLabel(f.org, "bug", ""); err != nil { | |
| 102 | t.Fatal(err) | |
| 103 | } | |
| 104 | c, out := f.ctx(f.alice) | |
| 105 | if code := runLabelSet(c, []string{"acme/core", "bug", "--color", "ff0000"}); code != protocol.ExitFailure || | |
| 106 | !strings.Contains(out.String(), "org label set acme bug") { | |
| 107 | t.Fatalf("label set over org name: exit %d %s", code, out.String()) | |
| 108 | } | |
| 109 | out.Reset() | |
| 110 | if code := runLabelRemove(c, []string{"acme/core", "bug"}); code != protocol.ExitFailure || | |
| 111 | !strings.Contains(out.String(), "org label remove acme bug") { | |
| 112 | t.Fatalf("label remove of org row: exit %d %s", code, out.String()) | |
| 113 | } | |
| 114 | out.Reset() | |
| 115 | // issue label --add resolves to the org row, and label list marks it. | |
| 116 | iid, _ := f.st.CreateIssue(f.core.ID, f.alice, "c1", "", "md") | |
| 117 | _ = iid | |
| 118 | if code := runIssueLabel(c, []string{"acme/core", "1", "--add", "bug"}); code != protocol.ExitOK { | |
| 119 | t.Fatalf("issue label: exit %d %s", code, out.String()) | |
| 120 | } | |
| 121 | out.Reset() | |
| 122 | if code := runLabelList(c, []string{"acme/core"}); code != protocol.ExitOK || | |
| 123 | !strings.Contains(out.String(), `"org":true`) || !strings.Contains(out.String(), `"issues":1`) { | |
| 124 | t.Fatalf("label list: exit %d %s", code, out.String()) | |
| 125 | } | |
| 126 | } | |
| 127 | ||
| 128 | func TestRepoMilestoneCommandsRefuseOrgTitles(t *testing.T) { | |
| 129 | f := newOrgFixture(t) | |
| 130 | if _, _, err := f.st.CreateOrgMilestone(f.org, "v1", "", ""); err != nil { | |
| 131 | t.Fatal(err) | |
| 132 | } | |
| 133 | c, out := f.ctx(f.alice) | |
| 134 | if code := runMilestoneCreate(c, []string{"acme/core", "v1"}); code != protocol.ExitFailure || | |
| 135 | !strings.Contains(out.String(), "org milestone create acme v1") { | |
| 136 | t.Fatalf("milestone create over org title: exit %d %s", code, out.String()) | |
| 137 | } | |
| 138 | out.Reset() | |
| 139 | if code := runMilestoneClose(c, []string{"acme/core", "v1"}); code != protocol.ExitFailure || | |
| 140 | !strings.Contains(out.String(), "org milestone close acme v1") { | |
| 141 | t.Fatalf("milestone close of org row: exit %d %s", code, out.String()) | |
| 142 | } | |
| 143 | out.Reset() | |
| 144 | // Attaching by title from a repo resolves the org milestone. | |
| 145 | f.st.CreateIssue(f.core.ID, f.alice, "c1", "", "md") | |
| 146 | if code := runIssueMilestone(c, []string{"acme/core", "1", "v1"}); code != protocol.ExitOK { | |
| 147 | t.Fatalf("issue milestone: exit %d %s", code, out.String()) | |
| 148 | } | |
| 149 | out.Reset() | |
| 150 | if code := runMilestoneList(c, []string{"acme/core"}); code != protocol.ExitOK || | |
| 151 | !strings.Contains(out.String(), `"org":true`) || !strings.Contains(out.String(), `"open":1`) { | |
| 152 | t.Fatalf("milestone list: exit %d %s", code, out.String()) | |
| 153 | } | |
| 154 | } | |
internal/control/scope.go added +48
| @@ -0,0 +1,48 @@ | ||
| 1 | package control | |
| 2 | ||
| 3 | import ( | |
| 4 | "fmt" | |
| 5 | ||
| 6 | "gitbay.org/gitbay/internal/policy" | |
| 7 | "gitbay.org/gitbay/internal/store" | |
| 8 | ) | |
| 9 | ||
| 10 | // ReadableOrgRepoIDs is the org's repositories user may read. Counts on | |
| 11 | // org labels and milestones are taken over these, so a private | |
| 12 | // repository's issues never show in a number someone outside it sees. A | |
| 13 | // zero user is anonymous. | |
| 14 | func ReadableOrgRepoIDs(st *store.Store, user store.User, orgID int64) ([]int64, error) { | |
| 15 | repos, err := st.ListReposForOwner("org", orgID) | |
| 16 | if err != nil { | |
| 17 | return nil, err | |
| 18 | } | |
| 19 | var ids []int64 | |
| 20 | for _, r := range repos { | |
| 21 | grant := "" | |
| 22 | if user.ID != 0 { | |
| 23 | if grant, err = st.AccessRole(r.ID, user.ID); err != nil { | |
| 24 | return nil, err | |
| 25 | } | |
| 26 | } | |
| 27 | if policy.CanRead(user, r, grant) { | |
| 28 | ids = append(ids, r.ID) | |
| 29 | } | |
| 30 | } | |
| 31 | return ids, nil | |
| 32 | } | |
| 33 | ||
| 34 | // ReadableScope is the set a repository's label and milestone counts | |
| 35 | // span: its org's readable repositories, or just itself when a user owns | |
| 36 | // it. The caller has already been allowed to read repo. | |
| 37 | func ReadableScope(st *store.Store, user store.User, repo store.Repo) ([]int64, error) { | |
| 38 | if repo.OwnerKind == "org" { | |
| 39 | return ReadableOrgRepoIDs(st, user, repo.OwnerID) | |
| 40 | } | |
| 41 | return []int64{repo.ID}, nil | |
| 42 | } | |
| 43 | ||
| 44 | // orgScopedMsg names the org command that manages a row a repository | |
| 45 | // command was asked to change. | |
| 46 | func orgScopedMsg(repo store.Repo, noun, name, verb string) string { | |
| 47 | return fmt.Sprintf("%s is an org %s of %s; manage it with org %s %s %s %s", name, noun, repo.OwnerName, noun, verb, repo.OwnerName, name) | |
| 48 | } | |
internal/httpd/labels.go +8 −2
| @@ -5,6 +5,7 @@ import ( | ||
| 5 | 5 | "net/http" |
| 6 | 6 | "strings" |
| 7 | 7 | |
| 8 | "gitbay.org/gitbay/internal/control" | |
| 8 | 9 | "gitbay.org/gitbay/internal/store" |
| 9 | 10 | ) |
| 10 | 11 | |
| @@ -17,7 +18,12 @@ func (s *Server) labels(w http.ResponseWriter, r *http.Request) { | ||
| 17 | 18 | return |
| 18 | 19 | } |
| 19 | 20 | p.Tab = "issues" |
| 20 | labels, err := s.st.ListLabels(p.Repo.ID) | |
| 21 | readable, err := control.ReadableScope(s.st, s.viewer(r), p.Repo) | |
| 22 | if err != nil { | |
| 23 | http.Error(w, "internal error", http.StatusInternalServerError) | |
| 24 | return | |
| 25 | } | |
| 26 | labels, err := s.st.ListLabels(p.Repo, readable) | |
| 21 | 27 | if err != nil { |
| 22 | 28 | http.Error(w, "internal error", http.StatusInternalServerError) |
| 23 | 29 | return |
| @@ -28,7 +34,7 @@ func (s *Server) labels(w http.ResponseWriter, r *http.Request) { | ||
| 28 | 34 | LabelColors map[string]template.CSS |
| 29 | 35 | CanWrite bool |
| 30 | 36 | Notice string |
| 31 | }{p, labels, s.labelColors(p.Repo.ID), s.canWriteRepo(r, p.Repo), s.takeFlash(w, r)}) | |
| 37 | }{p, labels, s.labelColors(p.Repo), s.canWriteRepo(r, p.Repo), s.takeFlash(w, r)}) | |
| 32 | 38 | } |
| 33 | 39 | |
| 34 | 40 | // labelSubmit creates a label, sets its colour, or removes it, through |
internal/httpd/web.go +23 −6
| @@ -702,7 +702,12 @@ func (s *Server) milestones(w http.ResponseWriter, r *http.Request) { | ||
| 702 | 702 | if state != "closed" && state != "all" { |
| 703 | 703 | state = "open" |
| 704 | 704 | } |
| 705 | ms, err := s.st.ListMilestones(p.Repo.ID, state) | |
| 705 | readable, err := control.ReadableScope(s.st, s.viewer(r), p.Repo) | |
| 706 | if err != nil { | |
| 707 | http.Error(w, "internal error", http.StatusInternalServerError) | |
| 708 | return | |
| 709 | } | |
| 710 | ms, err := s.st.ListMilestones(p.Repo, state, readable) | |
| 706 | 711 | if err != nil { |
| 707 | 712 | http.Error(w, "internal error", http.StatusInternalServerError) |
| 708 | 713 | return |
| @@ -1603,8 +1608,15 @@ func hexByte(s string) int64 { | ||
| 1603 | 1608 | // labelColors returns a complete label-name -> chip color map for a repo: |
| 1604 | 1609 | // the stored labels.color when it is a valid hex color, otherwise a |
| 1605 | 1610 | // stable default picked from the palette by name hash. |
| 1606 | func (s *Server) labelColors(repoID int64) map[string]template.CSS { | |
| 1607 | stored, _ := s.st.LabelColors(repoID) | |
| 1611 | func (s *Server) labelColors(repo store.Repo) map[string]template.CSS { | |
| 1612 | stored, _ := s.st.LabelColors(repo) | |
| 1613 | return colorStyles(stored) | |
| 1614 | } | |
| 1615 | ||
| 1616 | // colorStyles turns a label-name -> stored color map into chip styles: the | |
| 1617 | // stored color when it is a valid hex color, otherwise a stable default | |
| 1618 | // picked from the palette by name hash. | |
| 1619 | func colorStyles(stored map[string]string) map[string]template.CSS { | |
| 1608 | 1620 | out := make(map[string]template.CSS, len(stored)) |
| 1609 | 1621 | for name, color := range stored { |
| 1610 | 1622 | if !hexColorPat.MatchString(color) { |
| @@ -1672,7 +1684,7 @@ func (s *Server) issues(w http.ResponseWriter, r *http.Request) { | ||
| 1672 | 1684 | Older string |
| 1673 | 1685 | }{p, state, f.Label, f.Search, |
| 1674 | 1686 | activeFilters(state, [][2]string{{"label", f.Label}, {"assignee", f.Assignee}, {"author", f.Author}, {"milestone", f.Milestone}}), |
| 1675 | issues, s.labelColors(p.Repo.ID), older}) | |
| 1687 | issues, s.labelColors(p.Repo), older}) | |
| 1676 | 1688 | } |
| 1677 | 1689 | |
| 1678 | 1690 | func (s *Server) issue(w http.ResponseWriter, r *http.Request) { |
| @@ -1697,7 +1709,12 @@ func (s *Server) issue(w http.ResponseWriter, r *http.Request) { | ||
| 1697 | 1709 | return |
| 1698 | 1710 | } |
| 1699 | 1711 | md := s.ugcFor(r, p.Repo) |
| 1700 | milestones, _ := s.st.ListMilestones(p.Repo.ID, "open") | |
| 1712 | readable, err := control.ReadableScope(s.st, s.viewer(r), p.Repo) | |
| 1713 | if err != nil { | |
| 1714 | http.Error(w, "internal error", http.StatusInternalServerError) | |
| 1715 | return | |
| 1716 | } | |
| 1717 | milestones, _ := s.st.ListMilestones(p.Repo, "open", readable) | |
| 1701 | 1718 | s.render(w, "issue.html", struct { |
| 1702 | 1719 | repoPage |
| 1703 | 1720 | Issue store.Issue |
| @@ -1710,7 +1727,7 @@ func (s *Server) issue(w http.ResponseWriter, r *http.Request) { | ||
| 1710 | 1727 | LabelColors map[string]template.CSS |
| 1711 | 1728 | }{p, iss, md(iss.Body, iss.BodyFormat), renderComments(comments, md), |
| 1712 | 1729 | s.canEditItem(r, p.Repo, iss.Author), s.canWriteRepo(r, p.Repo), |
| 1713 | milestones, s.takeFlash(w, r), s.labelColors(p.Repo.ID)}) | |
| 1730 | milestones, s.takeFlash(w, r), s.labelColors(p.Repo)}) | |
| 1714 | 1731 | } |
| 1715 | 1732 | |
| 1716 | 1733 | // canEditItem: the author or anyone with write access may edit. |