ggdgsdbsdbbb / issue.v
253 lines · 229 sloc · 6.13 KB · 6ffab7283c4e0d62920b60afd19b2163f83cee5a
Raw
1// Copyright (c) 2019-2021 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by a GPL license that can be found in the LICENSE file.
3module main
4
5import time
6import veb
7import highlight
8
9struct Issue {
10 id int @[primary; sql: serial]
11mut:
12 author_id int
13 repo_id int
14 is_pr bool
15 assigned []int @[skip]
16 labels []Label @[skip]
17 comments_count int
18 title string
19 text string
20 created_at int
21 status IssueStatus @[skip]
22 linked_issues []int @[skip]
23 repo_author string @[skip]
24 repo_name string @[skip]
25}
26
27enum IssueStatus {
28 open = 0
29 closed = 1
30}
31
32struct Label {
33 id int @[primary; sql: serial]
34mut:
35 repo_id int
36 name string
37 color string
38}
39
40struct IssueLabel {
41 id int @[primary; sql: serial]
42mut:
43 issue_id int
44 label_id int
45}
46
47fn (mut app App) add_issue(repo_id int, author_id int, title string, text string) ! {
48 app.add_issue_returning_id(repo_id, author_id, title, text)!
49}
50
51fn (mut app App) add_issue_returning_id(repo_id int, author_id int, title string, text string) !int {
52 return app.add_imported_issue_returning_id(repo_id, author_id, title, text,
53 int(time.now().unix()))!
54}
55
56fn (mut app App) add_imported_issue_returning_id(repo_id int, author_id int, title string, text string, created_at int) !int {
57 issue := Issue{
58 title: title
59 text: text
60 repo_id: repo_id
61 author_id: author_id
62 created_at: created_at
63 }
64
65 sql app.db {
66 insert issue into Issue
67 }!
68 return db_last_insert_id(app.db)
69}
70
71fn (mut app App) find_or_create_label(repo_id int, name string, color string) !int {
72 existing := sql app.db {
73 select from Label where repo_id == repo_id && name == name limit 1
74 } or { []Label{} }
75 if existing.len > 0 {
76 return existing[0].id
77 }
78 label := Label{
79 repo_id: repo_id
80 name: name
81 color: color
82 }
83 sql app.db {
84 insert label into Label
85 }!
86 return db_last_insert_id(app.db)
87}
88
89fn (mut app App) add_issue_label(issue_id int, label_id int) ! {
90 existing := sql app.db {
91 select from IssueLabel where issue_id == issue_id && label_id == label_id limit 1
92 } or { []IssueLabel{} }
93 if existing.len > 0 {
94 return
95 }
96 link := IssueLabel{
97 issue_id: issue_id
98 label_id: label_id
99 }
100 sql app.db {
101 insert link into IssueLabel
102 }!
103}
104
105fn (app &App) get_issue_labels(issue_id int) []Label {
106 links := sql app.db {
107 select from IssueLabel where issue_id == issue_id
108 } or { []IssueLabel{} }
109 mut labels := []Label{cap: links.len}
110 for link in links {
111 label := sql app.db {
112 select from Label where id == link.label_id limit 1
113 } or { []Label{} }
114 if label.len > 0 {
115 labels << label[0]
116 }
117 }
118 return labels
119}
120
121fn (mut app App) find_issue_by_id(issue_id int) ?Issue {
122 issues := sql app.db {
123 select from Issue where id == issue_id limit 1
124 } or { []Issue{} }
125 if issues.len == 0 {
126 return none
127 }
128 return issues.first()
129}
130
131fn (mut app App) find_repo_issues_as_page(repo_id int, page int) []Issue {
132 off := page * commits_per_page
133 return sql app.db {
134 select from Issue where repo_id == repo_id && is_pr == false limit 35 offset off
135 } or { []Issue{} }
136}
137
138fn (mut app App) get_repo_issue_count(repo_id int) int {
139 return sql app.db {
140 select count from Issue where repo_id == repo_id
141 } or { 0 }
142}
143
144fn (mut app App) find_user_issues(user_id int) []Issue {
145 return sql app.db {
146 select from Issue where author_id == user_id && is_pr == false order by created_at desc
147 } or { []Issue{} }
148}
149
150fn (mut app App) find_user_mentioned_issues(username string) []Issue {
151 needle := '@' + username
152 mut seen := map[int]bool{}
153 mut result := []Issue{}
154 direct_rows := db_exec_values(app.db,
155 'select id from ${sql_table('Issue')} where is_pr = 0 and text like ${sql_like_pattern(needle)} order by created_at desc') or {
156 [][]string{}
157 }
158 for row in direct_rows {
159 id := row[0].int()
160 if id in seen {
161 continue
162 }
163 issue := app.find_issue_by_id(id) or { continue }
164 seen[id] = true
165 result << issue
166 }
167 comment_rows := db_exec_values(app.db,
168 'select distinct issue_id from ${sql_table('Comment')} where text like ${sql_like_pattern(needle)}') or {
169 [][]string{}
170 }
171 for row in comment_rows {
172 id := row[0].int()
173 if id in seen {
174 continue
175 }
176 issue := app.find_issue_by_id(id) or { continue }
177 if issue.is_pr {
178 continue
179 }
180 seen[id] = true
181 result << issue
182 }
183 result.sort(a.created_at > b.created_at)
184 return result
185}
186
187fn (mut app App) find_user_recent_issues(user_id int) []Issue {
188 mut seen := map[int]bool{}
189 mut result := []Issue{}
190 authored := app.find_user_issues(user_id)
191 for issue in authored {
192 if issue.id in seen {
193 continue
194 }
195 seen[issue.id] = true
196 result << issue
197 }
198 comment_rows := db_exec_values(app.db,
199 'select distinct issue_id from ${sql_table('Comment')} where author_id = ${user_id}') or {
200 [][]string{}
201 }
202 for row in comment_rows {
203 id := row[0].int()
204 if id in seen {
205 continue
206 }
207 issue := app.find_issue_by_id(id) or { continue }
208 if issue.is_pr {
209 continue
210 }
211 seen[id] = true
212 result << issue
213 }
214 result.sort(a.created_at > b.created_at)
215 return result
216}
217
218fn (mut app App) delete_repo_issues(repo_id int) ! {
219 sql app.db {
220 delete from Issue where repo_id == repo_id
221 }!
222}
223
224fn (mut app App) increment_issue_comments(id int) ! {
225 sql app.db {
226 update Issue set comments_count = comments_count + 1 where id == id
227 }!
228}
229
230fn (i &Issue) relative_time() string {
231 return time.unix(i.created_at).relative()
232}
233
234fn html_escape_text(s string) string {
235 return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
236}
237
238// formatted_title renders the issue title as inline markdown so titles like
239// `unknown method or field: ` + "`db.pg.Row.val`" + `` get <code> spans and
240// other inline markup. The wrapping <p> tag added by the markdown converter is
241// stripped so the title stays inline.
242fn (i &Issue) formatted_title() veb.RawHtml {
243 rendered := highlight.convert_markdown_to_html(i.title).trim_space()
244 if rendered.starts_with('<p>') && rendered.ends_with('</p>') {
245 return rendered[3..rendered.len - 4]
246 }
247 return rendered
248}
249
250// formatted_body renders the issue text as markdown.
251fn (i &Issue) formatted_body() veb.RawHtml {
252 return highlight.convert_markdown_to_html(i.text)
253}
254