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