internal/store/milestones.go
115 lines · 3284 bytes
1package store
2
3import (
4 "database/sql"
5 "errors"
6 "fmt"
7)
8
9type Milestone struct {
10 ID int64
11 RepoID int64
12 Title string
13 Description string
14 DueDate string
15 State string // open | closed
16 CreatedAt string
17 OpenItems int // open issues + open MRs attached
18 ClosedItems int // closed issues + merged/closed MRs attached
19}
20
21func (s *Store) CreateMilestone(repoID int64, title, description, due string) (int64, error) {
22 res, err := s.DB.Exec(
23 "INSERT INTO milestones (repo_id, title, description, due_date) VALUES (?, ?, ?, ?)",
24 repoID, title, description, due)
25 if err != nil {
26 if isUniqueErr(err) {
27 return 0, fmt.Errorf("milestone %q already exists", title)
28 }
29 return 0, err
30 }
31 return res.LastInsertId()
32}
33
34const milestoneSelect = `
35 SELECT m.id, m.repo_id, m.title, m.description, m.due_date, m.state, m.created_at,
36 (SELECT COUNT(*) FROM issues i WHERE i.milestone_id = m.id AND i.state = 'open')
37 + (SELECT COUNT(*) FROM merge_requests r WHERE r.milestone_id = m.id AND r.state IN ('open','source_gone')),
38 (SELECT COUNT(*) FROM issues i WHERE i.milestone_id = m.id AND i.state = 'closed')
39 + (SELECT COUNT(*) FROM merge_requests r WHERE r.milestone_id = m.id AND r.state IN ('merged','closed'))
40 FROM milestones m`
41
42func scanMilestone(row interface{ Scan(...any) error }) (Milestone, error) {
43 var m Milestone
44 err := row.Scan(&m.ID, &m.RepoID, &m.Title, &m.Description, &m.DueDate, &m.State,
45 &m.CreatedAt, &m.OpenItems, &m.ClosedItems)
46 return m, err
47}
48
49func (s *Store) MilestoneByTitle(repoID int64, title string) (Milestone, error) {
50 m, err := scanMilestone(s.DB.QueryRow(
51 milestoneSelect+" WHERE m.repo_id = ? AND m.title = ?", repoID, title))
52 if errors.Is(err, sql.ErrNoRows) {
53 return m, ErrNotFound
54 }
55 return m, err
56}
57
58func (s *Store) ListMilestones(repoID int64, state string) ([]Milestone, error) {
59 q := milestoneSelect + " WHERE m.repo_id = ?"
60 args := []any{repoID}
61 if state != "all" {
62 q += " AND m.state = ?"
63 args = append(args, state)
64 }
65 q += " ORDER BY m.due_date = '', m.due_date, m.title"
66 rows, err := s.DB.Query(q, args...)
67 if err != nil {
68 return nil, err
69 }
70 defer rows.Close()
71 var out []Milestone
72 for rows.Next() {
73 m, err := scanMilestone(rows)
74 if err != nil {
75 return nil, err
76 }
77 out = append(out, m)
78 }
79 return out, rows.Err()
80}
81
82func (s *Store) SetMilestoneState(id int64, state string) error {
83 res, err := s.DB.Exec("UPDATE milestones SET state = ? WHERE id = ?", state, id)
84 if err != nil {
85 return err
86 }
87 if n, _ := res.RowsAffected(); n == 0 {
88 return ErrNotFound
89 }
90 return nil
91}
92
93// SetIssueMilestone attaches (or with milestoneID 0 clears) a milestone.
94func (s *Store) SetIssueMilestone(issueID, milestoneID int64) error {
95 return s.setItemMilestone("issues", issueID, milestoneID)
96}
97
98func (s *Store) SetMRMilestone(mrID, milestoneID int64) error {
99 return s.setItemMilestone("merge_requests", mrID, milestoneID)
100}
101
102func (s *Store) setItemMilestone(table string, id, milestoneID int64) error {
103 var v any
104 if milestoneID != 0 {
105 v = milestoneID
106 }
107 res, err := s.DB.Exec("UPDATE "+table+" SET milestone_id = ? WHERE id = ?", v, id)
108 if err != nil {
109 return err
110 }
111 if n, _ := res.RowsAffected(); n == 0 {
112 return ErrNotFound
113 }
114 return nil
115}