this repo has no description
1package repo
2
3import (
4 "context"
5 "database/sql"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "io"
10 "log"
11 "net/http"
12 "net/url"
13 "path"
14 "slices"
15 "sort"
16 "strconv"
17 "strings"
18 "time"
19
20 "tangled.sh/tangled.sh/core/api/tangled"
21 "tangled.sh/tangled.sh/core/appview"
22 "tangled.sh/tangled.sh/core/appview/commitverify"
23 "tangled.sh/tangled.sh/core/appview/config"
24 "tangled.sh/tangled.sh/core/appview/db"
25 "tangled.sh/tangled.sh/core/appview/idresolver"
26 "tangled.sh/tangled.sh/core/appview/oauth"
27 "tangled.sh/tangled.sh/core/appview/pages"
28 "tangled.sh/tangled.sh/core/appview/pages/markup"
29 "tangled.sh/tangled.sh/core/appview/pages/repoinfo"
30 "tangled.sh/tangled.sh/core/appview/reporesolver"
31 "tangled.sh/tangled.sh/core/eventconsumer"
32 "tangled.sh/tangled.sh/core/knotclient"
33 "tangled.sh/tangled.sh/core/patchutil"
34 "tangled.sh/tangled.sh/core/rbac"
35 "tangled.sh/tangled.sh/core/types"
36
37 securejoin "github.com/cyphar/filepath-securejoin"
38 "github.com/go-chi/chi/v5"
39 "github.com/go-git/go-git/v5/plumbing"
40 "github.com/posthog/posthog-go"
41
42 comatproto "github.com/bluesky-social/indigo/api/atproto"
43 lexutil "github.com/bluesky-social/indigo/lex/util"
44)
45
46type Repo struct {
47 repoResolver *reporesolver.RepoResolver
48 idResolver *idresolver.Resolver
49 config *config.Config
50 oauth *oauth.OAuth
51 pages *pages.Pages
52 spindlestream *eventconsumer.Consumer
53 db *db.DB
54 enforcer *rbac.Enforcer
55 posthog posthog.Client
56}
57
58func New(
59 oauth *oauth.OAuth,
60 repoResolver *reporesolver.RepoResolver,
61 pages *pages.Pages,
62 spindlestream *eventconsumer.Consumer,
63 idResolver *idresolver.Resolver,
64 db *db.DB,
65 config *config.Config,
66 posthog posthog.Client,
67 enforcer *rbac.Enforcer,
68) *Repo {
69 return &Repo{oauth: oauth,
70 repoResolver: repoResolver,
71 pages: pages,
72 idResolver: idResolver,
73 config: config,
74 spindlestream: spindlestream,
75 db: db,
76 posthog: posthog,
77 enforcer: enforcer,
78 }
79}
80
81func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) {
82 ref := chi.URLParam(r, "ref")
83 f, err := rp.repoResolver.Resolve(r)
84 if err != nil {
85 log.Println("failed to fully resolve repo", err)
86 return
87 }
88
89 us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
90 if err != nil {
91 log.Printf("failed to create unsigned client for %s", f.Knot)
92 rp.pages.Error503(w)
93 return
94 }
95
96 result, err := us.Index(f.OwnerDid(), f.RepoName, ref)
97 if err != nil {
98 rp.pages.Error503(w)
99 log.Println("failed to reach knotserver", err)
100 return
101 }
102
103 tagMap := make(map[string][]string)
104 for _, tag := range result.Tags {
105 hash := tag.Hash
106 if tag.Tag != nil {
107 hash = tag.Tag.Target.String()
108 }
109 tagMap[hash] = append(tagMap[hash], tag.Name)
110 }
111
112 for _, branch := range result.Branches {
113 hash := branch.Hash
114 tagMap[hash] = append(tagMap[hash], branch.Name)
115 }
116
117 slices.SortFunc(result.Branches, func(a, b types.Branch) int {
118 if a.Name == result.Ref {
119 return -1
120 }
121 if a.IsDefault {
122 return -1
123 }
124 if b.IsDefault {
125 return 1
126 }
127 if a.Commit != nil && b.Commit != nil {
128 if a.Commit.Committer.When.Before(b.Commit.Committer.When) {
129 return 1
130 } else {
131 return -1
132 }
133 }
134 return strings.Compare(a.Name, b.Name) * -1
135 })
136
137 commitCount := len(result.Commits)
138 branchCount := len(result.Branches)
139 tagCount := len(result.Tags)
140 fileCount := len(result.Files)
141
142 commitCount, branchCount, tagCount = balanceIndexItems(commitCount, branchCount, tagCount, fileCount)
143 commitsTrunc := result.Commits[:min(commitCount, len(result.Commits))]
144 tagsTrunc := result.Tags[:min(tagCount, len(result.Tags))]
145 branchesTrunc := result.Branches[:min(branchCount, len(result.Branches))]
146
147 emails := uniqueEmails(commitsTrunc)
148 emailToDidMap, err := db.GetEmailToDid(rp.db, emails, true)
149 if err != nil {
150 log.Println("failed to get email to did map", err)
151 }
152
153 vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, commitsTrunc)
154 if err != nil {
155 log.Println(err)
156 }
157
158 user := rp.oauth.GetUser(r)
159 repoInfo := f.RepoInfo(user)
160
161 secret, err := db.GetRegistrationKey(rp.db, f.Knot)
162 if err != nil {
163 log.Printf("failed to get registration key for %s: %s", f.Knot, err)
164 rp.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
165 }
166
167 signedClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev)
168 if err != nil {
169 log.Printf("failed to create signed client for %s: %s", f.Knot, err)
170 return
171 }
172
173 var forkInfo *types.ForkInfo
174 if user != nil && (repoInfo.Roles.IsOwner() || repoInfo.Roles.IsCollaborator()) {
175 forkInfo, err = getForkInfo(repoInfo, rp, f, user, signedClient)
176 if err != nil {
177 log.Printf("Failed to fetch fork information: %v", err)
178 return
179 }
180 }
181
182 repoLanguages, err := signedClient.RepoLanguages(f.OwnerDid(), f.RepoName, ref)
183 if err != nil {
184 log.Printf("failed to compute language percentages: %s", err)
185 // non-fatal
186 }
187
188 rp.pages.RepoIndexPage(w, pages.RepoIndexParams{
189 LoggedInUser: user,
190 RepoInfo: repoInfo,
191 TagMap: tagMap,
192 RepoIndexResponse: *result,
193 CommitsTrunc: commitsTrunc,
194 TagsTrunc: tagsTrunc,
195 ForkInfo: forkInfo,
196 BranchesTrunc: branchesTrunc,
197 EmailToDidOrHandle: emailToDidOrHandle(rp, emailToDidMap),
198 VerifiedCommits: vc,
199 Languages: repoLanguages,
200 })
201 return
202}
203
204func getForkInfo(
205 repoInfo repoinfo.RepoInfo,
206 rp *Repo,
207 f *reporesolver.ResolvedRepo,
208 user *oauth.User,
209 signedClient *knotclient.SignedClient,
210) (*types.ForkInfo, error) {
211 if user == nil {
212 return nil, nil
213 }
214
215 forkInfo := types.ForkInfo{
216 IsFork: repoInfo.Source != nil,
217 Status: types.UpToDate,
218 }
219
220 if !forkInfo.IsFork {
221 forkInfo.IsFork = false
222 return &forkInfo, nil
223 }
224
225 us, err := knotclient.NewUnsignedClient(repoInfo.Source.Knot, rp.config.Core.Dev)
226 if err != nil {
227 log.Printf("failed to create unsigned client for %s", repoInfo.Source.Knot)
228 return nil, err
229 }
230
231 result, err := us.Branches(repoInfo.Source.Did, repoInfo.Source.Name)
232 if err != nil {
233 log.Println("failed to reach knotserver", err)
234 return nil, err
235 }
236
237 if !slices.ContainsFunc(result.Branches, func(branch types.Branch) bool {
238 return branch.Name == f.Ref
239 }) {
240 forkInfo.Status = types.MissingBranch
241 return &forkInfo, nil
242 }
243
244 newHiddenRefResp, err := signedClient.NewHiddenRef(user.Did, repoInfo.Name, f.Ref, f.Ref)
245 if err != nil || newHiddenRefResp.StatusCode != http.StatusNoContent {
246 log.Printf("failed to update tracking branch: %s", err)
247 return nil, err
248 }
249
250 hiddenRef := fmt.Sprintf("hidden/%s/%s", f.Ref, f.Ref)
251
252 var status types.AncestorCheckResponse
253 forkSyncableResp, err := signedClient.RepoForkAheadBehind(user.Did, string(f.RepoAt), repoInfo.Name, f.Ref, hiddenRef)
254 if err != nil {
255 log.Printf("failed to check if fork is ahead/behind: %s", err)
256 return nil, err
257 }
258
259 if err := json.NewDecoder(forkSyncableResp.Body).Decode(&status); err != nil {
260 log.Printf("failed to decode fork status: %s", err)
261 return nil, err
262 }
263
264 forkInfo.Status = status.Status
265 return &forkInfo, nil
266}
267
268func (rp *Repo) RepoLog(w http.ResponseWriter, r *http.Request) {
269 f, err := rp.repoResolver.Resolve(r)
270 if err != nil {
271 log.Println("failed to fully resolve repo", err)
272 return
273 }
274
275 page := 1
276 if r.URL.Query().Get("page") != "" {
277 page, err = strconv.Atoi(r.URL.Query().Get("page"))
278 if err != nil {
279 page = 1
280 }
281 }
282
283 ref := chi.URLParam(r, "ref")
284
285 us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
286 if err != nil {
287 log.Println("failed to create unsigned client", err)
288 return
289 }
290
291 repolog, err := us.Log(f.OwnerDid(), f.RepoName, ref, page)
292 if err != nil {
293 log.Println("failed to reach knotserver", err)
294 return
295 }
296
297 result, err := us.Tags(f.OwnerDid(), f.RepoName)
298 if err != nil {
299 log.Println("failed to reach knotserver", err)
300 return
301 }
302
303 tagMap := make(map[string][]string)
304 for _, tag := range result.Tags {
305 hash := tag.Hash
306 if tag.Tag != nil {
307 hash = tag.Tag.Target.String()
308 }
309 tagMap[hash] = append(tagMap[hash], tag.Name)
310 }
311
312 user := rp.oauth.GetUser(r)
313
314 emailToDidMap, err := db.GetEmailToDid(rp.db, uniqueEmails(repolog.Commits), true)
315 if err != nil {
316 log.Println("failed to fetch email to did mapping", err)
317 }
318
319 vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, repolog.Commits)
320 if err != nil {
321 log.Println(err)
322 }
323
324 rp.pages.RepoLog(w, pages.RepoLogParams{
325 LoggedInUser: user,
326 TagMap: tagMap,
327 RepoInfo: f.RepoInfo(user),
328 RepoLogResponse: *repolog,
329 EmailToDidOrHandle: emailToDidOrHandle(rp, emailToDidMap),
330 VerifiedCommits: vc,
331 })
332 return
333}
334
335func (rp *Repo) RepoDescriptionEdit(w http.ResponseWriter, r *http.Request) {
336 f, err := rp.repoResolver.Resolve(r)
337 if err != nil {
338 log.Println("failed to get repo and knot", err)
339 w.WriteHeader(http.StatusBadRequest)
340 return
341 }
342
343 user := rp.oauth.GetUser(r)
344 rp.pages.EditRepoDescriptionFragment(w, pages.RepoDescriptionParams{
345 RepoInfo: f.RepoInfo(user),
346 })
347 return
348}
349
350func (rp *Repo) RepoDescription(w http.ResponseWriter, r *http.Request) {
351 f, err := rp.repoResolver.Resolve(r)
352 if err != nil {
353 log.Println("failed to get repo and knot", err)
354 w.WriteHeader(http.StatusBadRequest)
355 return
356 }
357
358 repoAt := f.RepoAt
359 rkey := repoAt.RecordKey().String()
360 if rkey == "" {
361 log.Println("invalid aturi for repo", err)
362 w.WriteHeader(http.StatusInternalServerError)
363 return
364 }
365
366 user := rp.oauth.GetUser(r)
367
368 switch r.Method {
369 case http.MethodGet:
370 rp.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{
371 RepoInfo: f.RepoInfo(user),
372 })
373 return
374 case http.MethodPut:
375 newDescription := r.FormValue("description")
376 client, err := rp.oauth.AuthorizedClient(r)
377 if err != nil {
378 log.Println("failed to get client")
379 rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
380 return
381 }
382
383 // optimistic update
384 err = db.UpdateDescription(rp.db, string(repoAt), newDescription)
385 if err != nil {
386 log.Println("failed to perferom update-description query", err)
387 rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
388 return
389 }
390
391 // this is a bit of a pain because the golang atproto impl does not allow nil SwapRecord field
392 //
393 // SwapRecord is optional and should happen automagically, but given that it does not, we have to perform two requests
394 ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, user.Did, rkey)
395 if err != nil {
396 // failed to get record
397 rp.pages.Notice(w, "repo-notice", "Failed to update description, no record found on PDS.")
398 return
399 }
400 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
401 Collection: tangled.RepoNSID,
402 Repo: user.Did,
403 Rkey: rkey,
404 SwapRecord: ex.Cid,
405 Record: &lexutil.LexiconTypeDecoder{
406 Val: &tangled.Repo{
407 Knot: f.Knot,
408 Name: f.RepoName,
409 Owner: user.Did,
410 CreatedAt: f.CreatedAt,
411 Description: &newDescription,
412 },
413 },
414 })
415
416 if err != nil {
417 log.Println("failed to perferom update-description query", err)
418 // failed to get record
419 rp.pages.Notice(w, "repo-notice", "Failed to update description, unable to save to PDS.")
420 return
421 }
422
423 newRepoInfo := f.RepoInfo(user)
424 newRepoInfo.Description = newDescription
425
426 rp.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{
427 RepoInfo: newRepoInfo,
428 })
429 return
430 }
431}
432
433func (rp *Repo) RepoCommit(w http.ResponseWriter, r *http.Request) {
434 f, err := rp.repoResolver.Resolve(r)
435 if err != nil {
436 log.Println("failed to fully resolve repo", err)
437 return
438 }
439 ref := chi.URLParam(r, "ref")
440 protocol := "http"
441 if !rp.config.Core.Dev {
442 protocol = "https"
443 }
444
445 if !plumbing.IsHash(ref) {
446 rp.pages.Error404(w)
447 return
448 }
449
450 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref))
451 if err != nil {
452 log.Println("failed to reach knotserver", err)
453 return
454 }
455
456 body, err := io.ReadAll(resp.Body)
457 if err != nil {
458 log.Printf("Error reading response body: %v", err)
459 return
460 }
461
462 var result types.RepoCommitResponse
463 err = json.Unmarshal(body, &result)
464 if err != nil {
465 log.Println("failed to parse response:", err)
466 return
467 }
468
469 emailToDidMap, err := db.GetEmailToDid(rp.db, []string{result.Diff.Commit.Committer.Email, result.Diff.Commit.Author.Email}, true)
470 if err != nil {
471 log.Println("failed to get email to did mapping:", err)
472 }
473
474 vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, []types.NiceDiff{*result.Diff})
475 if err != nil {
476 log.Println(err)
477 }
478
479 user := rp.oauth.GetUser(r)
480 rp.pages.RepoCommit(w, pages.RepoCommitParams{
481 LoggedInUser: user,
482 RepoInfo: f.RepoInfo(user),
483 RepoCommitResponse: result,
484 EmailToDidOrHandle: emailToDidOrHandle(rp, emailToDidMap),
485 VerifiedCommit: vc,
486 })
487 return
488}
489
490func (rp *Repo) RepoTree(w http.ResponseWriter, r *http.Request) {
491 f, err := rp.repoResolver.Resolve(r)
492 if err != nil {
493 log.Println("failed to fully resolve repo", err)
494 return
495 }
496
497 ref := chi.URLParam(r, "ref")
498 treePath := chi.URLParam(r, "*")
499 protocol := "http"
500 if !rp.config.Core.Dev {
501 protocol = "https"
502 }
503 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath))
504 if err != nil {
505 log.Println("failed to reach knotserver", err)
506 return
507 }
508
509 body, err := io.ReadAll(resp.Body)
510 if err != nil {
511 log.Printf("Error reading response body: %v", err)
512 return
513 }
514
515 var result types.RepoTreeResponse
516 err = json.Unmarshal(body, &result)
517 if err != nil {
518 log.Println("failed to parse response:", err)
519 return
520 }
521
522 // redirects tree paths trying to access a blob; in this case the result.Files is unpopulated,
523 // so we can safely redirect to the "parent" (which is the same file).
524 if len(result.Files) == 0 && result.Parent == treePath {
525 http.Redirect(w, r, fmt.Sprintf("/%s/blob/%s/%s", f.OwnerSlashRepo(), ref, result.Parent), http.StatusFound)
526 return
527 }
528
529 user := rp.oauth.GetUser(r)
530
531 var breadcrumbs [][]string
532 breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)})
533 if treePath != "" {
534 for idx, elem := range strings.Split(treePath, "/") {
535 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)})
536 }
537 }
538
539 baseTreeLink := path.Join(f.OwnerSlashRepo(), "tree", ref, treePath)
540 baseBlobLink := path.Join(f.OwnerSlashRepo(), "blob", ref, treePath)
541
542 rp.pages.RepoTree(w, pages.RepoTreeParams{
543 LoggedInUser: user,
544 BreadCrumbs: breadcrumbs,
545 BaseTreeLink: baseTreeLink,
546 BaseBlobLink: baseBlobLink,
547 RepoInfo: f.RepoInfo(user),
548 RepoTreeResponse: result,
549 })
550 return
551}
552
553func (rp *Repo) RepoTags(w http.ResponseWriter, r *http.Request) {
554 f, err := rp.repoResolver.Resolve(r)
555 if err != nil {
556 log.Println("failed to get repo and knot", err)
557 return
558 }
559
560 us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
561 if err != nil {
562 log.Println("failed to create unsigned client", err)
563 return
564 }
565
566 result, err := us.Tags(f.OwnerDid(), f.RepoName)
567 if err != nil {
568 log.Println("failed to reach knotserver", err)
569 return
570 }
571
572 artifacts, err := db.GetArtifact(rp.db, db.FilterEq("repo_at", f.RepoAt))
573 if err != nil {
574 log.Println("failed grab artifacts", err)
575 return
576 }
577
578 // convert artifacts to map for easy UI building
579 artifactMap := make(map[plumbing.Hash][]db.Artifact)
580 for _, a := range artifacts {
581 artifactMap[a.Tag] = append(artifactMap[a.Tag], a)
582 }
583
584 var danglingArtifacts []db.Artifact
585 for _, a := range artifacts {
586 found := false
587 for _, t := range result.Tags {
588 if t.Tag != nil {
589 if t.Tag.Hash == a.Tag {
590 found = true
591 }
592 }
593 }
594
595 if !found {
596 danglingArtifacts = append(danglingArtifacts, a)
597 }
598 }
599
600 user := rp.oauth.GetUser(r)
601 rp.pages.RepoTags(w, pages.RepoTagsParams{
602 LoggedInUser: user,
603 RepoInfo: f.RepoInfo(user),
604 RepoTagsResponse: *result,
605 ArtifactMap: artifactMap,
606 DanglingArtifacts: danglingArtifacts,
607 })
608 return
609}
610
611func (rp *Repo) RepoBranches(w http.ResponseWriter, r *http.Request) {
612 f, err := rp.repoResolver.Resolve(r)
613 if err != nil {
614 log.Println("failed to get repo and knot", err)
615 return
616 }
617
618 us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
619 if err != nil {
620 log.Println("failed to create unsigned client", err)
621 return
622 }
623
624 result, err := us.Branches(f.OwnerDid(), f.RepoName)
625 if err != nil {
626 log.Println("failed to reach knotserver", err)
627 return
628 }
629
630 slices.SortFunc(result.Branches, func(a, b types.Branch) int {
631 if a.IsDefault {
632 return -1
633 }
634 if b.IsDefault {
635 return 1
636 }
637 if a.Commit != nil && b.Commit != nil {
638 if a.Commit.Committer.When.Before(b.Commit.Committer.When) {
639 return 1
640 } else {
641 return -1
642 }
643 }
644 return strings.Compare(a.Name, b.Name) * -1
645 })
646
647 user := rp.oauth.GetUser(r)
648 rp.pages.RepoBranches(w, pages.RepoBranchesParams{
649 LoggedInUser: user,
650 RepoInfo: f.RepoInfo(user),
651 RepoBranchesResponse: *result,
652 })
653 return
654}
655
656func (rp *Repo) RepoBlob(w http.ResponseWriter, r *http.Request) {
657 f, err := rp.repoResolver.Resolve(r)
658 if err != nil {
659 log.Println("failed to get repo and knot", err)
660 return
661 }
662
663 ref := chi.URLParam(r, "ref")
664 filePath := chi.URLParam(r, "*")
665 protocol := "http"
666 if !rp.config.Core.Dev {
667 protocol = "https"
668 }
669 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath))
670 if err != nil {
671 log.Println("failed to reach knotserver", err)
672 return
673 }
674
675 body, err := io.ReadAll(resp.Body)
676 if err != nil {
677 log.Printf("Error reading response body: %v", err)
678 return
679 }
680
681 var result types.RepoBlobResponse
682 err = json.Unmarshal(body, &result)
683 if err != nil {
684 log.Println("failed to parse response:", err)
685 return
686 }
687
688 var breadcrumbs [][]string
689 breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)})
690 if filePath != "" {
691 for idx, elem := range strings.Split(filePath, "/") {
692 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)})
693 }
694 }
695
696 showRendered := false
697 renderToggle := false
698
699 if markup.GetFormat(result.Path) == markup.FormatMarkdown {
700 renderToggle = true
701 showRendered = r.URL.Query().Get("code") != "true"
702 }
703
704 user := rp.oauth.GetUser(r)
705 rp.pages.RepoBlob(w, pages.RepoBlobParams{
706 LoggedInUser: user,
707 RepoInfo: f.RepoInfo(user),
708 RepoBlobResponse: result,
709 BreadCrumbs: breadcrumbs,
710 ShowRendered: showRendered,
711 RenderToggle: renderToggle,
712 })
713 return
714}
715
716func (rp *Repo) RepoBlobRaw(w http.ResponseWriter, r *http.Request) {
717 f, err := rp.repoResolver.Resolve(r)
718 if err != nil {
719 log.Println("failed to get repo and knot", err)
720 return
721 }
722
723 ref := chi.URLParam(r, "ref")
724 filePath := chi.URLParam(r, "*")
725
726 protocol := "http"
727 if !rp.config.Core.Dev {
728 protocol = "https"
729 }
730 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath))
731 if err != nil {
732 log.Println("failed to reach knotserver", err)
733 return
734 }
735
736 body, err := io.ReadAll(resp.Body)
737 if err != nil {
738 log.Printf("Error reading response body: %v", err)
739 return
740 }
741
742 var result types.RepoBlobResponse
743 err = json.Unmarshal(body, &result)
744 if err != nil {
745 log.Println("failed to parse response:", err)
746 return
747 }
748
749 if result.IsBinary {
750 w.Header().Set("Content-Type", "application/octet-stream")
751 w.Write(body)
752 return
753 }
754
755 w.Header().Set("Content-Type", "text/plain")
756 w.Write([]byte(result.Contents))
757 return
758}
759
760// modify the spindle configured for this repo
761func (rp *Repo) EditSpindle(w http.ResponseWriter, r *http.Request) {
762 f, err := rp.repoResolver.Resolve(r)
763 if err != nil {
764 log.Println("failed to get repo and knot", err)
765 w.WriteHeader(http.StatusBadRequest)
766 return
767 }
768
769 repoAt := f.RepoAt
770 rkey := repoAt.RecordKey().String()
771 if rkey == "" {
772 log.Println("invalid aturi for repo", err)
773 w.WriteHeader(http.StatusInternalServerError)
774 return
775 }
776
777 user := rp.oauth.GetUser(r)
778
779 newSpindle := r.FormValue("spindle")
780 client, err := rp.oauth.AuthorizedClient(r)
781 if err != nil {
782 log.Println("failed to get client")
783 rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")
784 return
785 }
786
787 // ensure that this is a valid spindle for this user
788 validSpindles, err := rp.enforcer.GetSpindlesForUser(user.Did)
789 if err != nil {
790 log.Println("failed to get valid spindles")
791 rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")
792 return
793 }
794
795 if !slices.Contains(validSpindles, newSpindle) {
796 log.Println("newSpindle not present in validSpindles", "newSpindle", newSpindle, "validSpindles", validSpindles)
797 rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")
798 return
799 }
800
801 // optimistic update
802 err = db.UpdateSpindle(rp.db, string(repoAt), newSpindle)
803 if err != nil {
804 log.Println("failed to perform update-spindle query", err)
805 rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")
806 return
807 }
808
809 ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, user.Did, rkey)
810 if err != nil {
811 // failed to get record
812 rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, no record found on PDS.")
813 return
814 }
815 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
816 Collection: tangled.RepoNSID,
817 Repo: user.Did,
818 Rkey: rkey,
819 SwapRecord: ex.Cid,
820 Record: &lexutil.LexiconTypeDecoder{
821 Val: &tangled.Repo{
822 Knot: f.Knot,
823 Name: f.RepoName,
824 Owner: user.Did,
825 CreatedAt: f.CreatedAt,
826 Description: &f.Description,
827 Spindle: &newSpindle,
828 },
829 },
830 })
831
832 if err != nil {
833 log.Println("failed to perform update-spindle query", err)
834 // failed to get record
835 rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, unable to save to PDS.")
836 return
837 }
838
839 // add this spindle to spindle stream
840 rp.spindlestream.AddSource(
841 context.Background(),
842 eventconsumer.NewSpindleSource(newSpindle),
843 )
844
845 w.Write(fmt.Append(nil, "spindle set to: ", newSpindle))
846}
847
848func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) {
849 f, err := rp.repoResolver.Resolve(r)
850 if err != nil {
851 log.Println("failed to get repo and knot", err)
852 return
853 }
854
855 collaborator := r.FormValue("collaborator")
856 if collaborator == "" {
857 http.Error(w, "malformed form", http.StatusBadRequest)
858 return
859 }
860
861 collaboratorIdent, err := rp.idResolver.ResolveIdent(r.Context(), collaborator)
862 if err != nil {
863 w.Write([]byte("failed to resolve collaborator did to a handle"))
864 return
865 }
866 log.Printf("adding %s to %s\n", collaboratorIdent.Handle.String(), f.Knot)
867
868 // TODO: create an atproto record for this
869
870 secret, err := db.GetRegistrationKey(rp.db, f.Knot)
871 if err != nil {
872 log.Printf("no key found for domain %s: %s\n", f.Knot, err)
873 return
874 }
875
876 ksClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev)
877 if err != nil {
878 log.Println("failed to create client to ", f.Knot)
879 return
880 }
881
882 ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String())
883 if err != nil {
884 log.Printf("failed to make request to %s: %s", f.Knot, err)
885 return
886 }
887
888 if ksResp.StatusCode != http.StatusNoContent {
889 w.Write(fmt.Append(nil, "knotserver failed to add collaborator: ", err))
890 return
891 }
892
893 tx, err := rp.db.BeginTx(r.Context(), nil)
894 if err != nil {
895 log.Println("failed to start tx")
896 w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
897 return
898 }
899 defer func() {
900 tx.Rollback()
901 err = rp.enforcer.E.LoadPolicy()
902 if err != nil {
903 log.Println("failed to rollback policies")
904 }
905 }()
906
907 err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo())
908 if err != nil {
909 w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
910 return
911 }
912
913 err = db.AddCollaborator(rp.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot)
914 if err != nil {
915 w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
916 return
917 }
918
919 err = tx.Commit()
920 if err != nil {
921 log.Println("failed to commit changes", err)
922 http.Error(w, err.Error(), http.StatusInternalServerError)
923 return
924 }
925
926 err = rp.enforcer.E.SavePolicy()
927 if err != nil {
928 log.Println("failed to update ACLs", err)
929 http.Error(w, err.Error(), http.StatusInternalServerError)
930 return
931 }
932
933 w.Write(fmt.Append(nil, "added collaborator: ", collaboratorIdent.Handle.String()))
934
935}
936
937func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) {
938 user := rp.oauth.GetUser(r)
939
940 f, err := rp.repoResolver.Resolve(r)
941 if err != nil {
942 log.Println("failed to get repo and knot", err)
943 return
944 }
945
946 // remove record from pds
947 xrpcClient, err := rp.oauth.AuthorizedClient(r)
948 if err != nil {
949 log.Println("failed to get authorized client", err)
950 return
951 }
952 repoRkey := f.RepoAt.RecordKey().String()
953 _, err = xrpcClient.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{
954 Collection: tangled.RepoNSID,
955 Repo: user.Did,
956 Rkey: repoRkey,
957 })
958 if err != nil {
959 log.Printf("failed to delete record: %s", err)
960 rp.pages.Notice(w, "settings-delete", "Failed to delete repository from PDS.")
961 return
962 }
963 log.Println("removed repo record ", f.RepoAt.String())
964
965 secret, err := db.GetRegistrationKey(rp.db, f.Knot)
966 if err != nil {
967 log.Printf("no key found for domain %s: %s\n", f.Knot, err)
968 return
969 }
970
971 ksClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev)
972 if err != nil {
973 log.Println("failed to create client to ", f.Knot)
974 return
975 }
976
977 ksResp, err := ksClient.RemoveRepo(f.OwnerDid(), f.RepoName)
978 if err != nil {
979 log.Printf("failed to make request to %s: %s", f.Knot, err)
980 return
981 }
982
983 if ksResp.StatusCode != http.StatusNoContent {
984 log.Println("failed to remove repo from knot, continuing anyway ", f.Knot)
985 } else {
986 log.Println("removed repo from knot ", f.Knot)
987 }
988
989 tx, err := rp.db.BeginTx(r.Context(), nil)
990 if err != nil {
991 log.Println("failed to start tx")
992 w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
993 return
994 }
995 defer func() {
996 tx.Rollback()
997 err = rp.enforcer.E.LoadPolicy()
998 if err != nil {
999 log.Println("failed to rollback policies")
1000 }
1001 }()
1002
1003 // remove collaborator RBAC
1004 repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot)
1005 if err != nil {
1006 rp.pages.Notice(w, "settings-delete", "Failed to remove collaborators")
1007 return
1008 }
1009 for _, c := range repoCollaborators {
1010 did := c[0]
1011 rp.enforcer.RemoveCollaborator(did, f.Knot, f.DidSlashRepo())
1012 }
1013 log.Println("removed collaborators")
1014
1015 // remove repo RBAC
1016 err = rp.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.DidSlashRepo())
1017 if err != nil {
1018 rp.pages.Notice(w, "settings-delete", "Failed to update RBAC rules")
1019 return
1020 }
1021
1022 // remove repo from db
1023 err = db.RemoveRepo(tx, f.OwnerDid(), f.RepoName)
1024 if err != nil {
1025 rp.pages.Notice(w, "settings-delete", "Failed to update appview")
1026 return
1027 }
1028 log.Println("removed repo from db")
1029
1030 err = tx.Commit()
1031 if err != nil {
1032 log.Println("failed to commit changes", err)
1033 http.Error(w, err.Error(), http.StatusInternalServerError)
1034 return
1035 }
1036
1037 err = rp.enforcer.E.SavePolicy()
1038 if err != nil {
1039 log.Println("failed to update ACLs", err)
1040 http.Error(w, err.Error(), http.StatusInternalServerError)
1041 return
1042 }
1043
1044 rp.pages.HxRedirect(w, fmt.Sprintf("/%s", f.OwnerDid()))
1045}
1046
1047func (rp *Repo) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {
1048 f, err := rp.repoResolver.Resolve(r)
1049 if err != nil {
1050 log.Println("failed to get repo and knot", err)
1051 return
1052 }
1053
1054 branch := r.FormValue("branch")
1055 if branch == "" {
1056 http.Error(w, "malformed form", http.StatusBadRequest)
1057 return
1058 }
1059
1060 secret, err := db.GetRegistrationKey(rp.db, f.Knot)
1061 if err != nil {
1062 log.Printf("no key found for domain %s: %s\n", f.Knot, err)
1063 return
1064 }
1065
1066 ksClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev)
1067 if err != nil {
1068 log.Println("failed to create client to ", f.Knot)
1069 return
1070 }
1071
1072 ksResp, err := ksClient.SetDefaultBranch(f.OwnerDid(), f.RepoName, branch)
1073 if err != nil {
1074 log.Printf("failed to make request to %s: %s", f.Knot, err)
1075 return
1076 }
1077
1078 if ksResp.StatusCode != http.StatusNoContent {
1079 rp.pages.Notice(w, "repo-settings", "Failed to set default branch. Try again later.")
1080 return
1081 }
1082
1083 w.Write(fmt.Append(nil, "default branch set to: ", branch))
1084}
1085
1086func (rp *Repo) RepoSettings(w http.ResponseWriter, r *http.Request) {
1087 f, err := rp.repoResolver.Resolve(r)
1088 if err != nil {
1089 log.Println("failed to get repo and knot", err)
1090 return
1091 }
1092
1093 switch r.Method {
1094 case http.MethodGet:
1095 // for now, this is just pubkeys
1096 user := rp.oauth.GetUser(r)
1097 repoCollaborators, err := f.Collaborators(r.Context())
1098 if err != nil {
1099 log.Println("failed to get collaborators", err)
1100 }
1101
1102 isCollaboratorInviteAllowed := false
1103 if user != nil {
1104 ok, err := rp.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.DidSlashRepo())
1105 if err == nil && ok {
1106 isCollaboratorInviteAllowed = true
1107 }
1108 }
1109
1110 us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
1111 if err != nil {
1112 log.Println("failed to create unsigned client", err)
1113 return
1114 }
1115
1116 result, err := us.Branches(f.OwnerDid(), f.RepoName)
1117 if err != nil {
1118 log.Println("failed to reach knotserver", err)
1119 return
1120 }
1121
1122 // all spindles that this user is a member of
1123 spindles, err := rp.enforcer.GetSpindlesForUser(user.Did)
1124 if err != nil {
1125 log.Println("failed to fetch spindles", err)
1126 return
1127 }
1128
1129 rp.pages.RepoSettings(w, pages.RepoSettingsParams{
1130 LoggedInUser: user,
1131 RepoInfo: f.RepoInfo(user),
1132 Collaborators: repoCollaborators,
1133 IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed,
1134 Branches: result.Branches,
1135 Spindles: spindles,
1136 CurrentSpindle: f.Spindle,
1137 })
1138 }
1139}
1140
1141func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) {
1142 user := rp.oauth.GetUser(r)
1143 f, err := rp.repoResolver.Resolve(r)
1144 if err != nil {
1145 log.Printf("failed to resolve source repo: %v", err)
1146 return
1147 }
1148
1149 switch r.Method {
1150 case http.MethodPost:
1151 secret, err := db.GetRegistrationKey(rp.db, f.Knot)
1152 if err != nil {
1153 rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", f.Knot))
1154 return
1155 }
1156
1157 client, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev)
1158 if err != nil {
1159 rp.pages.Notice(w, "repo", "Failed to reach knot server.")
1160 return
1161 }
1162
1163 var uri string
1164 if rp.config.Core.Dev {
1165 uri = "http"
1166 } else {
1167 uri = "https"
1168 }
1169 forkName := fmt.Sprintf("%s", f.RepoName)
1170 forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName)
1171
1172 _, err = client.SyncRepoFork(user.Did, forkSourceUrl, forkName, f.Ref)
1173 if err != nil {
1174 rp.pages.Notice(w, "repo", "Failed to sync repository fork.")
1175 return
1176 }
1177
1178 rp.pages.HxRefresh(w)
1179 return
1180 }
1181}
1182
1183func (rp *Repo) ForkRepo(w http.ResponseWriter, r *http.Request) {
1184 user := rp.oauth.GetUser(r)
1185 f, err := rp.repoResolver.Resolve(r)
1186 if err != nil {
1187 log.Printf("failed to resolve source repo: %v", err)
1188 return
1189 }
1190
1191 switch r.Method {
1192 case http.MethodGet:
1193 user := rp.oauth.GetUser(r)
1194 knots, err := rp.enforcer.GetKnotsForUser(user.Did)
1195 if err != nil {
1196 rp.pages.Notice(w, "repo", "Invalid user account.")
1197 return
1198 }
1199
1200 rp.pages.ForkRepo(w, pages.ForkRepoParams{
1201 LoggedInUser: user,
1202 Knots: knots,
1203 RepoInfo: f.RepoInfo(user),
1204 })
1205
1206 case http.MethodPost:
1207
1208 knot := r.FormValue("knot")
1209 if knot == "" {
1210 rp.pages.Notice(w, "repo", "Invalid form submission—missing knot domain.")
1211 return
1212 }
1213
1214 ok, err := rp.enforcer.E.Enforce(user.Did, knot, knot, "repo:create")
1215 if err != nil || !ok {
1216 rp.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.")
1217 return
1218 }
1219
1220 forkName := fmt.Sprintf("%s", f.RepoName)
1221
1222 // this check is *only* to see if the forked repo name already exists
1223 // in the user's account.
1224 existingRepo, err := db.GetRepo(rp.db, user.Did, f.RepoName)
1225 if err != nil {
1226 if errors.Is(err, sql.ErrNoRows) {
1227 // no existing repo with this name found, we can use the name as is
1228 } else {
1229 log.Println("error fetching existing repo from db", err)
1230 rp.pages.Notice(w, "repo", "Failed to fork this repository. Try again later.")
1231 return
1232 }
1233 } else if existingRepo != nil {
1234 // repo with this name already exists, append random string
1235 forkName = fmt.Sprintf("%s-%s", forkName, randomString(3))
1236 }
1237 secret, err := db.GetRegistrationKey(rp.db, knot)
1238 if err != nil {
1239 rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", knot))
1240 return
1241 }
1242
1243 client, err := knotclient.NewSignedClient(knot, secret, rp.config.Core.Dev)
1244 if err != nil {
1245 rp.pages.Notice(w, "repo", "Failed to reach knot server.")
1246 return
1247 }
1248
1249 var uri string
1250 if rp.config.Core.Dev {
1251 uri = "http"
1252 } else {
1253 uri = "https"
1254 }
1255 forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName)
1256 sourceAt := f.RepoAt.String()
1257
1258 rkey := appview.TID()
1259 repo := &db.Repo{
1260 Did: user.Did,
1261 Name: forkName,
1262 Knot: knot,
1263 Rkey: rkey,
1264 Source: sourceAt,
1265 }
1266
1267 tx, err := rp.db.BeginTx(r.Context(), nil)
1268 if err != nil {
1269 log.Println(err)
1270 rp.pages.Notice(w, "repo", "Failed to save repository information.")
1271 return
1272 }
1273 defer func() {
1274 tx.Rollback()
1275 err = rp.enforcer.E.LoadPolicy()
1276 if err != nil {
1277 log.Println("failed to rollback policies")
1278 }
1279 }()
1280
1281 resp, err := client.ForkRepo(user.Did, forkSourceUrl, forkName)
1282 if err != nil {
1283 rp.pages.Notice(w, "repo", "Failed to create repository on knot server.")
1284 return
1285 }
1286
1287 switch resp.StatusCode {
1288 case http.StatusConflict:
1289 rp.pages.Notice(w, "repo", "A repository with that name already exists.")
1290 return
1291 case http.StatusInternalServerError:
1292 rp.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.")
1293 case http.StatusNoContent:
1294 // continue
1295 }
1296
1297 xrpcClient, err := rp.oauth.AuthorizedClient(r)
1298 if err != nil {
1299 log.Println("failed to get authorized client", err)
1300 rp.pages.Notice(w, "repo", "Failed to create repository.")
1301 return
1302 }
1303
1304 createdAt := time.Now().Format(time.RFC3339)
1305 atresp, err := xrpcClient.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
1306 Collection: tangled.RepoNSID,
1307 Repo: user.Did,
1308 Rkey: rkey,
1309 Record: &lexutil.LexiconTypeDecoder{
1310 Val: &tangled.Repo{
1311 Knot: repo.Knot,
1312 Name: repo.Name,
1313 CreatedAt: createdAt,
1314 Owner: user.Did,
1315 Source: &sourceAt,
1316 }},
1317 })
1318 if err != nil {
1319 log.Printf("failed to create record: %s", err)
1320 rp.pages.Notice(w, "repo", "Failed to announce repository creation.")
1321 return
1322 }
1323 log.Println("created repo record: ", atresp.Uri)
1324
1325 repo.AtUri = atresp.Uri
1326 err = db.AddRepo(tx, repo)
1327 if err != nil {
1328 log.Println(err)
1329 rp.pages.Notice(w, "repo", "Failed to save repository information.")
1330 return
1331 }
1332
1333 // acls
1334 p, _ := securejoin.SecureJoin(user.Did, forkName)
1335 err = rp.enforcer.AddRepo(user.Did, knot, p)
1336 if err != nil {
1337 log.Println(err)
1338 rp.pages.Notice(w, "repo", "Failed to set up repository permissions.")
1339 return
1340 }
1341
1342 err = tx.Commit()
1343 if err != nil {
1344 log.Println("failed to commit changes", err)
1345 http.Error(w, err.Error(), http.StatusInternalServerError)
1346 return
1347 }
1348
1349 err = rp.enforcer.E.SavePolicy()
1350 if err != nil {
1351 log.Println("failed to update ACLs", err)
1352 http.Error(w, err.Error(), http.StatusInternalServerError)
1353 return
1354 }
1355
1356 rp.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, forkName))
1357 return
1358 }
1359}
1360
1361func (rp *Repo) RepoCompareNew(w http.ResponseWriter, r *http.Request) {
1362 user := rp.oauth.GetUser(r)
1363 f, err := rp.repoResolver.Resolve(r)
1364 if err != nil {
1365 log.Println("failed to get repo and knot", err)
1366 return
1367 }
1368
1369 us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
1370 if err != nil {
1371 log.Printf("failed to create unsigned client for %s", f.Knot)
1372 rp.pages.Error503(w)
1373 return
1374 }
1375
1376 result, err := us.Branches(f.OwnerDid(), f.RepoName)
1377 if err != nil {
1378 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
1379 log.Println("failed to reach knotserver", err)
1380 return
1381 }
1382 branches := result.Branches
1383 sort.Slice(branches, func(i int, j int) bool {
1384 return branches[i].Commit.Committer.When.After(branches[j].Commit.Committer.When)
1385 })
1386
1387 var defaultBranch string
1388 for _, b := range branches {
1389 if b.IsDefault {
1390 defaultBranch = b.Name
1391 }
1392 }
1393
1394 base := defaultBranch
1395 head := defaultBranch
1396
1397 params := r.URL.Query()
1398 queryBase := params.Get("base")
1399 queryHead := params.Get("head")
1400 if queryBase != "" {
1401 base = queryBase
1402 }
1403 if queryHead != "" {
1404 head = queryHead
1405 }
1406
1407 tags, err := us.Tags(f.OwnerDid(), f.RepoName)
1408 if err != nil {
1409 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
1410 log.Println("failed to reach knotserver", err)
1411 return
1412 }
1413
1414 repoinfo := f.RepoInfo(user)
1415
1416 rp.pages.RepoCompareNew(w, pages.RepoCompareNewParams{
1417 LoggedInUser: user,
1418 RepoInfo: repoinfo,
1419 Branches: branches,
1420 Tags: tags.Tags,
1421 Base: base,
1422 Head: head,
1423 })
1424}
1425
1426func (rp *Repo) RepoCompare(w http.ResponseWriter, r *http.Request) {
1427 user := rp.oauth.GetUser(r)
1428 f, err := rp.repoResolver.Resolve(r)
1429 if err != nil {
1430 log.Println("failed to get repo and knot", err)
1431 return
1432 }
1433
1434 // if user is navigating to one of
1435 // /compare/{base}/{head}
1436 // /compare/{base}...{head}
1437 base := chi.URLParam(r, "base")
1438 head := chi.URLParam(r, "head")
1439 if base == "" && head == "" {
1440 rest := chi.URLParam(r, "*") // master...feature/xyz
1441 parts := strings.SplitN(rest, "...", 2)
1442 if len(parts) == 2 {
1443 base = parts[0]
1444 head = parts[1]
1445 }
1446 }
1447
1448 base, _ = url.PathUnescape(base)
1449 head, _ = url.PathUnescape(head)
1450
1451 if base == "" || head == "" {
1452 log.Printf("invalid comparison")
1453 rp.pages.Error404(w)
1454 return
1455 }
1456
1457 us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
1458 if err != nil {
1459 log.Printf("failed to create unsigned client for %s", f.Knot)
1460 rp.pages.Error503(w)
1461 return
1462 }
1463
1464 branches, err := us.Branches(f.OwnerDid(), f.RepoName)
1465 if err != nil {
1466 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
1467 log.Println("failed to reach knotserver", err)
1468 return
1469 }
1470
1471 tags, err := us.Tags(f.OwnerDid(), f.RepoName)
1472 if err != nil {
1473 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
1474 log.Println("failed to reach knotserver", err)
1475 return
1476 }
1477
1478 formatPatch, err := us.Compare(f.OwnerDid(), f.RepoName, base, head)
1479 if err != nil {
1480 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
1481 log.Println("failed to compare", err)
1482 return
1483 }
1484 diff := patchutil.AsNiceDiff(formatPatch.Patch, base)
1485
1486 repoinfo := f.RepoInfo(user)
1487
1488 rp.pages.RepoCompare(w, pages.RepoCompareParams{
1489 LoggedInUser: user,
1490 RepoInfo: repoinfo,
1491 Branches: branches.Branches,
1492 Tags: tags.Tags,
1493 Base: base,
1494 Head: head,
1495 Diff: &diff,
1496 })
1497
1498}