package main import ( "errors" "html/template" "net/http" "regexp" "strconv" "strings" ) type Label struct { ID int64 Name string Colour string } var colourRe = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`) func (s *Store) Labels(repo int64) ([]Label, error) { rows, err := s.db.Query(`select id, name, colour from labels where repo = ? order by name`, repo) if err != nil { return nil, err } defer rows.Close() var out []Label for rows.Next() { var l Label if err := rows.Scan(&l.ID, &l.Name, &l.Colour); err != nil { return nil, err } out = append(out, l) } return out, rows.Err() } func (s *Store) AddLabel(repo int64, name, colour string) error { name = strings.TrimSpace(name) if name == "" || len(name) > 40 { return errors.New("Label names are 1 to 40 characters") } if !colourRe.MatchString(colour) { colour = "#6c757d" } _, err := s.db.Exec(`insert into labels(repo, name, colour) values (?, ?, ?)`, repo, name, strings.ToLower(colour)) if err != nil && strings.Contains(err.Error(), "UNIQUE") { return errors.New("That label already exists") } return err } func (s *Store) DeleteLabel(repo, id int64) { s.db.Exec(`delete from labels where repo = ? and id = ?`, repo, id) } func (s *Store) ItemLabels(kind string, item int64) []Label { rows, err := s.db.Query(`select l.id, l.name, l.colour from labelled x join labels l on l.id = x.label where x.kind = ? and x.item = ? order by l.name`, kind, item) if err != nil { return nil } defer rows.Close() var out []Label for rows.Next() { var l Label rows.Scan(&l.ID, &l.Name, &l.Colour) out = append(out, l) } return out } func (s *Store) SetItemLabels(kind string, item int64, repo int64, ids []int64) { s.db.Exec(`delete from labelled where kind = ? and item = ?`, kind, item) for _, id := range ids { s.db.Exec(`insert or ignore into labelled(kind, item, label) select ?, ?, id from labels where id = ? and repo = ?`, kind, item, id, repo) } } func (s *Store) Assignees(kind string, item int64) []User { rows, err := s.db.Query(`select u.id, u.name from assigned a join users u on u.id = a.user where a.kind = ? and a.item = ? order by u.name`, kind, item) if err != nil { return nil } defer rows.Close() var out []User for rows.Next() { var u User rows.Scan(&u.ID, &u.Name) out = append(out, u) } return out } func (s *Store) SetAssignees(kind string, item int64, ids []int64) { s.db.Exec(`delete from assigned where kind = ? and item = ?`, kind, item) for _, id := range ids { s.db.Exec(`insert or ignore into assigned(kind, item, user) values (?, ?, ?)`, kind, item, id) } } func (s *Store) Assignable(repo Repo) []User { users := []User{repo.Owner} collabs, _ := s.Collaborators(repo.ID) return append(users, collabs...) } func (s *Store) ItemsWithLabel(kind string, label int64) map[int64]bool { out := map[int64]bool{} rows, err := s.db.Query(`select item from labelled where kind = ? and label = ?`, kind, label) if err != nil { return out } defer rows.Close() for rows.Next() { var id int64 rows.Scan(&id) out[id] = true } return out } type Protection struct { Branch string RequirePR bool Approvals int } func (s *Store) Protections(repo int64) []Protection { rows, err := s.db.Query(`select branch, require_pr, approvals from protected where repo = ? order by branch`, repo) if err != nil { return nil } defer rows.Close() var out []Protection for rows.Next() { var p Protection var rp int rows.Scan(&p.Branch, &rp, &p.Approvals) p.RequirePR = rp != 0 out = append(out, p) } return out } func (s *Store) Protection(repo int64, branch string) (Protection, bool) { var p Protection var rp int err := s.db.QueryRow(`select branch, require_pr, approvals from protected where repo = ? and branch = ?`, repo, branch).Scan(&p.Branch, &rp, &p.Approvals) p.RequirePR = rp != 0 return p, err == nil } func (s *Store) SetProtection(repo int64, branch string, requirePR bool, approvals int) error { branch = strings.TrimSpace(branch) if !validBranch(branch) { return errors.New("Give a branch name") } if approvals < 0 || approvals > 10 { approvals = 0 } _, err := s.db.Exec(`insert into protected(repo, branch, require_pr, approvals) values (?, ?, ?, ?) on conflict(repo, branch) do update set require_pr = excluded.require_pr, approvals = excluded.approvals`, repo, branch, requirePR, approvals) return err } func (s *Store) DeleteProtection(repo int64, branch string) { s.db.Exec(`delete from protected where repo = ? and branch = ?`, repo, branch) } func parseIDs(values []string) []int64 { var out []int64 for _, v := range values { if id, err := strconv.ParseInt(v, 10, 64); err == nil { out = append(out, id) } } return out } func (s *Server) manageLabels(w http.ResponseWriter, r *http.Request, repo Repo) { rp, _, _ := s.repoPage(r, repo, "") if !rp.IsOwner { s.fail(w, r, 403, "Not your repository") return } back := "/" + repo.Path() + "/settings" if id, err := strconv.ParseInt(r.FormValue("remove"), 10, 64); err == nil { s.store.DeleteLabel(repo.ID, id) } else if err := s.store.AddLabel(repo.ID, r.FormValue("name"), r.FormValue("colour")); err != nil { http.Redirect(w, r, back+"?error="+template.URLQueryEscaper(err.Error()), http.StatusSeeOther) return } http.Redirect(w, r, back+"#labels", http.StatusSeeOther) } func (s *Server) manageProtection(w http.ResponseWriter, r *http.Request, repo Repo) { rp, _, _ := s.repoPage(r, repo, "") if !rp.IsOwner { s.fail(w, r, 403, "Not your repository") return } back := "/" + repo.Path() + "/settings" if branch := r.FormValue("remove"); branch != "" { s.store.DeleteProtection(repo.ID, branch) } else { approvals, _ := strconv.Atoi(r.FormValue("approvals")) if err := s.store.SetProtection(repo.ID, r.FormValue("branch"), r.FormValue("require_pr") == "on", approvals); err != nil { http.Redirect(w, r, back+"?error="+template.URLQueryEscaper(err.Error()), http.StatusSeeOther) return } } http.Redirect(w, r, back+"#protection", http.StatusSeeOther) } func (s *Server) setMeta(w http.ResponseWriter, r *http.Request, repo Repo, kind string, item int64, back string) { rp, _, _ := s.repoPage(r, repo, "") if !rp.Owner { s.fail(w, r, 403, "Only collaborators can change labels and assignees") return } r.ParseForm() if _, ok := r.Form["labels_form"]; ok { s.store.SetItemLabels(kind, item, repo.ID, parseIDs(r.Form["label"])) } if _, ok := r.Form["assignees_form"]; ok { allowed := map[int64]bool{} for _, u := range s.store.Assignable(repo) { allowed[u.ID] = true } var ids []int64 for _, id := range parseIDs(r.Form["assignee"]) { if allowed[id] { ids = append(ids, id) } } s.store.SetAssignees(kind, item, ids) } http.Redirect(w, r, back, http.StatusSeeOther) }