this repo has no description
1package state
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "io"
8 "log"
9 "math/rand/v2"
10 "net/http"
11 "path"
12 "slices"
13 "strconv"
14 "strings"
15 "time"
16
17 "github.com/bluesky-social/indigo/atproto/identity"
18 "github.com/bluesky-social/indigo/atproto/syntax"
19 securejoin "github.com/cyphar/filepath-securejoin"
20 "github.com/go-chi/chi/v5"
21 "github.com/sotangled/tangled/api/tangled"
22 "github.com/sotangled/tangled/appview/auth"
23 "github.com/sotangled/tangled/appview/db"
24 "github.com/sotangled/tangled/appview/pages"
25 "github.com/sotangled/tangled/types"
26
27 comatproto "github.com/bluesky-social/indigo/api/atproto"
28 lexutil "github.com/bluesky-social/indigo/lex/util"
29)
30
31func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) {
32 ref := chi.URLParam(r, "ref")
33 f, err := fullyResolvedRepo(r)
34 if err != nil {
35 log.Println("failed to fully resolve repo", err)
36 return
37 }
38
39 us, err := NewUnsignedClient(f.Knot, s.config.Dev)
40 if err != nil {
41 log.Printf("failed to create unsigned client for %s", f.Knot)
42 s.pages.Error503(w)
43 return
44 }
45
46 resp, err := us.Index(f.OwnerDid(), f.RepoName, ref)
47 if err != nil {
48 s.pages.Error503(w)
49 log.Println("failed to reach knotserver", err)
50 return
51 }
52 defer resp.Body.Close()
53
54 body, err := io.ReadAll(resp.Body)
55 if err != nil {
56 log.Printf("Error reading response body: %v", err)
57 return
58 }
59
60 var result types.RepoIndexResponse
61 err = json.Unmarshal(body, &result)
62 if err != nil {
63 log.Printf("Error unmarshalling response body: %v", err)
64 return
65 }
66
67 tagMap := make(map[string][]string)
68 for _, tag := range result.Tags {
69 hash := tag.Hash
70 tagMap[hash] = append(tagMap[hash], tag.Name)
71 }
72
73 for _, branch := range result.Branches {
74 hash := branch.Hash
75 tagMap[hash] = append(tagMap[hash], branch.Name)
76 }
77
78 user := s.auth.GetUser(r)
79 s.pages.RepoIndexPage(w, pages.RepoIndexParams{
80 LoggedInUser: user,
81 RepoInfo: f.RepoInfo(s, user),
82 TagMap: tagMap,
83 RepoIndexResponse: result,
84 })
85
86 return
87}
88
89func (s *State) RepoLog(w http.ResponseWriter, r *http.Request) {
90 f, err := fullyResolvedRepo(r)
91 if err != nil {
92 log.Println("failed to fully resolve repo", err)
93 return
94 }
95
96 page := 1
97 if r.URL.Query().Get("page") != "" {
98 page, err = strconv.Atoi(r.URL.Query().Get("page"))
99 if err != nil {
100 page = 1
101 }
102 }
103
104 ref := chi.URLParam(r, "ref")
105
106 protocol := "http"
107 if !s.config.Dev {
108 protocol = "https"
109 }
110
111 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/log/%s?page=%d&per_page=30", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, page))
112 if err != nil {
113 log.Println("failed to reach knotserver", err)
114 return
115 }
116
117 body, err := io.ReadAll(resp.Body)
118 if err != nil {
119 log.Printf("error reading response body: %v", err)
120 return
121 }
122
123 var repolog types.RepoLogResponse
124 err = json.Unmarshal(body, &repolog)
125 if err != nil {
126 log.Println("failed to parse json response", err)
127 return
128 }
129
130 user := s.auth.GetUser(r)
131 s.pages.RepoLog(w, pages.RepoLogParams{
132 LoggedInUser: user,
133 RepoInfo: f.RepoInfo(s, user),
134 RepoLogResponse: repolog,
135 })
136 return
137}
138
139func (s *State) RepoDescriptionEdit(w http.ResponseWriter, r *http.Request) {
140 f, err := fullyResolvedRepo(r)
141 if err != nil {
142 log.Println("failed to get repo and knot", err)
143 w.WriteHeader(http.StatusBadRequest)
144 return
145 }
146
147 user := s.auth.GetUser(r)
148 s.pages.EditRepoDescriptionFragment(w, pages.RepoDescriptionParams{
149 RepoInfo: f.RepoInfo(s, user),
150 })
151 return
152}
153
154func (s *State) RepoDescription(w http.ResponseWriter, r *http.Request) {
155 f, err := fullyResolvedRepo(r)
156 if err != nil {
157 log.Println("failed to get repo and knot", err)
158 w.WriteHeader(http.StatusBadRequest)
159 return
160 }
161
162 repoAt := f.RepoAt
163 rkey := repoAt.RecordKey().String()
164 if rkey == "" {
165 log.Println("invalid aturi for repo", err)
166 w.WriteHeader(http.StatusInternalServerError)
167 return
168 }
169
170 user := s.auth.GetUser(r)
171
172 switch r.Method {
173 case http.MethodGet:
174 s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{
175 RepoInfo: f.RepoInfo(s, user),
176 })
177 return
178 case http.MethodPut:
179 user := s.auth.GetUser(r)
180 newDescription := r.FormValue("description")
181 client, _ := s.auth.AuthorizedClient(r)
182
183 // optimistic update
184 err = db.UpdateDescription(s.db, string(repoAt), newDescription)
185 if err != nil {
186 log.Println("failed to perferom update-description query", err)
187 s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
188 return
189 }
190
191 // this is a bit of a pain because the golang atproto impl does not allow nil SwapRecord field
192 //
193 // SwapRecord is optional and should happen automagically, but given that it does not, we have to perform two requests
194 ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, user.Did, rkey)
195 if err != nil {
196 // failed to get record
197 s.pages.Notice(w, "repo-notice", "Failed to update description, no record found on PDS.")
198 return
199 }
200 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
201 Collection: tangled.RepoNSID,
202 Repo: user.Did,
203 Rkey: rkey,
204 SwapRecord: ex.Cid,
205 Record: &lexutil.LexiconTypeDecoder{
206 Val: &tangled.Repo{
207 Knot: f.Knot,
208 Name: f.RepoName,
209 Owner: user.Did,
210 AddedAt: &f.AddedAt,
211 Description: &newDescription,
212 },
213 },
214 })
215
216 if err != nil {
217 log.Println("failed to perferom update-description query", err)
218 // failed to get record
219 s.pages.Notice(w, "repo-notice", "Failed to update description, unable to save to PDS.")
220 return
221 }
222
223 newRepoInfo := f.RepoInfo(s, user)
224 newRepoInfo.Description = newDescription
225
226 s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{
227 RepoInfo: newRepoInfo,
228 })
229 return
230 }
231}
232
233func (s *State) EditPatch(w http.ResponseWriter, r *http.Request) {
234 user := s.auth.GetUser(r)
235 f, err := fullyResolvedRepo(r)
236 if err != nil {
237 log.Println("failed to get repo and knot", err)
238 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
239 return
240 }
241
242 prId := chi.URLParam(r, "pull")
243 prIdInt, err := strconv.Atoi(prId)
244 if err != nil {
245 http.Error(w, "bad pr id", http.StatusBadRequest)
246 log.Println("failed to parse pr id", err)
247 return
248 }
249
250 patch := r.FormValue("patch")
251 if patch == "" {
252 s.pages.Notice(w, "pull-error", "Patch is required.")
253 return
254 }
255
256 // Get pull information before updating to get the atproto record URI
257 pull, _, err := db.GetPullWithComments(s.db, f.RepoAt, prIdInt)
258 if err != nil {
259 log.Println("failed to get pull information", err)
260 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
261 return
262 }
263
264 // Start a transaction for database operations
265 tx, err := s.db.BeginTx(r.Context(), nil)
266 if err != nil {
267 log.Println("failed to start transaction", err)
268 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
269 return
270 }
271
272 // Set up deferred rollback that will be overridden by commit if successful
273 defer tx.Rollback()
274
275 // Update patch in the database within transaction
276 err = db.EditPatch(tx, f.RepoAt, prIdInt, patch)
277 if err != nil {
278 log.Println("failed to update patch", err)
279 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
280 return
281 }
282
283 // Update the atproto record
284 client, _ := s.auth.AuthorizedClient(r)
285 pullAt := pull.PullAt
286
287 // Get the existing record first
288 ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pullAt.RecordKey().String())
289 if err != nil {
290 log.Println("failed to get existing pull record", err)
291 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
292 return
293 }
294
295 // Update the record
296 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
297 Collection: tangled.RepoPullNSID,
298 Repo: user.Did,
299 Rkey: pullAt.RecordKey().String(),
300 SwapRecord: ex.Cid,
301 Record: &lexutil.LexiconTypeDecoder{
302 Val: &tangled.RepoPull{
303 Title: pull.Title,
304 PullId: int64(pull.PullId),
305 TargetRepo: string(f.RepoAt),
306 TargetBranch: pull.TargetBranch,
307 Patch: patch,
308 },
309 },
310 })
311
312 if err != nil {
313 log.Println("failed to update pull record in atproto", err)
314 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
315 return
316 }
317
318 // Commit the transaction now that both operations have succeeded
319 err = tx.Commit()
320 if err != nil {
321 log.Println("failed to commit transaction", err)
322 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
323 return
324 }
325
326 targetBranch := pull.TargetBranch
327
328 // Perform merge check
329 secret, err := db.GetRegistrationKey(s.db, f.Knot)
330 if err != nil {
331 log.Printf("no key found for domain %s: %s\n", f.Knot, err)
332 s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.")
333 return
334 }
335
336 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
337 if err != nil {
338 log.Printf("failed to create signed client for %s", f.Knot)
339 s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.")
340 return
341 }
342
343 resp, err := ksClient.MergeCheck([]byte(patch), user.Did, f.RepoName, targetBranch)
344 if err != nil {
345 log.Println("failed to check mergeability", err)
346 s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.")
347 return
348 }
349
350 respBody, err := io.ReadAll(resp.Body)
351 if err != nil {
352 log.Println("failed to read knotserver response body")
353 s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.")
354 return
355 }
356
357 var mergeCheckResponse types.MergeCheckResponse
358 err = json.Unmarshal(respBody, &mergeCheckResponse)
359 if err != nil {
360 log.Println("failed to unmarshal merge check response", err)
361 s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.")
362 return
363 }
364
365 s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, prIdInt))
366 return
367}
368
369func (s *State) NewPull(w http.ResponseWriter, r *http.Request) {
370 user := s.auth.GetUser(r)
371 f, err := fullyResolvedRepo(r)
372 if err != nil {
373 log.Println("failed to get repo and knot", err)
374 return
375 }
376
377 switch r.Method {
378 case http.MethodGet:
379 us, err := NewUnsignedClient(f.Knot, s.config.Dev)
380 if err != nil {
381 log.Printf("failed to create unsigned client for %s", f.Knot)
382 s.pages.Error503(w)
383 return
384 }
385
386 resp, err := us.Branches(f.OwnerDid(), f.RepoName)
387 if err != nil {
388 log.Println("failed to reach knotserver", err)
389 return
390 }
391
392 body, err := io.ReadAll(resp.Body)
393 if err != nil {
394 log.Printf("Error reading response body: %v", err)
395 return
396 }
397
398 var result types.RepoBranchesResponse
399 err = json.Unmarshal(body, &result)
400 if err != nil {
401 log.Println("failed to parse response:", err)
402 return
403 }
404
405 s.pages.RepoNewPull(w, pages.RepoNewPullParams{
406 LoggedInUser: user,
407 RepoInfo: f.RepoInfo(s, user),
408 Branches: result.Branches,
409 })
410 case http.MethodPost:
411 title := r.FormValue("title")
412 body := r.FormValue("body")
413 targetBranch := r.FormValue("targetBranch")
414 patch := r.FormValue("patch")
415
416 if title == "" || body == "" || patch == "" || targetBranch == "" {
417 s.pages.Notice(w, "pull", "Title, body and patch diff are required.")
418 return
419 }
420
421 tx, err := s.db.BeginTx(r.Context(), nil)
422 if err != nil {
423 log.Println("failed to start tx")
424 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
425 return
426 }
427
428 defer func() {
429 tx.Rollback()
430 err = s.enforcer.E.LoadPolicy()
431 if err != nil {
432 log.Println("failed to rollback policies")
433 }
434 }()
435
436 err = db.NewPull(tx, &db.Pull{
437 Title: title,
438 Body: body,
439 TargetBranch: targetBranch,
440 Patch: patch,
441 OwnerDid: user.Did,
442 RepoAt: f.RepoAt,
443 })
444 if err != nil {
445 log.Println("failed to create pull request", err)
446 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
447 return
448 }
449 client, _ := s.auth.AuthorizedClient(r)
450 pullId, err := db.NextPullId(s.db, f.RepoAt)
451 if err != nil {
452 log.Println("failed to get pull id", err)
453 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
454 return
455 }
456
457 atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
458 Collection: tangled.RepoPullNSID,
459 Repo: user.Did,
460 Rkey: s.TID(),
461 Record: &lexutil.LexiconTypeDecoder{
462 Val: &tangled.RepoPull{
463 Title: title,
464 PullId: int64(pullId),
465 TargetRepo: string(f.RepoAt),
466 TargetBranch: targetBranch,
467 Patch: patch,
468 },
469 },
470 })
471
472 err = db.SetPullAt(s.db, f.RepoAt, pullId, atResp.Uri)
473 if err != nil {
474 log.Println("failed to get pull id", err)
475 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
476 return
477 }
478
479 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId))
480 return
481 }
482}
483
484func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) {
485 user := s.auth.GetUser(r)
486 f, err := fullyResolvedRepo(r)
487 if err != nil {
488 log.Println("failed to get repo and knot", err)
489 return
490 }
491
492 prId := chi.URLParam(r, "pull")
493 prIdInt, err := strconv.Atoi(prId)
494 if err != nil {
495 http.Error(w, "bad pr id", http.StatusBadRequest)
496 log.Println("failed to parse pr id", err)
497 return
498 }
499
500 pr, comments, err := db.GetPullWithComments(s.db, f.RepoAt, prIdInt)
501 if err != nil {
502 log.Println("failed to get pr and comments", err)
503 s.pages.Notice(w, "pull", "Failed to load pull request. Try again later.")
504 return
505 }
506
507 pullOwnerIdent, err := s.resolver.ResolveIdent(r.Context(), pr.OwnerDid)
508 if err != nil {
509 log.Println("failed to resolve pull owner", err)
510 }
511
512 identsToResolve := make([]string, len(comments))
513 for i, comment := range comments {
514 identsToResolve[i] = comment.OwnerDid
515 }
516 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
517 didHandleMap := make(map[string]string)
518 for _, identity := range resolvedIds {
519 if !identity.Handle.IsInvalidHandle() {
520 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
521 } else {
522 didHandleMap[identity.DID.String()] = identity.DID.String()
523 }
524 }
525
526 secret, err := db.GetRegistrationKey(s.db, f.Knot)
527 if err != nil {
528 log.Printf("failed to get registration key for %s", f.Knot)
529 s.pages.Notice(w, "pull", "Failed to load pull request. Try again later.")
530 return
531 }
532
533 var mergeCheckResponse types.MergeCheckResponse
534 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
535 if err == nil {
536 resp, err := ksClient.MergeCheck([]byte(pr.Patch), pr.OwnerDid, f.RepoName, pr.TargetBranch)
537 if err != nil {
538 log.Println("failed to check for mergeability:", err)
539 } else {
540 respBody, err := io.ReadAll(resp.Body)
541 if err != nil {
542 log.Println("failed to read merge check response body")
543 } else {
544 err = json.Unmarshal(respBody, &mergeCheckResponse)
545 if err != nil {
546 log.Println("failed to unmarshal merge check response", err)
547 }
548 }
549 }
550 } else {
551 log.Printf("failed to setup signed client for %s; ignoring...", f.Knot)
552 }
553
554 s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
555 LoggedInUser: user,
556 RepoInfo: f.RepoInfo(s, user),
557 Pull: *pr,
558 Comments: comments,
559 PullOwnerHandle: pullOwnerIdent.Handle.String(),
560 DidHandleMap: didHandleMap,
561 MergeCheck: mergeCheckResponse,
562 })
563}
564
565func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) {
566 f, err := fullyResolvedRepo(r)
567 if err != nil {
568 log.Println("failed to fully resolve repo", err)
569 return
570 }
571 ref := chi.URLParam(r, "ref")
572 protocol := "http"
573 if !s.config.Dev {
574 protocol = "https"
575 }
576 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref))
577 if err != nil {
578 log.Println("failed to reach knotserver", err)
579 return
580 }
581
582 body, err := io.ReadAll(resp.Body)
583 if err != nil {
584 log.Printf("Error reading response body: %v", err)
585 return
586 }
587
588 var result types.RepoCommitResponse
589 err = json.Unmarshal(body, &result)
590 if err != nil {
591 log.Println("failed to parse response:", err)
592 return
593 }
594
595 user := s.auth.GetUser(r)
596 s.pages.RepoCommit(w, pages.RepoCommitParams{
597 LoggedInUser: user,
598 RepoInfo: f.RepoInfo(s, user),
599 RepoCommitResponse: result,
600 })
601 return
602}
603
604func (s *State) RepoTree(w http.ResponseWriter, r *http.Request) {
605 f, err := fullyResolvedRepo(r)
606 if err != nil {
607 log.Println("failed to fully resolve repo", err)
608 return
609 }
610
611 ref := chi.URLParam(r, "ref")
612 treePath := chi.URLParam(r, "*")
613 protocol := "http"
614 if !s.config.Dev {
615 protocol = "https"
616 }
617 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath))
618 if err != nil {
619 log.Println("failed to reach knotserver", err)
620 return
621 }
622
623 body, err := io.ReadAll(resp.Body)
624 if err != nil {
625 log.Printf("Error reading response body: %v", err)
626 return
627 }
628
629 var result types.RepoTreeResponse
630 err = json.Unmarshal(body, &result)
631 if err != nil {
632 log.Println("failed to parse response:", err)
633 return
634 }
635
636 user := s.auth.GetUser(r)
637
638 var breadcrumbs [][]string
639 breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/%s/tree/%s", f.OwnerDid(), f.RepoName, ref)})
640 if treePath != "" {
641 for idx, elem := range strings.Split(treePath, "/") {
642 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)})
643 }
644 }
645
646 baseTreeLink := path.Join(f.OwnerDid(), f.RepoName, "tree", ref, treePath)
647 baseBlobLink := path.Join(f.OwnerDid(), f.RepoName, "blob", ref, treePath)
648
649 s.pages.RepoTree(w, pages.RepoTreeParams{
650 LoggedInUser: user,
651 BreadCrumbs: breadcrumbs,
652 BaseTreeLink: baseTreeLink,
653 BaseBlobLink: baseBlobLink,
654 RepoInfo: f.RepoInfo(s, user),
655 RepoTreeResponse: result,
656 })
657 return
658}
659
660func (s *State) RepoTags(w http.ResponseWriter, r *http.Request) {
661 f, err := fullyResolvedRepo(r)
662 if err != nil {
663 log.Println("failed to get repo and knot", err)
664 return
665 }
666
667 protocol := "http"
668 if !s.config.Dev {
669 protocol = "https"
670 }
671
672 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tags", protocol, f.Knot, f.OwnerDid(), f.RepoName))
673 if err != nil {
674 log.Println("failed to reach knotserver", err)
675 return
676 }
677
678 body, err := io.ReadAll(resp.Body)
679 if err != nil {
680 log.Printf("Error reading response body: %v", err)
681 return
682 }
683
684 var result types.RepoTagsResponse
685 err = json.Unmarshal(body, &result)
686 if err != nil {
687 log.Println("failed to parse response:", err)
688 return
689 }
690
691 user := s.auth.GetUser(r)
692 s.pages.RepoTags(w, pages.RepoTagsParams{
693 LoggedInUser: user,
694 RepoInfo: f.RepoInfo(s, user),
695 RepoTagsResponse: result,
696 })
697 return
698}
699
700func (s *State) RepoBranches(w http.ResponseWriter, r *http.Request) {
701 f, err := fullyResolvedRepo(r)
702 if err != nil {
703 log.Println("failed to get repo and knot", err)
704 return
705 }
706
707 us, err := NewUnsignedClient(f.Knot, s.config.Dev)
708 if err != nil {
709 log.Println("failed to create unsigned client", err)
710 return
711 }
712
713 resp, err := us.Branches(f.OwnerDid(), f.RepoName)
714 if err != nil {
715 log.Println("failed to reach knotserver", err)
716 return
717 }
718
719 body, err := io.ReadAll(resp.Body)
720 if err != nil {
721 log.Printf("Error reading response body: %v", err)
722 return
723 }
724
725 var result types.RepoBranchesResponse
726 err = json.Unmarshal(body, &result)
727 if err != nil {
728 log.Println("failed to parse response:", err)
729 return
730 }
731
732 user := s.auth.GetUser(r)
733 s.pages.RepoBranches(w, pages.RepoBranchesParams{
734 LoggedInUser: user,
735 RepoInfo: f.RepoInfo(s, user),
736 RepoBranchesResponse: result,
737 })
738 return
739}
740
741func (s *State) RepoBlob(w http.ResponseWriter, r *http.Request) {
742 f, err := fullyResolvedRepo(r)
743 if err != nil {
744 log.Println("failed to get repo and knot", err)
745 return
746 }
747
748 ref := chi.URLParam(r, "ref")
749 filePath := chi.URLParam(r, "*")
750 protocol := "http"
751 if !s.config.Dev {
752 protocol = "https"
753 }
754 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath))
755 if err != nil {
756 log.Println("failed to reach knotserver", err)
757 return
758 }
759
760 body, err := io.ReadAll(resp.Body)
761 if err != nil {
762 log.Printf("Error reading response body: %v", err)
763 return
764 }
765
766 var result types.RepoBlobResponse
767 err = json.Unmarshal(body, &result)
768 if err != nil {
769 log.Println("failed to parse response:", err)
770 return
771 }
772
773 var breadcrumbs [][]string
774 breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/%s/tree/%s", f.OwnerDid(), f.RepoName, ref)})
775 if filePath != "" {
776 for idx, elem := range strings.Split(filePath, "/") {
777 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)})
778 }
779 }
780
781 user := s.auth.GetUser(r)
782 s.pages.RepoBlob(w, pages.RepoBlobParams{
783 LoggedInUser: user,
784 RepoInfo: f.RepoInfo(s, user),
785 RepoBlobResponse: result,
786 BreadCrumbs: breadcrumbs,
787 })
788 return
789}
790
791func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) {
792 f, err := fullyResolvedRepo(r)
793 if err != nil {
794 log.Println("failed to get repo and knot", err)
795 return
796 }
797
798 collaborator := r.FormValue("collaborator")
799 if collaborator == "" {
800 http.Error(w, "malformed form", http.StatusBadRequest)
801 return
802 }
803
804 collaboratorIdent, err := s.resolver.ResolveIdent(r.Context(), collaborator)
805 if err != nil {
806 w.Write([]byte("failed to resolve collaborator did to a handle"))
807 return
808 }
809 log.Printf("adding %s to %s\n", collaboratorIdent.Handle.String(), f.Knot)
810
811 // TODO: create an atproto record for this
812
813 secret, err := db.GetRegistrationKey(s.db, f.Knot)
814 if err != nil {
815 log.Printf("no key found for domain %s: %s\n", f.Knot, err)
816 return
817 }
818
819 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
820 if err != nil {
821 log.Println("failed to create client to ", f.Knot)
822 return
823 }
824
825 ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String())
826 if err != nil {
827 log.Printf("failed to make request to %s: %s", f.Knot, err)
828 return
829 }
830
831 if ksResp.StatusCode != http.StatusNoContent {
832 w.Write([]byte(fmt.Sprint("knotserver failed to add collaborator: ", err)))
833 return
834 }
835
836 tx, err := s.db.BeginTx(r.Context(), nil)
837 if err != nil {
838 log.Println("failed to start tx")
839 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
840 return
841 }
842 defer func() {
843 tx.Rollback()
844 err = s.enforcer.E.LoadPolicy()
845 if err != nil {
846 log.Println("failed to rollback policies")
847 }
848 }()
849
850 err = s.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.OwnerSlashRepo())
851 if err != nil {
852 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
853 return
854 }
855
856 err = db.AddCollaborator(s.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot)
857 if err != nil {
858 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
859 return
860 }
861
862 err = tx.Commit()
863 if err != nil {
864 log.Println("failed to commit changes", err)
865 http.Error(w, err.Error(), http.StatusInternalServerError)
866 return
867 }
868
869 err = s.enforcer.E.SavePolicy()
870 if err != nil {
871 log.Println("failed to update ACLs", err)
872 http.Error(w, err.Error(), http.StatusInternalServerError)
873 return
874 }
875
876 w.Write([]byte(fmt.Sprint("added collaborator: ", collaboratorIdent.Handle.String())))
877
878}
879
880func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) {
881 f, err := fullyResolvedRepo(r)
882 if err != nil {
883 log.Println("failed to get repo and knot", err)
884 return
885 }
886
887 switch r.Method {
888 case http.MethodGet:
889 // for now, this is just pubkeys
890 user := s.auth.GetUser(r)
891 repoCollaborators, err := f.Collaborators(r.Context(), s)
892 if err != nil {
893 log.Println("failed to get collaborators", err)
894 }
895
896 isCollaboratorInviteAllowed := false
897 if user != nil {
898 ok, err := s.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.OwnerSlashRepo())
899 if err == nil && ok {
900 isCollaboratorInviteAllowed = true
901 }
902 }
903
904 s.pages.RepoSettings(w, pages.RepoSettingsParams{
905 LoggedInUser: user,
906 RepoInfo: f.RepoInfo(s, user),
907 Collaborators: repoCollaborators,
908 IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed,
909 })
910 }
911}
912
913type FullyResolvedRepo struct {
914 Knot string
915 OwnerId identity.Identity
916 RepoName string
917 RepoAt syntax.ATURI
918 Description string
919 AddedAt string
920}
921
922func (f *FullyResolvedRepo) OwnerDid() string {
923 return f.OwnerId.DID.String()
924}
925
926func (f *FullyResolvedRepo) OwnerHandle() string {
927 return f.OwnerId.Handle.String()
928}
929
930func (f *FullyResolvedRepo) OwnerSlashRepo() string {
931 p, _ := securejoin.SecureJoin(f.OwnerDid(), f.RepoName)
932 return p
933}
934
935func (f *FullyResolvedRepo) Collaborators(ctx context.Context, s *State) ([]pages.Collaborator, error) {
936 repoCollaborators, err := s.enforcer.E.GetImplicitUsersForResourceByDomain(f.OwnerSlashRepo(), f.Knot)
937 if err != nil {
938 return nil, err
939 }
940
941 var collaborators []pages.Collaborator
942 for _, item := range repoCollaborators {
943 // currently only two roles: owner and member
944 var role string
945 if item[3] == "repo:owner" {
946 role = "owner"
947 } else if item[3] == "repo:collaborator" {
948 role = "collaborator"
949 } else {
950 continue
951 }
952
953 did := item[0]
954
955 c := pages.Collaborator{
956 Did: did,
957 Handle: "",
958 Role: role,
959 }
960 collaborators = append(collaborators, c)
961 }
962
963 // populate all collborators with handles
964 identsToResolve := make([]string, len(collaborators))
965 for i, collab := range collaborators {
966 identsToResolve[i] = collab.Did
967 }
968
969 resolvedIdents := s.resolver.ResolveIdents(ctx, identsToResolve)
970 for i, resolved := range resolvedIdents {
971 if resolved != nil {
972 collaborators[i].Handle = resolved.Handle.String()
973 }
974 }
975
976 return collaborators, nil
977}
978
979func (f *FullyResolvedRepo) RepoInfo(s *State, u *auth.User) pages.RepoInfo {
980 isStarred := false
981 if u != nil {
982 isStarred = db.GetStarStatus(s.db, u.Did, syntax.ATURI(f.RepoAt))
983 }
984
985 starCount, err := db.GetStarCount(s.db, f.RepoAt)
986 if err != nil {
987 log.Println("failed to get star count for ", f.RepoAt)
988 }
989 issueCount, err := db.GetIssueCount(s.db, f.RepoAt)
990 if err != nil {
991 log.Println("failed to get issue count for ", f.RepoAt)
992 }
993 pullCount, err := db.GetPullCount(s.db, f.RepoAt)
994 if err != nil {
995 log.Println("failed to get issue count for ", f.RepoAt)
996 }
997
998 knot := f.Knot
999 if knot == "knot1.tangled.sh" {
1000 knot = "tangled.sh"
1001 }
1002
1003 return pages.RepoInfo{
1004 OwnerDid: f.OwnerDid(),
1005 OwnerHandle: f.OwnerHandle(),
1006 Name: f.RepoName,
1007 RepoAt: f.RepoAt,
1008 Description: f.Description,
1009 IsStarred: isStarred,
1010 Knot: knot,
1011 Roles: rolesInRepo(s, u, f),
1012 Stats: db.RepoStats{
1013 StarCount: starCount,
1014 IssueCount: issueCount,
1015 PullCount: pullCount,
1016 },
1017 }
1018}
1019
1020func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) {
1021 user := s.auth.GetUser(r)
1022 f, err := fullyResolvedRepo(r)
1023 if err != nil {
1024 log.Println("failed to get repo and knot", err)
1025 return
1026 }
1027
1028 issueId := chi.URLParam(r, "issue")
1029 issueIdInt, err := strconv.Atoi(issueId)
1030 if err != nil {
1031 http.Error(w, "bad issue id", http.StatusBadRequest)
1032 log.Println("failed to parse issue id", err)
1033 return
1034 }
1035
1036 issue, comments, err := db.GetIssueWithComments(s.db, f.RepoAt, issueIdInt)
1037 if err != nil {
1038 log.Println("failed to get issue and comments", err)
1039 s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
1040 return
1041 }
1042
1043 issueOwnerIdent, err := s.resolver.ResolveIdent(r.Context(), issue.OwnerDid)
1044 if err != nil {
1045 log.Println("failed to resolve issue owner", err)
1046 }
1047
1048 identsToResolve := make([]string, len(comments))
1049 for i, comment := range comments {
1050 identsToResolve[i] = comment.OwnerDid
1051 }
1052 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
1053 didHandleMap := make(map[string]string)
1054 for _, identity := range resolvedIds {
1055 if !identity.Handle.IsInvalidHandle() {
1056 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
1057 } else {
1058 didHandleMap[identity.DID.String()] = identity.DID.String()
1059 }
1060 }
1061
1062 s.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{
1063 LoggedInUser: user,
1064 RepoInfo: f.RepoInfo(s, user),
1065 Issue: *issue,
1066 Comments: comments,
1067
1068 IssueOwnerHandle: issueOwnerIdent.Handle.String(),
1069 DidHandleMap: didHandleMap,
1070 })
1071
1072}
1073
1074func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) {
1075 user := s.auth.GetUser(r)
1076 f, err := fullyResolvedRepo(r)
1077 if err != nil {
1078 log.Println("failed to get repo and knot", err)
1079 return
1080 }
1081
1082 issueId := chi.URLParam(r, "issue")
1083 issueIdInt, err := strconv.Atoi(issueId)
1084 if err != nil {
1085 http.Error(w, "bad issue id", http.StatusBadRequest)
1086 log.Println("failed to parse issue id", err)
1087 return
1088 }
1089
1090 issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt)
1091 if err != nil {
1092 log.Println("failed to get issue", err)
1093 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
1094 return
1095 }
1096
1097 collaborators, err := f.Collaborators(r.Context(), s)
1098 if err != nil {
1099 log.Println("failed to fetch repo collaborators: %w", err)
1100 }
1101 isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {
1102 return user.Did == collab.Did
1103 })
1104 isIssueOwner := user.Did == issue.OwnerDid
1105
1106 // TODO: make this more granular
1107 if isIssueOwner || isCollaborator {
1108
1109 closed := tangled.RepoIssueStateClosed
1110
1111 client, _ := s.auth.AuthorizedClient(r)
1112 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1113 Collection: tangled.RepoIssueStateNSID,
1114 Repo: user.Did,
1115 Rkey: s.TID(),
1116 Record: &lexutil.LexiconTypeDecoder{
1117 Val: &tangled.RepoIssueState{
1118 Issue: issue.IssueAt,
1119 State: &closed,
1120 },
1121 },
1122 })
1123
1124 if err != nil {
1125 log.Println("failed to update issue state", err)
1126 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
1127 return
1128 }
1129
1130 err := db.CloseIssue(s.db, f.RepoAt, issueIdInt)
1131 if err != nil {
1132 log.Println("failed to close issue", err)
1133 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
1134 return
1135 }
1136
1137 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt))
1138 return
1139 } else {
1140 log.Println("user is not permitted to close issue")
1141 http.Error(w, "for biden", http.StatusUnauthorized)
1142 return
1143 }
1144}
1145
1146func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) {
1147 user := s.auth.GetUser(r)
1148 f, err := fullyResolvedRepo(r)
1149 if err != nil {
1150 log.Println("failed to get repo and knot", err)
1151 return
1152 }
1153
1154 issueId := chi.URLParam(r, "issue")
1155 issueIdInt, err := strconv.Atoi(issueId)
1156 if err != nil {
1157 http.Error(w, "bad issue id", http.StatusBadRequest)
1158 log.Println("failed to parse issue id", err)
1159 return
1160 }
1161
1162 issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt)
1163 if err != nil {
1164 log.Println("failed to get issue", err)
1165 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
1166 return
1167 }
1168
1169 collaborators, err := f.Collaborators(r.Context(), s)
1170 if err != nil {
1171 log.Println("failed to fetch repo collaborators: %w", err)
1172 }
1173 isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {
1174 return user.Did == collab.Did
1175 })
1176 isIssueOwner := user.Did == issue.OwnerDid
1177
1178 if isCollaborator || isIssueOwner {
1179 err := db.ReopenIssue(s.db, f.RepoAt, issueIdInt)
1180 if err != nil {
1181 log.Println("failed to reopen issue", err)
1182 s.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.")
1183 return
1184 }
1185 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt))
1186 return
1187 } else {
1188 log.Println("user is not the owner of the repo")
1189 http.Error(w, "forbidden", http.StatusUnauthorized)
1190 return
1191 }
1192}
1193
1194func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) {
1195 user := s.auth.GetUser(r)
1196 f, err := fullyResolvedRepo(r)
1197 if err != nil {
1198 log.Println("failed to get repo and knot", err)
1199 return
1200 }
1201
1202 issueId := chi.URLParam(r, "issue")
1203 issueIdInt, err := strconv.Atoi(issueId)
1204 if err != nil {
1205 http.Error(w, "bad issue id", http.StatusBadRequest)
1206 log.Println("failed to parse issue id", err)
1207 return
1208 }
1209
1210 switch r.Method {
1211 case http.MethodPost:
1212 body := r.FormValue("body")
1213 if body == "" {
1214 s.pages.Notice(w, "issue", "Body is required")
1215 return
1216 }
1217
1218 commentId := rand.IntN(1000000)
1219
1220 err := db.NewComment(s.db, &db.Comment{
1221 OwnerDid: user.Did,
1222 RepoAt: f.RepoAt,
1223 Issue: issueIdInt,
1224 CommentId: commentId,
1225 Body: body,
1226 })
1227 if err != nil {
1228 log.Println("failed to create comment", err)
1229 s.pages.Notice(w, "issue-comment", "Failed to create comment.")
1230 return
1231 }
1232
1233 createdAt := time.Now().Format(time.RFC3339)
1234 commentIdInt64 := int64(commentId)
1235 ownerDid := user.Did
1236 issueAt, err := db.GetIssueAt(s.db, f.RepoAt, issueIdInt)
1237 if err != nil {
1238 log.Println("failed to get issue at", err)
1239 s.pages.Notice(w, "issue-comment", "Failed to create comment.")
1240 return
1241 }
1242
1243 atUri := f.RepoAt.String()
1244 client, _ := s.auth.AuthorizedClient(r)
1245 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1246 Collection: tangled.RepoIssueCommentNSID,
1247 Repo: user.Did,
1248 Rkey: s.TID(),
1249 Record: &lexutil.LexiconTypeDecoder{
1250 Val: &tangled.RepoIssueComment{
1251 Repo: &atUri,
1252 Issue: issueAt,
1253 CommentId: &commentIdInt64,
1254 Owner: &ownerDid,
1255 Body: &body,
1256 CreatedAt: &createdAt,
1257 },
1258 },
1259 })
1260 if err != nil {
1261 log.Println("failed to create comment", err)
1262 s.pages.Notice(w, "issue-comment", "Failed to create comment.")
1263 return
1264 }
1265
1266 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issueIdInt, commentId))
1267 return
1268 }
1269}
1270
1271func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) {
1272 params := r.URL.Query()
1273 state := params.Get("state")
1274 isOpen := true
1275 switch state {
1276 case "open":
1277 isOpen = true
1278 case "closed":
1279 isOpen = false
1280 default:
1281 isOpen = true
1282 }
1283
1284 user := s.auth.GetUser(r)
1285 f, err := fullyResolvedRepo(r)
1286 if err != nil {
1287 log.Println("failed to get repo and knot", err)
1288 return
1289 }
1290
1291 issues, err := db.GetIssues(s.db, f.RepoAt, isOpen)
1292 if err != nil {
1293 log.Println("failed to get issues", err)
1294 s.pages.Notice(w, "issues", "Failed to load issues. Try again later.")
1295 return
1296 }
1297
1298 identsToResolve := make([]string, len(issues))
1299 for i, issue := range issues {
1300 identsToResolve[i] = issue.OwnerDid
1301 }
1302 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
1303 didHandleMap := make(map[string]string)
1304 for _, identity := range resolvedIds {
1305 if !identity.Handle.IsInvalidHandle() {
1306 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
1307 } else {
1308 didHandleMap[identity.DID.String()] = identity.DID.String()
1309 }
1310 }
1311
1312 s.pages.RepoIssues(w, pages.RepoIssuesParams{
1313 LoggedInUser: s.auth.GetUser(r),
1314 RepoInfo: f.RepoInfo(s, user),
1315 Issues: issues,
1316 DidHandleMap: didHandleMap,
1317 FilteringByOpen: isOpen,
1318 })
1319 return
1320}
1321
1322func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) {
1323 user := s.auth.GetUser(r)
1324
1325 f, err := fullyResolvedRepo(r)
1326 if err != nil {
1327 log.Println("failed to get repo and knot", err)
1328 return
1329 }
1330
1331 switch r.Method {
1332 case http.MethodGet:
1333 s.pages.RepoNewIssue(w, pages.RepoNewIssueParams{
1334 LoggedInUser: user,
1335 RepoInfo: f.RepoInfo(s, user),
1336 })
1337 case http.MethodPost:
1338 title := r.FormValue("title")
1339 body := r.FormValue("body")
1340
1341 if title == "" || body == "" {
1342 s.pages.Notice(w, "issues", "Title and body are required")
1343 return
1344 }
1345
1346 tx, err := s.db.BeginTx(r.Context(), nil)
1347 if err != nil {
1348 s.pages.Notice(w, "issues", "Failed to create issue, try again later")
1349 return
1350 }
1351
1352 err = db.NewIssue(tx, &db.Issue{
1353 RepoAt: f.RepoAt,
1354 Title: title,
1355 Body: body,
1356 OwnerDid: user.Did,
1357 })
1358 if err != nil {
1359 log.Println("failed to create issue", err)
1360 s.pages.Notice(w, "issues", "Failed to create issue.")
1361 return
1362 }
1363
1364 issueId, err := db.GetIssueId(s.db, f.RepoAt)
1365 if err != nil {
1366 log.Println("failed to get issue id", err)
1367 s.pages.Notice(w, "issues", "Failed to create issue.")
1368 return
1369 }
1370
1371 client, _ := s.auth.AuthorizedClient(r)
1372 atUri := f.RepoAt.String()
1373 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1374 Collection: tangled.RepoIssueNSID,
1375 Repo: user.Did,
1376 Rkey: s.TID(),
1377 Record: &lexutil.LexiconTypeDecoder{
1378 Val: &tangled.RepoIssue{
1379 Repo: atUri,
1380 Title: title,
1381 Body: &body,
1382 Owner: user.Did,
1383 IssueId: int64(issueId),
1384 },
1385 },
1386 })
1387 if err != nil {
1388 log.Println("failed to create issue", err)
1389 s.pages.Notice(w, "issues", "Failed to create issue.")
1390 return
1391 }
1392
1393 err = db.SetIssueAt(s.db, f.RepoAt, issueId, resp.Uri)
1394 if err != nil {
1395 log.Println("failed to set issue at", err)
1396 s.pages.Notice(w, "issues", "Failed to create issue.")
1397 return
1398 }
1399
1400 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueId))
1401 return
1402 }
1403}
1404
1405func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) {
1406 user := s.auth.GetUser(r)
1407 params := r.URL.Query()
1408
1409 state := db.PullOpen
1410 switch params.Get("state") {
1411 case "closed":
1412 state = db.PullClosed
1413 case "merged":
1414 state = db.PullMerged
1415 }
1416
1417 f, err := fullyResolvedRepo(r)
1418 if err != nil {
1419 log.Println("failed to get repo and knot", err)
1420 return
1421 }
1422
1423 pulls, err := db.GetPulls(s.db, f.RepoAt, state)
1424 if err != nil {
1425 log.Println("failed to get pulls", err)
1426 s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.")
1427 return
1428 }
1429
1430 identsToResolve := make([]string, len(pulls))
1431 for i, pull := range pulls {
1432 identsToResolve[i] = pull.OwnerDid
1433 }
1434 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
1435 didHandleMap := make(map[string]string)
1436 for _, identity := range resolvedIds {
1437 if !identity.Handle.IsInvalidHandle() {
1438 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
1439 } else {
1440 didHandleMap[identity.DID.String()] = identity.DID.String()
1441 }
1442 }
1443
1444 s.pages.RepoPulls(w, pages.RepoPullsParams{
1445 LoggedInUser: s.auth.GetUser(r),
1446 RepoInfo: f.RepoInfo(s, user),
1447 Pulls: pulls,
1448 DidHandleMap: didHandleMap,
1449 FilteringBy: state,
1450 })
1451 return
1452}
1453
1454func fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) {
1455 repoName := chi.URLParam(r, "repo")
1456 knot, ok := r.Context().Value("knot").(string)
1457 if !ok {
1458 log.Println("malformed middleware")
1459 return nil, fmt.Errorf("malformed middleware")
1460 }
1461 id, ok := r.Context().Value("resolvedId").(identity.Identity)
1462 if !ok {
1463 log.Println("malformed middleware")
1464 return nil, fmt.Errorf("malformed middleware")
1465 }
1466
1467 repoAt, ok := r.Context().Value("repoAt").(string)
1468 if !ok {
1469 log.Println("malformed middleware")
1470 return nil, fmt.Errorf("malformed middleware")
1471 }
1472
1473 parsedRepoAt, err := syntax.ParseATURI(repoAt)
1474 if err != nil {
1475 log.Println("malformed repo at-uri")
1476 return nil, fmt.Errorf("malformed middleware")
1477 }
1478
1479 // pass through values from the middleware
1480 description, ok := r.Context().Value("repoDescription").(string)
1481 addedAt, ok := r.Context().Value("repoAddedAt").(string)
1482
1483 return &FullyResolvedRepo{
1484 Knot: knot,
1485 OwnerId: id,
1486 RepoName: repoName,
1487 RepoAt: parsedRepoAt,
1488 Description: description,
1489 AddedAt: addedAt,
1490 }, nil
1491}
1492
1493func rolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) pages.RolesInRepo {
1494 if u != nil {
1495 r := s.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.OwnerSlashRepo())
1496 return pages.RolesInRepo{r}
1497 } else {
1498 return pages.RolesInRepo{}
1499 }
1500}