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