rin
Public Open-source lightweight git hosting.
releases.go · 198 lines · 5.4 KB
Raw
1 package repo
2
3 import (
4 "context"
5 "database/sql"
6 "errors"
7 "fmt"
8 "strings"
9 "time"
10 )
11
12 var (
13 ErrReleaseNotFound = errors.New("release not found")
14 ErrEmptyReleaseTag = errors.New("release tag cannot be empty")
15 ErrEmptyReleaseTitle = errors.New("release title cannot be empty")
16 ErrReleaseAlreadyExist = errors.New("a release for this tag already exists")
17 )
18
19 type Release struct {
20 ID int64
21 RepoID int64
22 AuthorID int64
23 AuthorUsername string
24 AuthorAvatar string
25 TagName string
26 Title string
27 Body string
28 IsDraft bool
29 IsPrerelease bool
30 CreatedAt time.Time
31 PublishedAt time.Time
32 }
33
34 func (s *Service) CreateRelease(ctx context.Context, repoID, authorID int64, tagName, title, body string, isDraft, isPrerelease bool) (*Release, error) {
35 tagName = strings.TrimSpace(tagName)
36 title = strings.TrimSpace(title)
37 body = strings.TrimSpace(body)
38
39 if tagName == "" {
40 return nil, ErrEmptyReleaseTag
41 }
42 if title == "" {
43 title = tagName
44 }
45 if len(title) > 200 {
46 title = title[:200]
47 }
48 if len(body) > 50000 {
49 body = body[:50000]
50 }
51
52 cctx, cancel := context.WithTimeout(ctx, 5*time.Second)
53 defer cancel()
54
55 b2i := func(b bool) int {
56 if b {
57 return 1
58 }
59 return 0
60 }
61
62 now := time.Now().UTC()
63 res, err := s.db.ExecContext(cctx, `
64 INSERT INTO releases (repo_id, author_id, tag_name, title, body, is_draft, is_prerelease, created_at, published_at)
65 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
66 `, repoID, authorID, tagName, title, body, b2i(isDraft), b2i(isPrerelease), now, now)
67 if err != nil {
68 if strings.Contains(strings.ToLower(err.Error()), "unique") {
69 return nil, ErrReleaseAlreadyExist
70 }
71 return nil, fmt.Errorf("insert release: %w", err)
72 }
73
74 id, _ := res.LastInsertId()
75 return s.GetReleaseByID(ctx, repoID, id)
76 }
77
78 func (s *Service) GetRelease(ctx context.Context, repoID int64, tagName string) (*Release, error) {
79 cctx, cancel := context.WithTimeout(ctx, 3*time.Second)
80 defer cancel()
81
82 var rel Release
83 var draft, prerelease int
84 err := s.db.QueryRowContext(cctx, `
85 SELECT r.id, r.repo_id, r.author_id, u.username, u.avatar_url,
86 r.tag_name, r.title, r.body, r.is_draft, r.is_prerelease, r.created_at, r.published_at
87 FROM releases r
88 JOIN users u ON u.id = r.author_id
89 WHERE r.repo_id = ? AND r.tag_name = ?
90 `, repoID, tagName).Scan(
91 &rel.ID, &rel.RepoID, &rel.AuthorID, &rel.AuthorUsername, &rel.AuthorAvatar,
92 &rel.TagName, &rel.Title, &rel.Body, &draft, &prerelease, &rel.CreatedAt, &rel.PublishedAt,
93 )
94 if err != nil {
95 if errors.Is(err, sql.ErrNoRows) {
96 return nil, ErrReleaseNotFound
97 }
98 return nil, fmt.Errorf("get release: %w", err)
99 }
100 rel.IsDraft = draft == 1
101 rel.IsPrerelease = prerelease == 1
102 return &rel, nil
103 }
104
105 func (s *Service) GetReleaseByID(ctx context.Context, repoID, releaseID int64) (*Release, error) {
106 cctx, cancel := context.WithTimeout(ctx, 3*time.Second)
107 defer cancel()
108
109 var rel Release
110 var draft, prerelease int
111 err := s.db.QueryRowContext(cctx, `
112 SELECT r.id, r.repo_id, r.author_id, u.username, u.avatar_url,
113 r.tag_name, r.title, r.body, r.is_draft, r.is_prerelease, r.created_at, r.published_at
114 FROM releases r
115 JOIN users u ON u.id = r.author_id
116 WHERE r.repo_id = ? AND r.id = ?
117 `, repoID, releaseID).Scan(
118 &rel.ID, &rel.RepoID, &rel.AuthorID, &rel.AuthorUsername, &rel.AuthorAvatar,
119 &rel.TagName, &rel.Title, &rel.Body, &draft, &prerelease, &rel.CreatedAt, &rel.PublishedAt,
120 )
121 if err != nil {
122 if errors.Is(err, sql.ErrNoRows) {
123 return nil, ErrReleaseNotFound
124 }
125 return nil, fmt.Errorf("get release: %w", err)
126 }
127 rel.IsDraft = draft == 1
128 rel.IsPrerelease = prerelease == 1
129 return &rel, nil
130 }
131
132 func (s *Service) ListReleases(ctx context.Context, repoID int64) ([]Release, error) {
133 cctx, cancel := context.WithTimeout(ctx, 5*time.Second)
134 defer cancel()
135
136 rows, err := s.db.QueryContext(cctx, `
137 SELECT r.id, r.repo_id, r.author_id, u.username, u.avatar_url,
138 r.tag_name, r.title, r.body, r.is_draft, r.is_prerelease, r.created_at, r.published_at
139 FROM releases r
140 JOIN users u ON u.id = r.author_id
141 WHERE r.repo_id = ?
142 ORDER BY r.published_at DESC
143 `, repoID)
144 if err != nil {
145 return nil, fmt.Errorf("list releases: %w", err)
146 }
147 defer rows.Close()
148
149 var list []Release
150 for rows.Next() {
151 var rel Release
152 var draft, prerelease int
153 if err := rows.Scan(
154 &rel.ID, &rel.RepoID, &rel.AuthorID, &rel.AuthorUsername, &rel.AuthorAvatar,
155 &rel.TagName, &rel.Title, &rel.Body, &draft, &prerelease, &rel.CreatedAt, &rel.PublishedAt,
156 ); err != nil {
157 return nil, fmt.Errorf("scan release: %w", err)
158 }
159 rel.IsDraft = draft == 1
160 rel.IsPrerelease = prerelease == 1
161 list = append(list, rel)
162 }
163 return list, rows.Err()
164 }
165
166 func (s *Service) UpdateRelease(ctx context.Context, releaseID int64, title, body string, isPrerelease bool) error {
167 cctx, cancel := context.WithTimeout(ctx, 3*time.Second)
168 defer cancel()
169
170 title = strings.TrimSpace(title)
171 body = strings.TrimSpace(body)
172 if len(title) > 200 {
173 title = title[:200]
174 }
175 if len(body) > 50000 {
176 body = body[:50000]
177 }
178
179 preInt := 0
180 if isPrerelease {
181 preInt = 1
182 }
183
184 _, err := s.db.ExecContext(cctx, `
185 UPDATE releases
186 SET title = ?, body = ?, is_prerelease = ?
187 WHERE id = ?
188 `, title, body, preInt, releaseID)
189 return err
190 }
191
192 func (s *Service) DeleteRelease(ctx context.Context, releaseID int64) error {
193 cctx, cancel := context.WithTimeout(ctx, 3*time.Second)
194 defer cancel()
195
196 _, err := s.db.ExecContext(cctx, `DELETE FROM releases WHERE id = ?`, releaseID)
197 return err
198 }
199