this repo has no description
1package state 2 3import ( 4 "context" 5 "database/sql" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "io" 10 "log" 11 mathrand "math/rand/v2" 12 "net/http" 13 "path" 14 "slices" 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/db" 22 "tangled.sh/tangled.sh/core/appview/oauth" 23 "tangled.sh/tangled.sh/core/appview/pages" 24 "tangled.sh/tangled.sh/core/appview/pages/markup" 25 "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 26 "tangled.sh/tangled.sh/core/appview/pagination" 27 "tangled.sh/tangled.sh/core/knotclient" 28 "tangled.sh/tangled.sh/core/patchutil" 29 "tangled.sh/tangled.sh/core/types" 30 31 "github.com/bluesky-social/indigo/atproto/data" 32 "github.com/bluesky-social/indigo/atproto/identity" 33 "github.com/bluesky-social/indigo/atproto/syntax" 34 securejoin "github.com/cyphar/filepath-securejoin" 35 "github.com/go-chi/chi/v5" 36 "github.com/go-git/go-git/v5/plumbing" 37 "github.com/posthog/posthog-go" 38 39 comatproto "github.com/bluesky-social/indigo/api/atproto" 40 lexutil "github.com/bluesky-social/indigo/lex/util" 41) 42 43func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) { 44 ref := chi.URLParam(r, "ref") 45 f, err := s.fullyResolvedRepo(r) 46 if err != nil { 47 log.Println("failed to fully resolve repo", err) 48 return 49 } 50 51 us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 52 if err != nil { 53 log.Printf("failed to create unsigned client for %s", f.Knot) 54 s.pages.Error503(w) 55 return 56 } 57 58 result, err := us.Index(f.OwnerDid(), f.RepoName, ref) 59 if err != nil { 60 s.pages.Error503(w) 61 log.Println("failed to reach knotserver", err) 62 return 63 } 64 65 tagMap := make(map[string][]string) 66 for _, tag := range result.Tags { 67 hash := tag.Hash 68 if tag.Tag != nil { 69 hash = tag.Tag.Target.String() 70 } 71 tagMap[hash] = append(tagMap[hash], tag.Name) 72 } 73 74 for _, branch := range result.Branches { 75 hash := branch.Hash 76 tagMap[hash] = append(tagMap[hash], branch.Name) 77 } 78 79 slices.SortFunc(result.Branches, func(a, b types.Branch) int { 80 if a.Name == result.Ref { 81 return -1 82 } 83 if a.IsDefault { 84 return -1 85 } 86 if b.IsDefault { 87 return 1 88 } 89 if a.Commit != nil { 90 if a.Commit.Committer.When.Before(b.Commit.Committer.When) { 91 return 1 92 } else { 93 return -1 94 } 95 } 96 return strings.Compare(a.Name, b.Name) * -1 97 }) 98 99 commitCount := len(result.Commits) 100 branchCount := len(result.Branches) 101 tagCount := len(result.Tags) 102 fileCount := len(result.Files) 103 104 commitCount, branchCount, tagCount = balanceIndexItems(commitCount, branchCount, tagCount, fileCount) 105 commitsTrunc := result.Commits[:min(commitCount, len(result.Commits))] 106 tagsTrunc := result.Tags[:min(tagCount, len(result.Tags))] 107 branchesTrunc := result.Branches[:min(branchCount, len(result.Branches))] 108 109 emails := uniqueEmails(commitsTrunc) 110 111 user := s.oauth.GetUser(r) 112 repoInfo := f.RepoInfo(s, user) 113 114 secret, err := db.GetRegistrationKey(s.db, f.Knot) 115 if err != nil { 116 log.Printf("failed to get registration key for %s: %s", f.Knot, err) 117 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 118 } 119 120 signedClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 121 if err != nil { 122 log.Printf("failed to create signed client for %s: %s", f.Knot, err) 123 return 124 } 125 126 var forkInfo *types.ForkInfo 127 if user != nil && (repoInfo.Roles.IsOwner() || repoInfo.Roles.IsCollaborator()) { 128 forkInfo, err = getForkInfo(repoInfo, s, f, user, signedClient) 129 if err != nil { 130 log.Printf("Failed to fetch fork information: %v", err) 131 return 132 } 133 } 134 135 repoLanguages, err := signedClient.RepoLanguages(f.OwnerDid(), f.RepoName, ref) 136 if err != nil { 137 log.Printf("failed to compute language percentages: %s", err) 138 // non-fatal 139 } 140 141 s.pages.RepoIndexPage(w, pages.RepoIndexParams{ 142 LoggedInUser: user, 143 RepoInfo: repoInfo, 144 TagMap: tagMap, 145 RepoIndexResponse: *result, 146 CommitsTrunc: commitsTrunc, 147 TagsTrunc: tagsTrunc, 148 ForkInfo: forkInfo, 149 BranchesTrunc: branchesTrunc, 150 EmailToDidOrHandle: EmailToDidOrHandle(s, emails), 151 Languages: repoLanguages, 152 }) 153 return 154} 155 156func getForkInfo( 157 repoInfo repoinfo.RepoInfo, 158 s *State, 159 f *FullyResolvedRepo, 160 user *oauth.User, 161 signedClient *knotclient.SignedClient, 162) (*types.ForkInfo, error) { 163 if user == nil { 164 return nil, nil 165 } 166 167 forkInfo := types.ForkInfo{ 168 IsFork: repoInfo.Source != nil, 169 Status: types.UpToDate, 170 } 171 172 if !forkInfo.IsFork { 173 forkInfo.IsFork = false 174 return &forkInfo, nil 175 } 176 177 us, err := knotclient.NewUnsignedClient(repoInfo.Source.Knot, s.config.Core.Dev) 178 if err != nil { 179 log.Printf("failed to create unsigned client for %s", repoInfo.Source.Knot) 180 return nil, err 181 } 182 183 result, err := us.Branches(repoInfo.Source.Did, repoInfo.Source.Name) 184 if err != nil { 185 log.Println("failed to reach knotserver", err) 186 return nil, err 187 } 188 189 if !slices.ContainsFunc(result.Branches, func(branch types.Branch) bool { 190 return branch.Name == f.Ref 191 }) { 192 forkInfo.Status = types.MissingBranch 193 return &forkInfo, nil 194 } 195 196 newHiddenRefResp, err := signedClient.NewHiddenRef(user.Did, repoInfo.Name, f.Ref, f.Ref) 197 if err != nil || newHiddenRefResp.StatusCode != http.StatusNoContent { 198 log.Printf("failed to update tracking branch: %s", err) 199 return nil, err 200 } 201 202 hiddenRef := fmt.Sprintf("hidden/%s/%s", f.Ref, f.Ref) 203 204 var status types.AncestorCheckResponse 205 forkSyncableResp, err := signedClient.RepoForkAheadBehind(user.Did, string(f.RepoAt), repoInfo.Name, f.Ref, hiddenRef) 206 if err != nil { 207 log.Printf("failed to check if fork is ahead/behind: %s", err) 208 return nil, err 209 } 210 211 if err := json.NewDecoder(forkSyncableResp.Body).Decode(&status); err != nil { 212 log.Printf("failed to decode fork status: %s", err) 213 return nil, err 214 } 215 216 forkInfo.Status = status.Status 217 return &forkInfo, nil 218} 219 220func (s *State) RepoLog(w http.ResponseWriter, r *http.Request) { 221 f, err := s.fullyResolvedRepo(r) 222 if err != nil { 223 log.Println("failed to fully resolve repo", err) 224 return 225 } 226 227 page := 1 228 if r.URL.Query().Get("page") != "" { 229 page, err = strconv.Atoi(r.URL.Query().Get("page")) 230 if err != nil { 231 page = 1 232 } 233 } 234 235 ref := chi.URLParam(r, "ref") 236 237 us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 238 if err != nil { 239 log.Println("failed to create unsigned client", err) 240 return 241 } 242 243 repolog, err := us.Log(f.OwnerDid(), f.RepoName, ref, page) 244 if err != nil { 245 log.Println("failed to reach knotserver", err) 246 return 247 } 248 249 result, err := us.Tags(f.OwnerDid(), f.RepoName) 250 if err != nil { 251 log.Println("failed to reach knotserver", err) 252 return 253 } 254 255 tagMap := make(map[string][]string) 256 for _, tag := range result.Tags { 257 hash := tag.Hash 258 if tag.Tag != nil { 259 hash = tag.Tag.Target.String() 260 } 261 tagMap[hash] = append(tagMap[hash], tag.Name) 262 } 263 264 user := s.oauth.GetUser(r) 265 s.pages.RepoLog(w, pages.RepoLogParams{ 266 LoggedInUser: user, 267 TagMap: tagMap, 268 RepoInfo: f.RepoInfo(s, user), 269 RepoLogResponse: *repolog, 270 EmailToDidOrHandle: EmailToDidOrHandle(s, uniqueEmails(repolog.Commits)), 271 }) 272 return 273} 274 275func (s *State) RepoDescriptionEdit(w http.ResponseWriter, r *http.Request) { 276 f, err := s.fullyResolvedRepo(r) 277 if err != nil { 278 log.Println("failed to get repo and knot", err) 279 w.WriteHeader(http.StatusBadRequest) 280 return 281 } 282 283 user := s.oauth.GetUser(r) 284 s.pages.EditRepoDescriptionFragment(w, pages.RepoDescriptionParams{ 285 RepoInfo: f.RepoInfo(s, user), 286 }) 287 return 288} 289 290func (s *State) RepoDescription(w http.ResponseWriter, r *http.Request) { 291 f, err := s.fullyResolvedRepo(r) 292 if err != nil { 293 log.Println("failed to get repo and knot", err) 294 w.WriteHeader(http.StatusBadRequest) 295 return 296 } 297 298 repoAt := f.RepoAt 299 rkey := repoAt.RecordKey().String() 300 if rkey == "" { 301 log.Println("invalid aturi for repo", err) 302 w.WriteHeader(http.StatusInternalServerError) 303 return 304 } 305 306 user := s.oauth.GetUser(r) 307 308 switch r.Method { 309 case http.MethodGet: 310 s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{ 311 RepoInfo: f.RepoInfo(s, user), 312 }) 313 return 314 case http.MethodPut: 315 user := s.oauth.GetUser(r) 316 newDescription := r.FormValue("description") 317 client, err := s.oauth.AuthorizedClient(r) 318 if err != nil { 319 log.Println("failed to get client") 320 s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") 321 return 322 } 323 324 // optimistic update 325 err = db.UpdateDescription(s.db, string(repoAt), newDescription) 326 if err != nil { 327 log.Println("failed to perferom update-description query", err) 328 s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") 329 return 330 } 331 332 // this is a bit of a pain because the golang atproto impl does not allow nil SwapRecord field 333 // 334 // SwapRecord is optional and should happen automagically, but given that it does not, we have to perform two requests 335 ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, user.Did, rkey) 336 if err != nil { 337 // failed to get record 338 s.pages.Notice(w, "repo-notice", "Failed to update description, no record found on PDS.") 339 return 340 } 341 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 342 Collection: tangled.RepoNSID, 343 Repo: user.Did, 344 Rkey: rkey, 345 SwapRecord: ex.Cid, 346 Record: &lexutil.LexiconTypeDecoder{ 347 Val: &tangled.Repo{ 348 Knot: f.Knot, 349 Name: f.RepoName, 350 Owner: user.Did, 351 CreatedAt: f.CreatedAt, 352 Description: &newDescription, 353 }, 354 }, 355 }) 356 357 if err != nil { 358 log.Println("failed to perferom update-description query", err) 359 // failed to get record 360 s.pages.Notice(w, "repo-notice", "Failed to update description, unable to save to PDS.") 361 return 362 } 363 364 newRepoInfo := f.RepoInfo(s, user) 365 newRepoInfo.Description = newDescription 366 367 s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{ 368 RepoInfo: newRepoInfo, 369 }) 370 return 371 } 372} 373 374func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) { 375 f, err := s.fullyResolvedRepo(r) 376 if err != nil { 377 log.Println("failed to fully resolve repo", err) 378 return 379 } 380 ref := chi.URLParam(r, "ref") 381 protocol := "http" 382 if !s.config.Core.Dev { 383 protocol = "https" 384 } 385 386 if !plumbing.IsHash(ref) { 387 s.pages.Error404(w) 388 return 389 } 390 391 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref)) 392 if err != nil { 393 log.Println("failed to reach knotserver", err) 394 return 395 } 396 397 body, err := io.ReadAll(resp.Body) 398 if err != nil { 399 log.Printf("Error reading response body: %v", err) 400 return 401 } 402 403 var result types.RepoCommitResponse 404 err = json.Unmarshal(body, &result) 405 if err != nil { 406 log.Println("failed to parse response:", err) 407 return 408 } 409 410 user := s.oauth.GetUser(r) 411 s.pages.RepoCommit(w, pages.RepoCommitParams{ 412 LoggedInUser: user, 413 RepoInfo: f.RepoInfo(s, user), 414 RepoCommitResponse: result, 415 EmailToDidOrHandle: EmailToDidOrHandle(s, []string{result.Diff.Commit.Author.Email}), 416 }) 417 return 418} 419 420func (s *State) RepoTree(w http.ResponseWriter, r *http.Request) { 421 f, err := s.fullyResolvedRepo(r) 422 if err != nil { 423 log.Println("failed to fully resolve repo", err) 424 return 425 } 426 427 ref := chi.URLParam(r, "ref") 428 treePath := chi.URLParam(r, "*") 429 protocol := "http" 430 if !s.config.Core.Dev { 431 protocol = "https" 432 } 433 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath)) 434 if err != nil { 435 log.Println("failed to reach knotserver", err) 436 return 437 } 438 439 body, err := io.ReadAll(resp.Body) 440 if err != nil { 441 log.Printf("Error reading response body: %v", err) 442 return 443 } 444 445 var result types.RepoTreeResponse 446 err = json.Unmarshal(body, &result) 447 if err != nil { 448 log.Println("failed to parse response:", err) 449 return 450 } 451 452 // redirects tree paths trying to access a blob; in this case the result.Files is unpopulated, 453 // so we can safely redirect to the "parent" (which is the same file). 454 if len(result.Files) == 0 && result.Parent == treePath { 455 http.Redirect(w, r, fmt.Sprintf("/%s/blob/%s/%s", f.OwnerSlashRepo(), ref, result.Parent), http.StatusFound) 456 return 457 } 458 459 user := s.oauth.GetUser(r) 460 461 var breadcrumbs [][]string 462 breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) 463 if treePath != "" { 464 for idx, elem := range strings.Split(treePath, "/") { 465 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)}) 466 } 467 } 468 469 baseTreeLink := path.Join(f.OwnerSlashRepo(), "tree", ref, treePath) 470 baseBlobLink := path.Join(f.OwnerSlashRepo(), "blob", ref, treePath) 471 472 s.pages.RepoTree(w, pages.RepoTreeParams{ 473 LoggedInUser: user, 474 BreadCrumbs: breadcrumbs, 475 BaseTreeLink: baseTreeLink, 476 BaseBlobLink: baseBlobLink, 477 RepoInfo: f.RepoInfo(s, user), 478 RepoTreeResponse: result, 479 }) 480 return 481} 482 483func (s *State) RepoTags(w http.ResponseWriter, r *http.Request) { 484 f, err := s.fullyResolvedRepo(r) 485 if err != nil { 486 log.Println("failed to get repo and knot", err) 487 return 488 } 489 490 us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 491 if err != nil { 492 log.Println("failed to create unsigned client", err) 493 return 494 } 495 496 result, err := us.Tags(f.OwnerDid(), f.RepoName) 497 if err != nil { 498 log.Println("failed to reach knotserver", err) 499 return 500 } 501 502 artifacts, err := db.GetArtifact(s.db, db.FilterEq("repo_at", f.RepoAt)) 503 if err != nil { 504 log.Println("failed grab artifacts", err) 505 return 506 } 507 508 // convert artifacts to map for easy UI building 509 artifactMap := make(map[plumbing.Hash][]db.Artifact) 510 for _, a := range artifacts { 511 artifactMap[a.Tag] = append(artifactMap[a.Tag], a) 512 } 513 514 var danglingArtifacts []db.Artifact 515 for _, a := range artifacts { 516 found := false 517 for _, t := range result.Tags { 518 if t.Tag != nil { 519 if t.Tag.Hash == a.Tag { 520 found = true 521 } 522 } 523 } 524 525 if !found { 526 danglingArtifacts = append(danglingArtifacts, a) 527 } 528 } 529 530 user := s.oauth.GetUser(r) 531 s.pages.RepoTags(w, pages.RepoTagsParams{ 532 LoggedInUser: user, 533 RepoInfo: f.RepoInfo(s, user), 534 RepoTagsResponse: *result, 535 ArtifactMap: artifactMap, 536 DanglingArtifacts: danglingArtifacts, 537 }) 538 return 539} 540 541func (s *State) RepoBranches(w http.ResponseWriter, r *http.Request) { 542 f, err := s.fullyResolvedRepo(r) 543 if err != nil { 544 log.Println("failed to get repo and knot", err) 545 return 546 } 547 548 us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 549 if err != nil { 550 log.Println("failed to create unsigned client", err) 551 return 552 } 553 554 result, err := us.Branches(f.OwnerDid(), f.RepoName) 555 if err != nil { 556 log.Println("failed to reach knotserver", err) 557 return 558 } 559 560 slices.SortFunc(result.Branches, func(a, b types.Branch) int { 561 if a.IsDefault { 562 return -1 563 } 564 if b.IsDefault { 565 return 1 566 } 567 if a.Commit != nil { 568 if a.Commit.Committer.When.Before(b.Commit.Committer.When) { 569 return 1 570 } else { 571 return -1 572 } 573 } 574 return strings.Compare(a.Name, b.Name) * -1 575 }) 576 577 user := s.oauth.GetUser(r) 578 s.pages.RepoBranches(w, pages.RepoBranchesParams{ 579 LoggedInUser: user, 580 RepoInfo: f.RepoInfo(s, user), 581 RepoBranchesResponse: *result, 582 }) 583 return 584} 585 586func (s *State) RepoBlob(w http.ResponseWriter, r *http.Request) { 587 f, err := s.fullyResolvedRepo(r) 588 if err != nil { 589 log.Println("failed to get repo and knot", err) 590 return 591 } 592 593 ref := chi.URLParam(r, "ref") 594 filePath := chi.URLParam(r, "*") 595 protocol := "http" 596 if !s.config.Core.Dev { 597 protocol = "https" 598 } 599 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) 600 if err != nil { 601 log.Println("failed to reach knotserver", err) 602 return 603 } 604 605 body, err := io.ReadAll(resp.Body) 606 if err != nil { 607 log.Printf("Error reading response body: %v", err) 608 return 609 } 610 611 var result types.RepoBlobResponse 612 err = json.Unmarshal(body, &result) 613 if err != nil { 614 log.Println("failed to parse response:", err) 615 return 616 } 617 618 var breadcrumbs [][]string 619 breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) 620 if filePath != "" { 621 for idx, elem := range strings.Split(filePath, "/") { 622 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)}) 623 } 624 } 625 626 showRendered := false 627 renderToggle := false 628 629 if markup.GetFormat(result.Path) == markup.FormatMarkdown { 630 renderToggle = true 631 showRendered = r.URL.Query().Get("code") != "true" 632 } 633 634 user := s.oauth.GetUser(r) 635 s.pages.RepoBlob(w, pages.RepoBlobParams{ 636 LoggedInUser: user, 637 RepoInfo: f.RepoInfo(s, user), 638 RepoBlobResponse: result, 639 BreadCrumbs: breadcrumbs, 640 ShowRendered: showRendered, 641 RenderToggle: renderToggle, 642 }) 643 return 644} 645 646func (s *State) RepoBlobRaw(w http.ResponseWriter, r *http.Request) { 647 f, err := s.fullyResolvedRepo(r) 648 if err != nil { 649 log.Println("failed to get repo and knot", err) 650 return 651 } 652 653 ref := chi.URLParam(r, "ref") 654 filePath := chi.URLParam(r, "*") 655 656 protocol := "http" 657 if !s.config.Core.Dev { 658 protocol = "https" 659 } 660 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) 661 if err != nil { 662 log.Println("failed to reach knotserver", err) 663 return 664 } 665 666 body, err := io.ReadAll(resp.Body) 667 if err != nil { 668 log.Printf("Error reading response body: %v", err) 669 return 670 } 671 672 var result types.RepoBlobResponse 673 err = json.Unmarshal(body, &result) 674 if err != nil { 675 log.Println("failed to parse response:", err) 676 return 677 } 678 679 if result.IsBinary { 680 w.Header().Set("Content-Type", "application/octet-stream") 681 w.Write(body) 682 return 683 } 684 685 w.Header().Set("Content-Type", "text/plain") 686 w.Write([]byte(result.Contents)) 687 return 688} 689 690func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) { 691 f, err := s.fullyResolvedRepo(r) 692 if err != nil { 693 log.Println("failed to get repo and knot", err) 694 return 695 } 696 697 collaborator := r.FormValue("collaborator") 698 if collaborator == "" { 699 http.Error(w, "malformed form", http.StatusBadRequest) 700 return 701 } 702 703 collaboratorIdent, err := s.resolver.ResolveIdent(r.Context(), collaborator) 704 if err != nil { 705 w.Write([]byte("failed to resolve collaborator did to a handle")) 706 return 707 } 708 log.Printf("adding %s to %s\n", collaboratorIdent.Handle.String(), f.Knot) 709 710 // TODO: create an atproto record for this 711 712 secret, err := db.GetRegistrationKey(s.db, f.Knot) 713 if err != nil { 714 log.Printf("no key found for domain %s: %s\n", f.Knot, err) 715 return 716 } 717 718 ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 719 if err != nil { 720 log.Println("failed to create client to ", f.Knot) 721 return 722 } 723 724 ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String()) 725 if err != nil { 726 log.Printf("failed to make request to %s: %s", f.Knot, err) 727 return 728 } 729 730 if ksResp.StatusCode != http.StatusNoContent { 731 w.Write([]byte(fmt.Sprint("knotserver failed to add collaborator: ", err))) 732 return 733 } 734 735 tx, err := s.db.BeginTx(r.Context(), nil) 736 if err != nil { 737 log.Println("failed to start tx") 738 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 739 return 740 } 741 defer func() { 742 tx.Rollback() 743 err = s.enforcer.E.LoadPolicy() 744 if err != nil { 745 log.Println("failed to rollback policies") 746 } 747 }() 748 749 err = s.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo()) 750 if err != nil { 751 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 752 return 753 } 754 755 err = db.AddCollaborator(s.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot) 756 if err != nil { 757 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 758 return 759 } 760 761 err = tx.Commit() 762 if err != nil { 763 log.Println("failed to commit changes", err) 764 http.Error(w, err.Error(), http.StatusInternalServerError) 765 return 766 } 767 768 err = s.enforcer.E.SavePolicy() 769 if err != nil { 770 log.Println("failed to update ACLs", err) 771 http.Error(w, err.Error(), http.StatusInternalServerError) 772 return 773 } 774 775 w.Write([]byte(fmt.Sprint("added collaborator: ", collaboratorIdent.Handle.String()))) 776 777} 778 779func (s *State) DeleteRepo(w http.ResponseWriter, r *http.Request) { 780 user := s.oauth.GetUser(r) 781 782 f, err := s.fullyResolvedRepo(r) 783 if err != nil { 784 log.Println("failed to get repo and knot", err) 785 return 786 } 787 788 // remove record from pds 789 xrpcClient, err := s.oauth.AuthorizedClient(r) 790 if err != nil { 791 log.Println("failed to get authorized client", err) 792 return 793 } 794 repoRkey := f.RepoAt.RecordKey().String() 795 _, err = xrpcClient.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 796 Collection: tangled.RepoNSID, 797 Repo: user.Did, 798 Rkey: repoRkey, 799 }) 800 if err != nil { 801 log.Printf("failed to delete record: %s", err) 802 s.pages.Notice(w, "settings-delete", "Failed to delete repository from PDS.") 803 return 804 } 805 log.Println("removed repo record ", f.RepoAt.String()) 806 807 secret, err := db.GetRegistrationKey(s.db, f.Knot) 808 if err != nil { 809 log.Printf("no key found for domain %s: %s\n", f.Knot, err) 810 return 811 } 812 813 ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 814 if err != nil { 815 log.Println("failed to create client to ", f.Knot) 816 return 817 } 818 819 ksResp, err := ksClient.RemoveRepo(f.OwnerDid(), f.RepoName) 820 if err != nil { 821 log.Printf("failed to make request to %s: %s", f.Knot, err) 822 return 823 } 824 825 if ksResp.StatusCode != http.StatusNoContent { 826 log.Println("failed to remove repo from knot, continuing anyway ", f.Knot) 827 } else { 828 log.Println("removed repo from knot ", f.Knot) 829 } 830 831 tx, err := s.db.BeginTx(r.Context(), nil) 832 if err != nil { 833 log.Println("failed to start tx") 834 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 835 return 836 } 837 defer func() { 838 tx.Rollback() 839 err = s.enforcer.E.LoadPolicy() 840 if err != nil { 841 log.Println("failed to rollback policies") 842 } 843 }() 844 845 // remove collaborator RBAC 846 repoCollaborators, err := s.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot) 847 if err != nil { 848 s.pages.Notice(w, "settings-delete", "Failed to remove collaborators") 849 return 850 } 851 for _, c := range repoCollaborators { 852 did := c[0] 853 s.enforcer.RemoveCollaborator(did, f.Knot, f.DidSlashRepo()) 854 } 855 log.Println("removed collaborators") 856 857 // remove repo RBAC 858 err = s.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.DidSlashRepo()) 859 if err != nil { 860 s.pages.Notice(w, "settings-delete", "Failed to update RBAC rules") 861 return 862 } 863 864 // remove repo from db 865 err = db.RemoveRepo(tx, f.OwnerDid(), f.RepoName) 866 if err != nil { 867 s.pages.Notice(w, "settings-delete", "Failed to update appview") 868 return 869 } 870 log.Println("removed repo from db") 871 872 err = tx.Commit() 873 if err != nil { 874 log.Println("failed to commit changes", err) 875 http.Error(w, err.Error(), http.StatusInternalServerError) 876 return 877 } 878 879 err = s.enforcer.E.SavePolicy() 880 if err != nil { 881 log.Println("failed to update ACLs", err) 882 http.Error(w, err.Error(), http.StatusInternalServerError) 883 return 884 } 885 886 s.pages.HxRedirect(w, fmt.Sprintf("/%s", f.OwnerDid())) 887} 888 889func (s *State) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { 890 f, err := s.fullyResolvedRepo(r) 891 if err != nil { 892 log.Println("failed to get repo and knot", err) 893 return 894 } 895 896 branch := r.FormValue("branch") 897 if branch == "" { 898 http.Error(w, "malformed form", http.StatusBadRequest) 899 return 900 } 901 902 secret, err := db.GetRegistrationKey(s.db, f.Knot) 903 if err != nil { 904 log.Printf("no key found for domain %s: %s\n", f.Knot, err) 905 return 906 } 907 908 ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 909 if err != nil { 910 log.Println("failed to create client to ", f.Knot) 911 return 912 } 913 914 ksResp, err := ksClient.SetDefaultBranch(f.OwnerDid(), f.RepoName, branch) 915 if err != nil { 916 log.Printf("failed to make request to %s: %s", f.Knot, err) 917 return 918 } 919 920 if ksResp.StatusCode != http.StatusNoContent { 921 s.pages.Notice(w, "repo-settings", "Failed to set default branch. Try again later.") 922 return 923 } 924 925 w.Write([]byte(fmt.Sprint("default branch set to: ", branch))) 926} 927 928func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) { 929 f, err := s.fullyResolvedRepo(r) 930 if err != nil { 931 log.Println("failed to get repo and knot", err) 932 return 933 } 934 935 switch r.Method { 936 case http.MethodGet: 937 // for now, this is just pubkeys 938 user := s.oauth.GetUser(r) 939 repoCollaborators, err := f.Collaborators(r.Context(), s) 940 if err != nil { 941 log.Println("failed to get collaborators", err) 942 } 943 944 isCollaboratorInviteAllowed := false 945 if user != nil { 946 ok, err := s.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.DidSlashRepo()) 947 if err == nil && ok { 948 isCollaboratorInviteAllowed = true 949 } 950 } 951 952 us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 953 if err != nil { 954 log.Println("failed to create unsigned client", err) 955 return 956 } 957 958 result, err := us.Branches(f.OwnerDid(), f.RepoName) 959 if err != nil { 960 log.Println("failed to reach knotserver", err) 961 return 962 } 963 964 s.pages.RepoSettings(w, pages.RepoSettingsParams{ 965 LoggedInUser: user, 966 RepoInfo: f.RepoInfo(s, user), 967 Collaborators: repoCollaborators, 968 IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed, 969 Branches: result.Branches, 970 }) 971 } 972} 973 974type FullyResolvedRepo struct { 975 Knot string 976 OwnerId identity.Identity 977 RepoName string 978 RepoAt syntax.ATURI 979 Description string 980 CreatedAt string 981 Ref string 982 CurrentDir string 983} 984 985func (f *FullyResolvedRepo) OwnerDid() string { 986 return f.OwnerId.DID.String() 987} 988 989func (f *FullyResolvedRepo) OwnerHandle() string { 990 return f.OwnerId.Handle.String() 991} 992 993func (f *FullyResolvedRepo) OwnerSlashRepo() string { 994 handle := f.OwnerId.Handle 995 996 var p string 997 if handle != "" && !handle.IsInvalidHandle() { 998 p, _ = securejoin.SecureJoin(fmt.Sprintf("@%s", handle), f.RepoName) 999 } else { 1000 p, _ = securejoin.SecureJoin(f.OwnerDid(), f.RepoName) 1001 } 1002 1003 return p 1004} 1005 1006func (f *FullyResolvedRepo) DidSlashRepo() string { 1007 p, _ := securejoin.SecureJoin(f.OwnerDid(), f.RepoName) 1008 return p 1009} 1010 1011func (f *FullyResolvedRepo) Collaborators(ctx context.Context, s *State) ([]pages.Collaborator, error) { 1012 repoCollaborators, err := s.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot) 1013 if err != nil { 1014 return nil, err 1015 } 1016 1017 var collaborators []pages.Collaborator 1018 for _, item := range repoCollaborators { 1019 // currently only two roles: owner and member 1020 var role string 1021 if item[3] == "repo:owner" { 1022 role = "owner" 1023 } else if item[3] == "repo:collaborator" { 1024 role = "collaborator" 1025 } else { 1026 continue 1027 } 1028 1029 did := item[0] 1030 1031 c := pages.Collaborator{ 1032 Did: did, 1033 Handle: "", 1034 Role: role, 1035 } 1036 collaborators = append(collaborators, c) 1037 } 1038 1039 // populate all collborators with handles 1040 identsToResolve := make([]string, len(collaborators)) 1041 for i, collab := range collaborators { 1042 identsToResolve[i] = collab.Did 1043 } 1044 1045 resolvedIdents := s.resolver.ResolveIdents(ctx, identsToResolve) 1046 for i, resolved := range resolvedIdents { 1047 if resolved != nil { 1048 collaborators[i].Handle = resolved.Handle.String() 1049 } 1050 } 1051 1052 return collaborators, nil 1053} 1054 1055func (f *FullyResolvedRepo) RepoInfo(s *State, u *oauth.User) repoinfo.RepoInfo { 1056 isStarred := false 1057 if u != nil { 1058 isStarred = db.GetStarStatus(s.db, u.Did, syntax.ATURI(f.RepoAt)) 1059 } 1060 1061 starCount, err := db.GetStarCount(s.db, f.RepoAt) 1062 if err != nil { 1063 log.Println("failed to get star count for ", f.RepoAt) 1064 } 1065 issueCount, err := db.GetIssueCount(s.db, f.RepoAt) 1066 if err != nil { 1067 log.Println("failed to get issue count for ", f.RepoAt) 1068 } 1069 pullCount, err := db.GetPullCount(s.db, f.RepoAt) 1070 if err != nil { 1071 log.Println("failed to get issue count for ", f.RepoAt) 1072 } 1073 source, err := db.GetRepoSource(s.db, f.RepoAt) 1074 if errors.Is(err, sql.ErrNoRows) { 1075 source = "" 1076 } else if err != nil { 1077 log.Println("failed to get repo source for ", f.RepoAt, err) 1078 } 1079 1080 var sourceRepo *db.Repo 1081 if source != "" { 1082 sourceRepo, err = db.GetRepoByAtUri(s.db, source) 1083 if err != nil { 1084 log.Println("failed to get repo by at uri", err) 1085 } 1086 } 1087 1088 var sourceHandle *identity.Identity 1089 if sourceRepo != nil { 1090 sourceHandle, err = s.resolver.ResolveIdent(context.Background(), sourceRepo.Did) 1091 if err != nil { 1092 log.Println("failed to resolve source repo", err) 1093 } 1094 } 1095 1096 knot := f.Knot 1097 var disableFork bool 1098 us, err := knotclient.NewUnsignedClient(knot, s.config.Core.Dev) 1099 if err != nil { 1100 log.Printf("failed to create unsigned client for %s: %v", knot, err) 1101 } else { 1102 result, err := us.Branches(f.OwnerDid(), f.RepoName) 1103 if err != nil { 1104 log.Printf("failed to get branches for %s/%s: %v", f.OwnerDid(), f.RepoName, err) 1105 } 1106 1107 if len(result.Branches) == 0 { 1108 disableFork = true 1109 } 1110 } 1111 1112 repoInfo := repoinfo.RepoInfo{ 1113 OwnerDid: f.OwnerDid(), 1114 OwnerHandle: f.OwnerHandle(), 1115 Name: f.RepoName, 1116 RepoAt: f.RepoAt, 1117 Description: f.Description, 1118 Ref: f.Ref, 1119 IsStarred: isStarred, 1120 Knot: knot, 1121 Roles: RolesInRepo(s, u, f), 1122 Stats: db.RepoStats{ 1123 StarCount: starCount, 1124 IssueCount: issueCount, 1125 PullCount: pullCount, 1126 }, 1127 DisableFork: disableFork, 1128 CurrentDir: f.CurrentDir, 1129 } 1130 1131 if sourceRepo != nil { 1132 repoInfo.Source = sourceRepo 1133 repoInfo.SourceHandle = sourceHandle.Handle.String() 1134 } 1135 1136 return repoInfo 1137} 1138 1139func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { 1140 user := s.oauth.GetUser(r) 1141 f, err := s.fullyResolvedRepo(r) 1142 if err != nil { 1143 log.Println("failed to get repo and knot", err) 1144 return 1145 } 1146 1147 issueId := chi.URLParam(r, "issue") 1148 issueIdInt, err := strconv.Atoi(issueId) 1149 if err != nil { 1150 http.Error(w, "bad issue id", http.StatusBadRequest) 1151 log.Println("failed to parse issue id", err) 1152 return 1153 } 1154 1155 issue, comments, err := db.GetIssueWithComments(s.db, f.RepoAt, issueIdInt) 1156 if err != nil { 1157 log.Println("failed to get issue and comments", err) 1158 s.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 1159 return 1160 } 1161 1162 issueOwnerIdent, err := s.resolver.ResolveIdent(r.Context(), issue.OwnerDid) 1163 if err != nil { 1164 log.Println("failed to resolve issue owner", err) 1165 } 1166 1167 identsToResolve := make([]string, len(comments)) 1168 for i, comment := range comments { 1169 identsToResolve[i] = comment.OwnerDid 1170 } 1171 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 1172 didHandleMap := make(map[string]string) 1173 for _, identity := range resolvedIds { 1174 if !identity.Handle.IsInvalidHandle() { 1175 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 1176 } else { 1177 didHandleMap[identity.DID.String()] = identity.DID.String() 1178 } 1179 } 1180 1181 s.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{ 1182 LoggedInUser: user, 1183 RepoInfo: f.RepoInfo(s, user), 1184 Issue: *issue, 1185 Comments: comments, 1186 1187 IssueOwnerHandle: issueOwnerIdent.Handle.String(), 1188 DidHandleMap: didHandleMap, 1189 }) 1190 1191} 1192 1193func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) { 1194 user := s.oauth.GetUser(r) 1195 f, err := s.fullyResolvedRepo(r) 1196 if err != nil { 1197 log.Println("failed to get repo and knot", err) 1198 return 1199 } 1200 1201 issueId := chi.URLParam(r, "issue") 1202 issueIdInt, err := strconv.Atoi(issueId) 1203 if err != nil { 1204 http.Error(w, "bad issue id", http.StatusBadRequest) 1205 log.Println("failed to parse issue id", err) 1206 return 1207 } 1208 1209 issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt) 1210 if err != nil { 1211 log.Println("failed to get issue", err) 1212 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 1213 return 1214 } 1215 1216 collaborators, err := f.Collaborators(r.Context(), s) 1217 if err != nil { 1218 log.Println("failed to fetch repo collaborators: %w", err) 1219 } 1220 isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool { 1221 return user.Did == collab.Did 1222 }) 1223 isIssueOwner := user.Did == issue.OwnerDid 1224 1225 // TODO: make this more granular 1226 if isIssueOwner || isCollaborator { 1227 1228 closed := tangled.RepoIssueStateClosed 1229 1230 client, err := s.oauth.AuthorizedClient(r) 1231 if err != nil { 1232 log.Println("failed to get authorized client", err) 1233 return 1234 } 1235 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1236 Collection: tangled.RepoIssueStateNSID, 1237 Repo: user.Did, 1238 Rkey: appview.TID(), 1239 Record: &lexutil.LexiconTypeDecoder{ 1240 Val: &tangled.RepoIssueState{ 1241 Issue: issue.IssueAt, 1242 State: closed, 1243 }, 1244 }, 1245 }) 1246 1247 if err != nil { 1248 log.Println("failed to update issue state", err) 1249 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 1250 return 1251 } 1252 1253 err = db.CloseIssue(s.db, f.RepoAt, issueIdInt) 1254 if err != nil { 1255 log.Println("failed to close issue", err) 1256 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 1257 return 1258 } 1259 1260 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt)) 1261 return 1262 } else { 1263 log.Println("user is not permitted to close issue") 1264 http.Error(w, "for biden", http.StatusUnauthorized) 1265 return 1266 } 1267} 1268 1269func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) { 1270 user := s.oauth.GetUser(r) 1271 f, err := s.fullyResolvedRepo(r) 1272 if err != nil { 1273 log.Println("failed to get repo and knot", err) 1274 return 1275 } 1276 1277 issueId := chi.URLParam(r, "issue") 1278 issueIdInt, err := strconv.Atoi(issueId) 1279 if err != nil { 1280 http.Error(w, "bad issue id", http.StatusBadRequest) 1281 log.Println("failed to parse issue id", err) 1282 return 1283 } 1284 1285 issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt) 1286 if err != nil { 1287 log.Println("failed to get issue", err) 1288 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 1289 return 1290 } 1291 1292 collaborators, err := f.Collaborators(r.Context(), s) 1293 if err != nil { 1294 log.Println("failed to fetch repo collaborators: %w", err) 1295 } 1296 isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool { 1297 return user.Did == collab.Did 1298 }) 1299 isIssueOwner := user.Did == issue.OwnerDid 1300 1301 if isCollaborator || isIssueOwner { 1302 err := db.ReopenIssue(s.db, f.RepoAt, issueIdInt) 1303 if err != nil { 1304 log.Println("failed to reopen issue", err) 1305 s.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.") 1306 return 1307 } 1308 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt)) 1309 return 1310 } else { 1311 log.Println("user is not the owner of the repo") 1312 http.Error(w, "forbidden", http.StatusUnauthorized) 1313 return 1314 } 1315} 1316 1317func (s *State) NewIssueComment(w http.ResponseWriter, r *http.Request) { 1318 user := s.oauth.GetUser(r) 1319 f, err := s.fullyResolvedRepo(r) 1320 if err != nil { 1321 log.Println("failed to get repo and knot", err) 1322 return 1323 } 1324 1325 issueId := chi.URLParam(r, "issue") 1326 issueIdInt, err := strconv.Atoi(issueId) 1327 if err != nil { 1328 http.Error(w, "bad issue id", http.StatusBadRequest) 1329 log.Println("failed to parse issue id", err) 1330 return 1331 } 1332 1333 switch r.Method { 1334 case http.MethodPost: 1335 body := r.FormValue("body") 1336 if body == "" { 1337 s.pages.Notice(w, "issue", "Body is required") 1338 return 1339 } 1340 1341 commentId := mathrand.IntN(1000000) 1342 rkey := appview.TID() 1343 1344 err := db.NewIssueComment(s.db, &db.Comment{ 1345 OwnerDid: user.Did, 1346 RepoAt: f.RepoAt, 1347 Issue: issueIdInt, 1348 CommentId: commentId, 1349 Body: body, 1350 Rkey: rkey, 1351 }) 1352 if err != nil { 1353 log.Println("failed to create comment", err) 1354 s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1355 return 1356 } 1357 1358 createdAt := time.Now().Format(time.RFC3339) 1359 commentIdInt64 := int64(commentId) 1360 ownerDid := user.Did 1361 issueAt, err := db.GetIssueAt(s.db, f.RepoAt, issueIdInt) 1362 if err != nil { 1363 log.Println("failed to get issue at", err) 1364 s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1365 return 1366 } 1367 1368 atUri := f.RepoAt.String() 1369 client, err := s.oauth.AuthorizedClient(r) 1370 if err != nil { 1371 log.Println("failed to get authorized client", err) 1372 s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1373 return 1374 } 1375 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1376 Collection: tangled.RepoIssueCommentNSID, 1377 Repo: user.Did, 1378 Rkey: rkey, 1379 Record: &lexutil.LexiconTypeDecoder{ 1380 Val: &tangled.RepoIssueComment{ 1381 Repo: &atUri, 1382 Issue: issueAt, 1383 CommentId: &commentIdInt64, 1384 Owner: &ownerDid, 1385 Body: body, 1386 CreatedAt: createdAt, 1387 }, 1388 }, 1389 }) 1390 if err != nil { 1391 log.Println("failed to create comment", err) 1392 s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1393 return 1394 } 1395 1396 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issueIdInt, commentId)) 1397 return 1398 } 1399} 1400 1401func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) { 1402 user := s.oauth.GetUser(r) 1403 f, err := s.fullyResolvedRepo(r) 1404 if err != nil { 1405 log.Println("failed to get repo and knot", err) 1406 return 1407 } 1408 1409 issueId := chi.URLParam(r, "issue") 1410 issueIdInt, err := strconv.Atoi(issueId) 1411 if err != nil { 1412 http.Error(w, "bad issue id", http.StatusBadRequest) 1413 log.Println("failed to parse issue id", err) 1414 return 1415 } 1416 1417 commentId := chi.URLParam(r, "comment_id") 1418 commentIdInt, err := strconv.Atoi(commentId) 1419 if err != nil { 1420 http.Error(w, "bad comment id", http.StatusBadRequest) 1421 log.Println("failed to parse issue id", err) 1422 return 1423 } 1424 1425 issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt) 1426 if err != nil { 1427 log.Println("failed to get issue", err) 1428 s.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 1429 return 1430 } 1431 1432 comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt) 1433 if err != nil { 1434 http.Error(w, "bad comment id", http.StatusBadRequest) 1435 return 1436 } 1437 1438 identity, err := s.resolver.ResolveIdent(r.Context(), comment.OwnerDid) 1439 if err != nil { 1440 log.Println("failed to resolve did") 1441 return 1442 } 1443 1444 didHandleMap := make(map[string]string) 1445 if !identity.Handle.IsInvalidHandle() { 1446 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 1447 } else { 1448 didHandleMap[identity.DID.String()] = identity.DID.String() 1449 } 1450 1451 s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ 1452 LoggedInUser: user, 1453 RepoInfo: f.RepoInfo(s, user), 1454 DidHandleMap: didHandleMap, 1455 Issue: issue, 1456 Comment: comment, 1457 }) 1458} 1459 1460func (s *State) EditIssueComment(w http.ResponseWriter, r *http.Request) { 1461 user := s.oauth.GetUser(r) 1462 f, err := s.fullyResolvedRepo(r) 1463 if err != nil { 1464 log.Println("failed to get repo and knot", err) 1465 return 1466 } 1467 1468 issueId := chi.URLParam(r, "issue") 1469 issueIdInt, err := strconv.Atoi(issueId) 1470 if err != nil { 1471 http.Error(w, "bad issue id", http.StatusBadRequest) 1472 log.Println("failed to parse issue id", err) 1473 return 1474 } 1475 1476 commentId := chi.URLParam(r, "comment_id") 1477 commentIdInt, err := strconv.Atoi(commentId) 1478 if err != nil { 1479 http.Error(w, "bad comment id", http.StatusBadRequest) 1480 log.Println("failed to parse issue id", err) 1481 return 1482 } 1483 1484 issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt) 1485 if err != nil { 1486 log.Println("failed to get issue", err) 1487 s.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 1488 return 1489 } 1490 1491 comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt) 1492 if err != nil { 1493 http.Error(w, "bad comment id", http.StatusBadRequest) 1494 return 1495 } 1496 1497 if comment.OwnerDid != user.Did { 1498 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 1499 return 1500 } 1501 1502 switch r.Method { 1503 case http.MethodGet: 1504 s.pages.EditIssueCommentFragment(w, pages.EditIssueCommentParams{ 1505 LoggedInUser: user, 1506 RepoInfo: f.RepoInfo(s, user), 1507 Issue: issue, 1508 Comment: comment, 1509 }) 1510 case http.MethodPost: 1511 // extract form value 1512 newBody := r.FormValue("body") 1513 client, err := s.oauth.AuthorizedClient(r) 1514 if err != nil { 1515 log.Println("failed to get authorized client", err) 1516 s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1517 return 1518 } 1519 rkey := comment.Rkey 1520 1521 // optimistic update 1522 edited := time.Now() 1523 err = db.EditComment(s.db, comment.RepoAt, comment.Issue, comment.CommentId, newBody) 1524 if err != nil { 1525 log.Println("failed to perferom update-description query", err) 1526 s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") 1527 return 1528 } 1529 1530 // rkey is optional, it was introduced later 1531 if comment.Rkey != "" { 1532 // update the record on pds 1533 ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoIssueCommentNSID, user.Did, rkey) 1534 if err != nil { 1535 // failed to get record 1536 log.Println(err, rkey) 1537 s.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.") 1538 return 1539 } 1540 value, _ := ex.Value.MarshalJSON() // we just did get record; it is valid json 1541 record, _ := data.UnmarshalJSON(value) 1542 1543 repoAt := record["repo"].(string) 1544 issueAt := record["issue"].(string) 1545 createdAt := record["createdAt"].(string) 1546 commentIdInt64 := int64(commentIdInt) 1547 1548 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1549 Collection: tangled.RepoIssueCommentNSID, 1550 Repo: user.Did, 1551 Rkey: rkey, 1552 SwapRecord: ex.Cid, 1553 Record: &lexutil.LexiconTypeDecoder{ 1554 Val: &tangled.RepoIssueComment{ 1555 Repo: &repoAt, 1556 Issue: issueAt, 1557 CommentId: &commentIdInt64, 1558 Owner: &comment.OwnerDid, 1559 Body: newBody, 1560 CreatedAt: createdAt, 1561 }, 1562 }, 1563 }) 1564 if err != nil { 1565 log.Println(err) 1566 } 1567 } 1568 1569 // optimistic update for htmx 1570 didHandleMap := map[string]string{ 1571 user.Did: user.Handle, 1572 } 1573 comment.Body = newBody 1574 comment.Edited = &edited 1575 1576 // return new comment body with htmx 1577 s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ 1578 LoggedInUser: user, 1579 RepoInfo: f.RepoInfo(s, user), 1580 DidHandleMap: didHandleMap, 1581 Issue: issue, 1582 Comment: comment, 1583 }) 1584 return 1585 1586 } 1587 1588} 1589 1590func (s *State) DeleteIssueComment(w http.ResponseWriter, r *http.Request) { 1591 user := s.oauth.GetUser(r) 1592 f, err := s.fullyResolvedRepo(r) 1593 if err != nil { 1594 log.Println("failed to get repo and knot", err) 1595 return 1596 } 1597 1598 issueId := chi.URLParam(r, "issue") 1599 issueIdInt, err := strconv.Atoi(issueId) 1600 if err != nil { 1601 http.Error(w, "bad issue id", http.StatusBadRequest) 1602 log.Println("failed to parse issue id", err) 1603 return 1604 } 1605 1606 issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt) 1607 if err != nil { 1608 log.Println("failed to get issue", err) 1609 s.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 1610 return 1611 } 1612 1613 commentId := chi.URLParam(r, "comment_id") 1614 commentIdInt, err := strconv.Atoi(commentId) 1615 if err != nil { 1616 http.Error(w, "bad comment id", http.StatusBadRequest) 1617 log.Println("failed to parse issue id", err) 1618 return 1619 } 1620 1621 comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt) 1622 if err != nil { 1623 http.Error(w, "bad comment id", http.StatusBadRequest) 1624 return 1625 } 1626 1627 if comment.OwnerDid != user.Did { 1628 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 1629 return 1630 } 1631 1632 if comment.Deleted != nil { 1633 http.Error(w, "comment already deleted", http.StatusBadRequest) 1634 return 1635 } 1636 1637 // optimistic deletion 1638 deleted := time.Now() 1639 err = db.DeleteComment(s.db, f.RepoAt, issueIdInt, commentIdInt) 1640 if err != nil { 1641 log.Println("failed to delete comment") 1642 s.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment") 1643 return 1644 } 1645 1646 // delete from pds 1647 if comment.Rkey != "" { 1648 client, err := s.oauth.AuthorizedClient(r) 1649 if err != nil { 1650 log.Println("failed to get authorized client", err) 1651 s.pages.Notice(w, "issue-comment", "Failed to delete comment.") 1652 return 1653 } 1654 _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 1655 Collection: tangled.GraphFollowNSID, 1656 Repo: user.Did, 1657 Rkey: comment.Rkey, 1658 }) 1659 if err != nil { 1660 log.Println(err) 1661 } 1662 } 1663 1664 // optimistic update for htmx 1665 didHandleMap := map[string]string{ 1666 user.Did: user.Handle, 1667 } 1668 comment.Body = "" 1669 comment.Deleted = &deleted 1670 1671 // htmx fragment of comment after deletion 1672 s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ 1673 LoggedInUser: user, 1674 RepoInfo: f.RepoInfo(s, user), 1675 DidHandleMap: didHandleMap, 1676 Issue: issue, 1677 Comment: comment, 1678 }) 1679 return 1680} 1681 1682func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) { 1683 params := r.URL.Query() 1684 state := params.Get("state") 1685 isOpen := true 1686 switch state { 1687 case "open": 1688 isOpen = true 1689 case "closed": 1690 isOpen = false 1691 default: 1692 isOpen = true 1693 } 1694 1695 page, ok := r.Context().Value("page").(pagination.Page) 1696 if !ok { 1697 log.Println("failed to get page") 1698 page = pagination.FirstPage() 1699 } 1700 1701 user := s.oauth.GetUser(r) 1702 f, err := s.fullyResolvedRepo(r) 1703 if err != nil { 1704 log.Println("failed to get repo and knot", err) 1705 return 1706 } 1707 1708 issues, err := db.GetIssues(s.db, f.RepoAt, isOpen, page) 1709 if err != nil { 1710 log.Println("failed to get issues", err) 1711 s.pages.Notice(w, "issues", "Failed to load issues. Try again later.") 1712 return 1713 } 1714 1715 identsToResolve := make([]string, len(issues)) 1716 for i, issue := range issues { 1717 identsToResolve[i] = issue.OwnerDid 1718 } 1719 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 1720 didHandleMap := make(map[string]string) 1721 for _, identity := range resolvedIds { 1722 if !identity.Handle.IsInvalidHandle() { 1723 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 1724 } else { 1725 didHandleMap[identity.DID.String()] = identity.DID.String() 1726 } 1727 } 1728 1729 s.pages.RepoIssues(w, pages.RepoIssuesParams{ 1730 LoggedInUser: s.oauth.GetUser(r), 1731 RepoInfo: f.RepoInfo(s, user), 1732 Issues: issues, 1733 DidHandleMap: didHandleMap, 1734 FilteringByOpen: isOpen, 1735 Page: page, 1736 }) 1737 return 1738} 1739 1740func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) { 1741 user := s.oauth.GetUser(r) 1742 1743 f, err := s.fullyResolvedRepo(r) 1744 if err != nil { 1745 log.Println("failed to get repo and knot", err) 1746 return 1747 } 1748 1749 switch r.Method { 1750 case http.MethodGet: 1751 s.pages.RepoNewIssue(w, pages.RepoNewIssueParams{ 1752 LoggedInUser: user, 1753 RepoInfo: f.RepoInfo(s, user), 1754 }) 1755 case http.MethodPost: 1756 title := r.FormValue("title") 1757 body := r.FormValue("body") 1758 1759 if title == "" || body == "" { 1760 s.pages.Notice(w, "issues", "Title and body are required") 1761 return 1762 } 1763 1764 tx, err := s.db.BeginTx(r.Context(), nil) 1765 if err != nil { 1766 s.pages.Notice(w, "issues", "Failed to create issue, try again later") 1767 return 1768 } 1769 1770 err = db.NewIssue(tx, &db.Issue{ 1771 RepoAt: f.RepoAt, 1772 Title: title, 1773 Body: body, 1774 OwnerDid: user.Did, 1775 }) 1776 if err != nil { 1777 log.Println("failed to create issue", err) 1778 s.pages.Notice(w, "issues", "Failed to create issue.") 1779 return 1780 } 1781 1782 issueId, err := db.GetIssueId(s.db, f.RepoAt) 1783 if err != nil { 1784 log.Println("failed to get issue id", err) 1785 s.pages.Notice(w, "issues", "Failed to create issue.") 1786 return 1787 } 1788 1789 client, err := s.oauth.AuthorizedClient(r) 1790 if err != nil { 1791 log.Println("failed to get authorized client", err) 1792 s.pages.Notice(w, "issues", "Failed to create issue.") 1793 return 1794 } 1795 atUri := f.RepoAt.String() 1796 resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1797 Collection: tangled.RepoIssueNSID, 1798 Repo: user.Did, 1799 Rkey: appview.TID(), 1800 Record: &lexutil.LexiconTypeDecoder{ 1801 Val: &tangled.RepoIssue{ 1802 Repo: atUri, 1803 Title: title, 1804 Body: &body, 1805 Owner: user.Did, 1806 IssueId: int64(issueId), 1807 }, 1808 }, 1809 }) 1810 if err != nil { 1811 log.Println("failed to create issue", err) 1812 s.pages.Notice(w, "issues", "Failed to create issue.") 1813 return 1814 } 1815 1816 err = db.SetIssueAt(s.db, f.RepoAt, issueId, resp.Uri) 1817 if err != nil { 1818 log.Println("failed to set issue at", err) 1819 s.pages.Notice(w, "issues", "Failed to create issue.") 1820 return 1821 } 1822 1823 if !s.config.Core.Dev { 1824 err = s.posthog.Enqueue(posthog.Capture{ 1825 DistinctId: user.Did, 1826 Event: "new_issue", 1827 Properties: posthog.Properties{"repo_at": f.RepoAt.String(), "issue_id": issueId}, 1828 }) 1829 if err != nil { 1830 log.Println("failed to enqueue posthog event:", err) 1831 } 1832 } 1833 1834 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueId)) 1835 return 1836 } 1837} 1838 1839func (s *State) SyncRepoFork(w http.ResponseWriter, r *http.Request) { 1840 user := s.oauth.GetUser(r) 1841 f, err := s.fullyResolvedRepo(r) 1842 if err != nil { 1843 log.Printf("failed to resolve source repo: %v", err) 1844 return 1845 } 1846 1847 switch r.Method { 1848 case http.MethodPost: 1849 secret, err := db.GetRegistrationKey(s.db, f.Knot) 1850 if err != nil { 1851 s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", f.Knot)) 1852 return 1853 } 1854 1855 client, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 1856 if err != nil { 1857 s.pages.Notice(w, "repo", "Failed to reach knot server.") 1858 return 1859 } 1860 1861 var uri string 1862 if s.config.Core.Dev { 1863 uri = "http" 1864 } else { 1865 uri = "https" 1866 } 1867 forkName := fmt.Sprintf("%s", f.RepoName) 1868 forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName) 1869 1870 _, err = client.SyncRepoFork(user.Did, forkSourceUrl, forkName, f.Ref) 1871 if err != nil { 1872 s.pages.Notice(w, "repo", "Failed to sync repository fork.") 1873 return 1874 } 1875 1876 s.pages.HxRefresh(w) 1877 return 1878 } 1879} 1880 1881func (s *State) ForkRepo(w http.ResponseWriter, r *http.Request) { 1882 user := s.oauth.GetUser(r) 1883 f, err := s.fullyResolvedRepo(r) 1884 if err != nil { 1885 log.Printf("failed to resolve source repo: %v", err) 1886 return 1887 } 1888 1889 switch r.Method { 1890 case http.MethodGet: 1891 user := s.oauth.GetUser(r) 1892 knots, err := s.enforcer.GetDomainsForUser(user.Did) 1893 if err != nil { 1894 s.pages.Notice(w, "repo", "Invalid user account.") 1895 return 1896 } 1897 1898 s.pages.ForkRepo(w, pages.ForkRepoParams{ 1899 LoggedInUser: user, 1900 Knots: knots, 1901 RepoInfo: f.RepoInfo(s, user), 1902 }) 1903 1904 case http.MethodPost: 1905 1906 knot := r.FormValue("knot") 1907 if knot == "" { 1908 s.pages.Notice(w, "repo", "Invalid form submission—missing knot domain.") 1909 return 1910 } 1911 1912 ok, err := s.enforcer.E.Enforce(user.Did, knot, knot, "repo:create") 1913 if err != nil || !ok { 1914 s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") 1915 return 1916 } 1917 1918 forkName := fmt.Sprintf("%s", f.RepoName) 1919 1920 // this check is *only* to see if the forked repo name already exists 1921 // in the user's account. 1922 existingRepo, err := db.GetRepo(s.db, user.Did, f.RepoName) 1923 if err != nil { 1924 if errors.Is(err, sql.ErrNoRows) { 1925 // no existing repo with this name found, we can use the name as is 1926 } else { 1927 log.Println("error fetching existing repo from db", err) 1928 s.pages.Notice(w, "repo", "Failed to fork this repository. Try again later.") 1929 return 1930 } 1931 } else if existingRepo != nil { 1932 // repo with this name already exists, append random string 1933 forkName = fmt.Sprintf("%s-%s", forkName, randomString(3)) 1934 } 1935 secret, err := db.GetRegistrationKey(s.db, knot) 1936 if err != nil { 1937 s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", knot)) 1938 return 1939 } 1940 1941 client, err := knotclient.NewSignedClient(knot, secret, s.config.Core.Dev) 1942 if err != nil { 1943 s.pages.Notice(w, "repo", "Failed to reach knot server.") 1944 return 1945 } 1946 1947 var uri string 1948 if s.config.Core.Dev { 1949 uri = "http" 1950 } else { 1951 uri = "https" 1952 } 1953 forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName) 1954 sourceAt := f.RepoAt.String() 1955 1956 rkey := appview.TID() 1957 repo := &db.Repo{ 1958 Did: user.Did, 1959 Name: forkName, 1960 Knot: knot, 1961 Rkey: rkey, 1962 Source: sourceAt, 1963 } 1964 1965 tx, err := s.db.BeginTx(r.Context(), nil) 1966 if err != nil { 1967 log.Println(err) 1968 s.pages.Notice(w, "repo", "Failed to save repository information.") 1969 return 1970 } 1971 defer func() { 1972 tx.Rollback() 1973 err = s.enforcer.E.LoadPolicy() 1974 if err != nil { 1975 log.Println("failed to rollback policies") 1976 } 1977 }() 1978 1979 resp, err := client.ForkRepo(user.Did, forkSourceUrl, forkName) 1980 if err != nil { 1981 s.pages.Notice(w, "repo", "Failed to create repository on knot server.") 1982 return 1983 } 1984 1985 switch resp.StatusCode { 1986 case http.StatusConflict: 1987 s.pages.Notice(w, "repo", "A repository with that name already exists.") 1988 return 1989 case http.StatusInternalServerError: 1990 s.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.") 1991 case http.StatusNoContent: 1992 // continue 1993 } 1994 1995 xrpcClient, err := s.oauth.AuthorizedClient(r) 1996 if err != nil { 1997 log.Println("failed to get authorized client", err) 1998 s.pages.Notice(w, "repo", "Failed to create repository.") 1999 return 2000 } 2001 2002 createdAt := time.Now().Format(time.RFC3339) 2003 atresp, err := xrpcClient.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 2004 Collection: tangled.RepoNSID, 2005 Repo: user.Did, 2006 Rkey: rkey, 2007 Record: &lexutil.LexiconTypeDecoder{ 2008 Val: &tangled.Repo{ 2009 Knot: repo.Knot, 2010 Name: repo.Name, 2011 CreatedAt: createdAt, 2012 Owner: user.Did, 2013 Source: &sourceAt, 2014 }}, 2015 }) 2016 if err != nil { 2017 log.Printf("failed to create record: %s", err) 2018 s.pages.Notice(w, "repo", "Failed to announce repository creation.") 2019 return 2020 } 2021 log.Println("created repo record: ", atresp.Uri) 2022 2023 repo.AtUri = atresp.Uri 2024 err = db.AddRepo(tx, repo) 2025 if err != nil { 2026 log.Println(err) 2027 s.pages.Notice(w, "repo", "Failed to save repository information.") 2028 return 2029 } 2030 2031 // acls 2032 p, _ := securejoin.SecureJoin(user.Did, forkName) 2033 err = s.enforcer.AddRepo(user.Did, knot, p) 2034 if err != nil { 2035 log.Println(err) 2036 s.pages.Notice(w, "repo", "Failed to set up repository permissions.") 2037 return 2038 } 2039 2040 err = tx.Commit() 2041 if err != nil { 2042 log.Println("failed to commit changes", err) 2043 http.Error(w, err.Error(), http.StatusInternalServerError) 2044 return 2045 } 2046 2047 err = s.enforcer.E.SavePolicy() 2048 if err != nil { 2049 log.Println("failed to update ACLs", err) 2050 http.Error(w, err.Error(), http.StatusInternalServerError) 2051 return 2052 } 2053 2054 s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, forkName)) 2055 return 2056 } 2057} 2058 2059func (s *State) RepoCompare(w http.ResponseWriter, r *http.Request) { 2060 user := s.oauth.GetUser(r) 2061 f, err := s.fullyResolvedRepo(r) 2062 if err != nil { 2063 log.Println("failed to get repo and knot", err) 2064 return 2065 } 2066 2067 // if user is navigating to one of 2068 // /compare/{base}/{head} 2069 // /compare/{base}...{head} 2070 base := chi.URLParam(r, "base") 2071 head := chi.URLParam(r, "head") 2072 if base == "" && head == "" { 2073 rest := chi.URLParam(r, "*") // master...feature/xyz 2074 parts := strings.SplitN(rest, "...", 2) 2075 if len(parts) == 2 { 2076 base = parts[0] 2077 head = parts[1] 2078 } 2079 } 2080 2081 us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 2082 if err != nil { 2083 log.Printf("failed to create unsigned client for %s", f.Knot) 2084 s.pages.Error503(w) 2085 return 2086 } 2087 2088 branches, err := us.Branches(f.OwnerDid(), f.RepoName) 2089 if err != nil { 2090 s.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 2091 log.Println("failed to reach knotserver", err) 2092 return 2093 } 2094 2095 tags, err := us.Tags(f.OwnerDid(), f.RepoName) 2096 if err != nil { 2097 s.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 2098 log.Println("failed to reach knotserver", err) 2099 return 2100 } 2101 2102 var forks []db.Repo 2103 if user != nil { 2104 var err error 2105 forks, err = db.GetForksByDid(s.db, user.Did) 2106 if err != nil { 2107 s.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 2108 log.Println("failed to get forks", err) 2109 return 2110 } 2111 } 2112 2113 var allowPull bool = false 2114 if user != nil { 2115 if slices.ContainsFunc(branches.Branches, func(branch types.Branch) bool { 2116 return branch.Name == head || branch.Name == base 2117 }) { 2118 allowPull = true 2119 } 2120 } 2121 2122 s.pages.RepoCompare(w, pages.RepoCompareParams{ 2123 LoggedInUser: user, 2124 RepoInfo: f.RepoInfo(s, user), 2125 Forks: forks, 2126 Branches: branches.Branches, 2127 Tags: tags.Tags, 2128 Base: base, 2129 Head: head, 2130 AllowPull: allowPull, 2131 }) 2132 2133} 2134 2135func (s *State) RepoCompareAllowPullFragment(w http.ResponseWriter, r *http.Request) { 2136 user := s.oauth.GetUser(r) 2137 f, err := s.fullyResolvedRepo(r) 2138 if err != nil { 2139 log.Println("failed to get repo and knot", err) 2140 return 2141 } 2142 2143 s.pages.RepoCompareAllowPullFragment(w, pages.RepoCompareAllowPullParams{ 2144 Head: chi.URLParam(r, "head"), 2145 Base: chi.URLParam(r, "base"), 2146 RepoInfo: f.RepoInfo(s, user), 2147 LoggedInUser: user, 2148 }) 2149} 2150 2151func (s *State) RepoCompareDiffFragment(w http.ResponseWriter, r *http.Request) { 2152 f, err := s.fullyResolvedRepo(r) 2153 if err != nil { 2154 log.Println("failed to get repo and knot", err) 2155 return 2156 } 2157 user := s.oauth.GetUser(r) 2158 2159 base := chi.URLParam(r, "base") 2160 head := chi.URLParam(r, "head") 2161 2162 if base == "" || head == "" { 2163 s.pages.Notice(w, "compare-error", "Invalid ref format.") 2164 return 2165 } 2166 2167 us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 2168 if err != nil { 2169 s.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 2170 log.Println("failed to reach knotserver", err) 2171 return 2172 } 2173 2174 formatPatch, err := us.Compare(f.OwnerDid(), f.RepoName, base, head) 2175 if err != nil { 2176 s.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 2177 log.Println("failed to compare", err) 2178 return 2179 } 2180 diff := patchutil.AsNiceDiff(formatPatch.Patch, base) 2181 2182 w.Header().Add("Hx-Push-Url", fmt.Sprintf("/%s/compare/%s...%s", f.OwnerSlashRepo(), base, head)) 2183 s.pages.RepoCompareDiff(w, pages.RepoCompareDiffParams{ 2184 LoggedInUser: user, 2185 RepoInfo: f.RepoInfo(s, user), 2186 Diff: diff, 2187 }) 2188}