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)
}