1package archesrv
2
3import (
4 "io"
5 "net/http"
6 "path/filepath"
7 "strings"
8
9 "arche/internal/issuedb"
10)
11
12func (s *forgeServer) openIssueDB(repoName string) (*issuedb.DB, error) {
13 dir := filepath.Join(repoPath(s.dataDir(), repoName), ".arche")
14 return issuedb.Open(dir)
15}
16
17func (s *forgeServer) openIssueDBWithStore(repoName string) (*issuedb.DB, io.Closer, error) {
18 r, err := openRepo(s.dataDir(), repoName)
19 if err != nil {
20 return nil, nil, err
21 }
22 dir := filepath.Join(repoPath(s.dataDir(), repoName), ".arche")
23 idb, err := issuedb.NewWithStore(dir, r.Store)
24 if err != nil {
25 r.Store.Close() //nolint:errcheck
26 return nil, nil, err
27 }
28 return idb, r.Store, nil
29}
30
31type srvIssuesData struct {
32 Repo string
33 User *User
34 Issues []issueStubView
35}
36
37type issueStubView struct {
38 ID string
39 Title string
40 Status string
41}
42
43func (s *forgeServer) handleRepoIssues(w http.ResponseWriter, r *http.Request) {
44 repoName := r.PathValue("repo")
45 rec, err := s.db.GetRepo(repoName)
46 if err != nil || rec == nil {
47 http.NotFound(w, r)
48 return
49 }
50 user := s.db.currentUser(r)
51 if !s.db.CanRead(rec, user) {
52 http.Error(w, "Unauthorized", http.StatusUnauthorized)
53 return
54 }
55
56 idb, err := s.openIssueDB(repoName)
57 if err != nil {
58 http.Error(w, "open issuedb: "+err.Error(), http.StatusInternalServerError)
59 return
60 }
61 defer idb.Close()
62
63 stubs, err := idb.Issues.ListIssues()
64 if err != nil {
65 http.Error(w, "list issues: "+err.Error(), http.StatusInternalServerError)
66 return
67 }
68 var items []issueStubView
69 for _, st := range stubs {
70 items = append(items, issueStubView{ID: st.ID, Title: st.Title, Status: st.Status})
71 }
72 s.render(w, "srv_repo_issues.html", srvIssuesData{Repo: repoName, User: user, Issues: items})
73}
74
75func (s *forgeServer) handleRepoCreateIssue(w http.ResponseWriter, r *http.Request) {
76 repoName := r.PathValue("repo")
77 rec, err := s.db.GetRepo(repoName)
78 if err != nil || rec == nil {
79 http.NotFound(w, r)
80 return
81 }
82 user := s.db.currentUser(r)
83 if user == nil {
84 http.Error(w, "login required", http.StatusUnauthorized)
85 return
86 }
87 if !s.db.CanRead(rec, user) {
88 http.Error(w, "Unauthorized", http.StatusUnauthorized)
89 return
90 }
91
92 r.ParseForm() //nolint:errcheck
93 title := strings.TrimSpace(r.FormValue("title"))
94 body := r.FormValue("body")
95 if title == "" {
96 http.Error(w, "title required", http.StatusBadRequest)
97 return
98 }
99
100 idb, storeCloser, err := s.openIssueDBWithStore(repoName)
101 if err != nil {
102 http.Error(w, "open issuedb: "+err.Error(), http.StatusInternalServerError)
103 return
104 }
105 defer storeCloser.Close() //nolint:errcheck
106 defer idb.Close()
107
108 id, err := idb.Issues.CreateIssue(title, body, user.Username)
109 if err != nil {
110 http.Error(w, "create issue: "+err.Error(), http.StatusInternalServerError)
111 return
112 }
113 http.Redirect(w, r, "/"+repoName+"/issue?id="+id, http.StatusFound)
114}
115
116type srvIssueData struct {
117 Repo string
118 User *User
119 ID string
120 Title string
121 Status string
122 Body string
123 Labels []string
124 Comments []issueCommentView
125 BodyConflict *issueBodyConflictView
126}
127
128type issueBodyConflictView struct {
129 OurEdit string
130 TheirEdit string
131}
132
133type issueCommentView struct {
134 Author string
135 Text string
136}
137
138func (s *forgeServer) handleRepoIssue(w http.ResponseWriter, r *http.Request) {
139 repoName := r.PathValue("repo")
140 rec, err := s.db.GetRepo(repoName)
141 if err != nil || rec == nil {
142 http.NotFound(w, r)
143 return
144 }
145 user := s.db.currentUser(r)
146 if !s.db.CanRead(rec, user) {
147 http.Error(w, "Unauthorized", http.StatusUnauthorized)
148 return
149 }
150
151 id := r.URL.Query().Get("id")
152 if id == "" {
153 http.Error(w, "id required", http.StatusBadRequest)
154 return
155 }
156
157 idb, err := s.openIssueDB(repoName)
158 if err != nil {
159 http.Error(w, "open issuedb: "+err.Error(), http.StatusInternalServerError)
160 return
161 }
162 defer idb.Close()
163
164 iss, err := idb.Issues.GetIssue(id)
165 if err != nil {
166 http.NotFound(w, r)
167 return
168 }
169
170 var comments []issueCommentView
171 for _, c := range iss.Comments {
172 comments = append(comments, issueCommentView{Author: c.Author, Text: c.Text})
173 }
174
175 var bodyConflict *issueBodyConflictView
176 if iss.BodyConflict != nil {
177 bodyConflict = &issueBodyConflictView{
178 OurEdit: iss.BodyConflict.OurEdit,
179 TheirEdit: iss.BodyConflict.TheirEdit,
180 }
181 }
182
183 s.render(w, "srv_repo_issue.html", srvIssueData{
184 Repo: repoName,
185 User: user,
186 ID: iss.ID,
187 Title: iss.Title,
188 Status: iss.Status,
189 Body: iss.Body,
190 Labels: iss.Labels,
191 Comments: comments,
192 BodyConflict: bodyConflict,
193 })
194}
195
196func (s *forgeServer) handleRepoAddComment(w http.ResponseWriter, r *http.Request) {
197 repoName := r.PathValue("repo")
198 rec, err := s.db.GetRepo(repoName)
199 if err != nil || rec == nil {
200 http.NotFound(w, r)
201 return
202 }
203 user := s.db.currentUser(r)
204 if user == nil {
205 http.Error(w, "login required", http.StatusUnauthorized)
206 return
207 }
208 if !s.db.CanRead(rec, user) {
209 http.Error(w, "Unauthorized", http.StatusUnauthorized)
210 return
211 }
212
213 r.ParseForm() //nolint:errcheck
214 issueID := r.FormValue("issue_id")
215 text := strings.TrimSpace(r.FormValue("text"))
216 if issueID == "" || text == "" {
217 http.Error(w, "issue_id and text required", http.StatusBadRequest)
218 return
219 }
220
221 idb, storeCloser, err := s.openIssueDBWithStore(repoName)
222 if err != nil {
223 http.Error(w, "open issuedb: "+err.Error(), http.StatusInternalServerError)
224 return
225 }
226 defer storeCloser.Close() //nolint:errcheck
227 defer idb.Close()
228
229 if err := idb.Issues.AddComment(issueID, text, user.Username); err != nil {
230 http.Error(w, "add comment: "+err.Error(), http.StatusInternalServerError)
231 return
232 }
233 http.Redirect(w, r, "/"+repoName+"/issue?id="+issueID, http.StatusFound)
234}
235
236func (s *forgeServer) handleRepoResolveBody(w http.ResponseWriter, r *http.Request) {
237 repoName := r.PathValue("repo")
238 rec, err := s.db.GetRepo(repoName)
239 if err != nil || rec == nil {
240 http.NotFound(w, r)
241 return
242 }
243 user := s.db.currentUser(r)
244 if user == nil {
245 http.Error(w, "login required", http.StatusUnauthorized)
246 return
247 }
248 if !s.db.CanRead(rec, user) {
249 http.Error(w, "Unauthorized", http.StatusUnauthorized)
250 return
251 }
252
253 r.ParseForm() //nolint:errcheck
254 issueID := r.FormValue("issue_id")
255 choice := r.FormValue("choice")
256 if issueID == "" || (choice != "ours" && choice != "theirs") {
257 http.Error(w, "issue_id and valid choice required", http.StatusBadRequest)
258 return
259 }
260
261 idb, storeCloser, err := s.openIssueDBWithStore(repoName)
262 if err != nil {
263 http.Error(w, "open issuedb: "+err.Error(), http.StatusInternalServerError)
264 return
265 }
266 defer storeCloser.Close() //nolint:errcheck
267 defer idb.Close()
268
269 iss, err := idb.Issues.GetIssue(issueID)
270 if err != nil || iss.BodyConflict == nil {
271 http.Error(w, "no body conflict on this issue", http.StatusBadRequest)
272 return
273 }
274
275 resolved := iss.BodyConflict.OurEdit
276 if choice == "theirs" {
277 resolved = iss.BodyConflict.TheirEdit
278 }
279 if err := idb.Issues.SetBody(issueID, resolved, user.Username); err != nil {
280 http.Error(w, "resolve: "+err.Error(), http.StatusInternalServerError)
281 return
282 }
283 http.Redirect(w, r, "/"+repoName+"/issue?id="+issueID, http.StatusFound)
284}
285
286func (s *forgeServer) handleRepoSetStatus(w http.ResponseWriter, r *http.Request) {
287 repoName := r.PathValue("repo")
288 rec, err := s.db.GetRepo(repoName)
289 if err != nil || rec == nil {
290 http.NotFound(w, r)
291 return
292 }
293 user := s.db.currentUser(r)
294 if user == nil {
295 http.Error(w, "login required", http.StatusUnauthorized)
296 return
297 }
298 if !s.db.CanRead(rec, user) {
299 http.Error(w, "Unauthorized", http.StatusUnauthorized)
300 return
301 }
302
303 r.ParseForm() //nolint:errcheck
304 issueID := r.FormValue("issue_id")
305 status := r.FormValue("status")
306 if issueID == "" || status == "" {
307 http.Error(w, "issue_id and status required", http.StatusBadRequest)
308 return
309 }
310
311 idb, storeCloser, err := s.openIssueDBWithStore(repoName)
312 if err != nil {
313 http.Error(w, "open issuedb: "+err.Error(), http.StatusInternalServerError)
314 return
315 }
316 defer storeCloser.Close() //nolint:errcheck
317 defer idb.Close()
318
319 if err := idb.Issues.SetStatus(issueID, status, user.Username); err != nil {
320 http.Error(w, "set status: "+err.Error(), http.StatusInternalServerError)
321 return
322 }
323 http.Redirect(w, r, "/"+repoName+"/issue?id="+issueID, http.StatusFound)
324}