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: issue.OwnerDid, 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 if user.Did == f.OwnerDid() { 1167 err := db.ReopenIssue(s.db, f.RepoAt, issueIdInt) 1168 if err != nil { 1169 log.Println("failed to reopen issue", err) 1170 s.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.") 1171 return 1172 } 1173 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt)) 1174 return 1175 } else { 1176 log.Println("user is not the owner of the repo") 1177 http.Error(w, "forbidden", http.StatusUnauthorized) 1178 return 1179 } 1180} 1181 1182func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) { 1183 user := s.auth.GetUser(r) 1184 f, err := fullyResolvedRepo(r) 1185 if err != nil { 1186 log.Println("failed to get repo and knot", err) 1187 return 1188 } 1189 1190 issueId := chi.URLParam(r, "issue") 1191 issueIdInt, err := strconv.Atoi(issueId) 1192 if err != nil { 1193 http.Error(w, "bad issue id", http.StatusBadRequest) 1194 log.Println("failed to parse issue id", err) 1195 return 1196 } 1197 1198 switch r.Method { 1199 case http.MethodPost: 1200 body := r.FormValue("body") 1201 if body == "" { 1202 s.pages.Notice(w, "issue", "Body is required") 1203 return 1204 } 1205 1206 commentId := rand.IntN(1000000) 1207 1208 err := db.NewComment(s.db, &db.Comment{ 1209 OwnerDid: user.Did, 1210 RepoAt: f.RepoAt, 1211 Issue: issueIdInt, 1212 CommentId: commentId, 1213 Body: body, 1214 }) 1215 if err != nil { 1216 log.Println("failed to create comment", err) 1217 s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1218 return 1219 } 1220 1221 createdAt := time.Now().Format(time.RFC3339) 1222 commentIdInt64 := int64(commentId) 1223 ownerDid := user.Did 1224 issueAt, err := db.GetIssueAt(s.db, f.RepoAt, issueIdInt) 1225 if err != nil { 1226 log.Println("failed to get issue at", err) 1227 s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1228 return 1229 } 1230 1231 atUri := f.RepoAt.String() 1232 client, _ := s.auth.AuthorizedClient(r) 1233 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1234 Collection: tangled.RepoIssueCommentNSID, 1235 Repo: user.Did, 1236 Rkey: s.TID(), 1237 Record: &lexutil.LexiconTypeDecoder{ 1238 Val: &tangled.RepoIssueComment{ 1239 Repo: &atUri, 1240 Issue: issueAt, 1241 CommentId: &commentIdInt64, 1242 Owner: &ownerDid, 1243 Body: &body, 1244 CreatedAt: &createdAt, 1245 }, 1246 }, 1247 }) 1248 if err != nil { 1249 log.Println("failed to create comment", err) 1250 s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1251 return 1252 } 1253 1254 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issueIdInt, commentId)) 1255 return 1256 } 1257} 1258 1259func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) { 1260 params := r.URL.Query() 1261 state := params.Get("state") 1262 isOpen := true 1263 switch state { 1264 case "open": 1265 isOpen = true 1266 case "closed": 1267 isOpen = false 1268 default: 1269 isOpen = true 1270 } 1271 1272 user := s.auth.GetUser(r) 1273 f, err := fullyResolvedRepo(r) 1274 if err != nil { 1275 log.Println("failed to get repo and knot", err) 1276 return 1277 } 1278 1279 issues, err := db.GetIssues(s.db, f.RepoAt, isOpen) 1280 if err != nil { 1281 log.Println("failed to get issues", err) 1282 s.pages.Notice(w, "issues", "Failed to load issues. Try again later.") 1283 return 1284 } 1285 1286 identsToResolve := make([]string, len(issues)) 1287 for i, issue := range issues { 1288 identsToResolve[i] = issue.OwnerDid 1289 } 1290 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 1291 didHandleMap := make(map[string]string) 1292 for _, identity := range resolvedIds { 1293 if !identity.Handle.IsInvalidHandle() { 1294 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 1295 } else { 1296 didHandleMap[identity.DID.String()] = identity.DID.String() 1297 } 1298 } 1299 1300 s.pages.RepoIssues(w, pages.RepoIssuesParams{ 1301 LoggedInUser: s.auth.GetUser(r), 1302 RepoInfo: f.RepoInfo(s, user), 1303 Issues: issues, 1304 DidHandleMap: didHandleMap, 1305 FilteringByOpen: isOpen, 1306 }) 1307 return 1308} 1309 1310func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) { 1311 user := s.auth.GetUser(r) 1312 1313 f, err := fullyResolvedRepo(r) 1314 if err != nil { 1315 log.Println("failed to get repo and knot", err) 1316 return 1317 } 1318 1319 switch r.Method { 1320 case http.MethodGet: 1321 s.pages.RepoNewIssue(w, pages.RepoNewIssueParams{ 1322 LoggedInUser: user, 1323 RepoInfo: f.RepoInfo(s, user), 1324 }) 1325 case http.MethodPost: 1326 title := r.FormValue("title") 1327 body := r.FormValue("body") 1328 1329 if title == "" || body == "" { 1330 s.pages.Notice(w, "issues", "Title and body are required") 1331 return 1332 } 1333 1334 tx, err := s.db.BeginTx(r.Context(), nil) 1335 if err != nil { 1336 s.pages.Notice(w, "issues", "Failed to create issue, try again later") 1337 return 1338 } 1339 1340 err = db.NewIssue(tx, &db.Issue{ 1341 RepoAt: f.RepoAt, 1342 Title: title, 1343 Body: body, 1344 OwnerDid: user.Did, 1345 }) 1346 if err != nil { 1347 log.Println("failed to create issue", err) 1348 s.pages.Notice(w, "issues", "Failed to create issue.") 1349 return 1350 } 1351 1352 issueId, err := db.GetIssueId(s.db, f.RepoAt) 1353 if err != nil { 1354 log.Println("failed to get issue id", err) 1355 s.pages.Notice(w, "issues", "Failed to create issue.") 1356 return 1357 } 1358 1359 client, _ := s.auth.AuthorizedClient(r) 1360 atUri := f.RepoAt.String() 1361 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1362 Collection: tangled.RepoIssueNSID, 1363 Repo: user.Did, 1364 Rkey: s.TID(), 1365 Record: &lexutil.LexiconTypeDecoder{ 1366 Val: &tangled.RepoIssue{ 1367 Repo: atUri, 1368 Title: title, 1369 Body: &body, 1370 Owner: user.Did, 1371 IssueId: int64(issueId), 1372 }, 1373 }, 1374 }) 1375 if err != nil { 1376 log.Println("failed to create issue", err) 1377 s.pages.Notice(w, "issues", "Failed to create issue.") 1378 return 1379 } 1380 1381 err = db.SetIssueAt(s.db, f.RepoAt, issueId, resp.Uri) 1382 if err != nil { 1383 log.Println("failed to set issue at", err) 1384 s.pages.Notice(w, "issues", "Failed to create issue.") 1385 return 1386 } 1387 1388 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueId)) 1389 return 1390 } 1391} 1392 1393func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) { 1394 user := s.auth.GetUser(r) 1395 params := r.URL.Query() 1396 1397 state := db.PullOpen 1398 switch params.Get("state") { 1399 case "closed": 1400 state = db.PullClosed 1401 case "merged": 1402 state = db.PullMerged 1403 } 1404 1405 f, err := fullyResolvedRepo(r) 1406 if err != nil { 1407 log.Println("failed to get repo and knot", err) 1408 return 1409 } 1410 1411 pulls, err := db.GetPulls(s.db, f.RepoAt, state) 1412 if err != nil { 1413 log.Println("failed to get pulls", err) 1414 s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.") 1415 return 1416 } 1417 1418 identsToResolve := make([]string, len(pulls)) 1419 for i, pull := range pulls { 1420 identsToResolve[i] = pull.OwnerDid 1421 } 1422 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 1423 didHandleMap := make(map[string]string) 1424 for _, identity := range resolvedIds { 1425 if !identity.Handle.IsInvalidHandle() { 1426 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 1427 } else { 1428 didHandleMap[identity.DID.String()] = identity.DID.String() 1429 } 1430 } 1431 1432 s.pages.RepoPulls(w, pages.RepoPullsParams{ 1433 LoggedInUser: s.auth.GetUser(r), 1434 RepoInfo: f.RepoInfo(s, user), 1435 Pulls: pulls, 1436 DidHandleMap: didHandleMap, 1437 FilteringBy: state, 1438 }) 1439 return 1440} 1441 1442func (s *State) MergePull(w http.ResponseWriter, r *http.Request) { 1443 user := s.auth.GetUser(r) 1444 f, err := fullyResolvedRepo(r) 1445 if err != nil { 1446 log.Println("failed to resolve repo:", err) 1447 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1448 return 1449 } 1450 1451 // Get the pull request ID from the request URL 1452 pullId := chi.URLParam(r, "pull") 1453 pullIdInt, err := strconv.Atoi(pullId) 1454 if err != nil { 1455 log.Println("failed to parse pull ID:", err) 1456 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1457 return 1458 } 1459 1460 // Get the patch data from the request body 1461 patch := r.FormValue("patch") 1462 branch := r.FormValue("targetBranch") 1463 1464 secret, err := db.GetRegistrationKey(s.db, f.Knot) 1465 if err != nil { 1466 log.Printf("no registration key found for domain %s: %s\n", f.Knot, err) 1467 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1468 return 1469 } 1470 1471 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 1472 if err != nil { 1473 log.Printf("failed to create signed client for %s: %s", f.Knot, err) 1474 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1475 return 1476 } 1477 1478 // Merge the pull request 1479 resp, err := ksClient.Merge([]byte(patch), user.Did, f.RepoName, branch) 1480 if err != nil { 1481 log.Printf("failed to merge pull request: %s", err) 1482 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1483 return 1484 } 1485 1486 if resp.StatusCode == http.StatusOK { 1487 err := db.MergePull(s.db, f.RepoAt, pullIdInt) 1488 if err != nil { 1489 log.Printf("failed to update pull request status in database: %s", err) 1490 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1491 return 1492 } 1493 s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pullIdInt)) 1494 } else { 1495 log.Printf("knotserver returned non-OK status code for merge: %d", resp.StatusCode) 1496 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1497 } 1498} 1499 1500func (s *State) PullComment(w http.ResponseWriter, r *http.Request) { 1501 user := s.auth.GetUser(r) 1502 f, err := fullyResolvedRepo(r) 1503 if err != nil { 1504 log.Println("failed to get repo and knot", err) 1505 return 1506 } 1507 1508 pullId := chi.URLParam(r, "pull") 1509 pullIdInt, err := strconv.Atoi(pullId) 1510 if err != nil { 1511 http.Error(w, "bad pull id", http.StatusBadRequest) 1512 log.Println("failed to parse pull id", err) 1513 return 1514 } 1515 1516 switch r.Method { 1517 case http.MethodPost: 1518 body := r.FormValue("body") 1519 if body == "" { 1520 s.pages.Notice(w, "pull", "Comment body is required") 1521 return 1522 } 1523 1524 // Start a transaction 1525 tx, err := s.db.BeginTx(r.Context(), nil) 1526 if err != nil { 1527 log.Println("failed to start transaction", err) 1528 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 1529 return 1530 } 1531 defer tx.Rollback() // Will be ignored if we commit 1532 1533 commentId := rand.IntN(1000000) 1534 createdAt := time.Now().Format(time.RFC3339) 1535 commentIdInt64 := int64(commentId) 1536 ownerDid := user.Did 1537 1538 pullAt, err := db.GetPullAt(s.db, f.RepoAt, pullIdInt) 1539 if err != nil { 1540 log.Println("failed to get pull at", err) 1541 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 1542 return 1543 } 1544 1545 atUri := f.RepoAt.String() 1546 client, _ := s.auth.AuthorizedClient(r) 1547 atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1548 Collection: tangled.RepoPullCommentNSID, 1549 Repo: user.Did, 1550 Rkey: s.TID(), 1551 Record: &lexutil.LexiconTypeDecoder{ 1552 Val: &tangled.RepoPullComment{ 1553 Repo: &atUri, 1554 Pull: pullAt, 1555 CommentId: &commentIdInt64, 1556 Owner: &ownerDid, 1557 Body: &body, 1558 CreatedAt: &createdAt, 1559 }, 1560 }, 1561 }) 1562 if err != nil { 1563 log.Println("failed to create pull comment", err) 1564 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 1565 return 1566 } 1567 1568 // Create the pull comment in the database with the commentAt field 1569 err = db.NewPullComment(tx, &db.PullComment{ 1570 OwnerDid: user.Did, 1571 RepoAt: f.RepoAt.String(), 1572 CommentId: commentId, 1573 PullId: pullIdInt, 1574 Body: body, 1575 CommentAt: atResp.Uri, 1576 }) 1577 if err != nil { 1578 log.Println("failed to create pull comment", err) 1579 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 1580 return 1581 } 1582 1583 // Commit the transaction 1584 if err = tx.Commit(); err != nil { 1585 log.Println("failed to commit transaction", err) 1586 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 1587 return 1588 } 1589 1590 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", f.OwnerSlashRepo(), pullIdInt, commentId)) 1591 return 1592 } 1593} 1594 1595func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) { 1596 f, err := fullyResolvedRepo(r) 1597 if err != nil { 1598 log.Println("malformed middleware") 1599 return 1600 } 1601 1602 pullId := chi.URLParam(r, "pull") 1603 pullIdInt, err := strconv.Atoi(pullId) 1604 if err != nil { 1605 log.Println("malformed middleware") 1606 return 1607 } 1608 1609 // Start a transaction 1610 tx, err := s.db.BeginTx(r.Context(), nil) 1611 if err != nil { 1612 log.Println("failed to start transaction", err) 1613 s.pages.Notice(w, "pull-close", "Failed to close pull.") 1614 return 1615 } 1616 1617 // Close the pull in the database 1618 err = db.ClosePull(tx, f.RepoAt, pullIdInt) 1619 if err != nil { 1620 log.Println("failed to close pull", err) 1621 s.pages.Notice(w, "pull-close", "Failed to close pull.") 1622 return 1623 } 1624 1625 // Commit the transaction 1626 if err = tx.Commit(); err != nil { 1627 log.Println("failed to commit transaction", err) 1628 s.pages.Notice(w, "pull-close", "Failed to close pull.") 1629 return 1630 } 1631 1632 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullIdInt)) 1633 return 1634} 1635 1636func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) { 1637 f, err := fullyResolvedRepo(r) 1638 if err != nil { 1639 log.Println("failed to resolve repo", err) 1640 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1641 return 1642 } 1643 1644 // Start a transaction 1645 tx, err := s.db.BeginTx(r.Context(), nil) 1646 if err != nil { 1647 log.Println("failed to start transaction", err) 1648 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1649 return 1650 } 1651 1652 pullId := chi.URLParam(r, "pull") 1653 pullIdInt, err := strconv.Atoi(pullId) 1654 if err != nil { 1655 log.Println("failed to parse pull id", err) 1656 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1657 return 1658 } 1659 1660 // Reopen the pull in the database 1661 err = db.ReopenPull(tx, f.RepoAt, pullIdInt) 1662 if err != nil { 1663 log.Println("failed to reopen pull", err) 1664 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1665 return 1666 } 1667 1668 // Commit the transaction 1669 if err = tx.Commit(); err != nil { 1670 log.Println("failed to commit transaction", err) 1671 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1672 return 1673 } 1674 1675 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullIdInt)) 1676 return 1677} 1678 1679func fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) { 1680 repoName := chi.URLParam(r, "repo") 1681 knot, ok := r.Context().Value("knot").(string) 1682 if !ok { 1683 log.Println("malformed middleware") 1684 return nil, fmt.Errorf("malformed middleware") 1685 } 1686 id, ok := r.Context().Value("resolvedId").(identity.Identity) 1687 if !ok { 1688 log.Println("malformed middleware") 1689 return nil, fmt.Errorf("malformed middleware") 1690 } 1691 1692 repoAt, ok := r.Context().Value("repoAt").(string) 1693 if !ok { 1694 log.Println("malformed middleware") 1695 return nil, fmt.Errorf("malformed middleware") 1696 } 1697 1698 parsedRepoAt, err := syntax.ParseATURI(repoAt) 1699 if err != nil { 1700 log.Println("malformed repo at-uri") 1701 return nil, fmt.Errorf("malformed middleware") 1702 } 1703 1704 // pass through values from the middleware 1705 description, ok := r.Context().Value("repoDescription").(string) 1706 addedAt, ok := r.Context().Value("repoAddedAt").(string) 1707 1708 return &FullyResolvedRepo{ 1709 Knot: knot, 1710 OwnerId: id, 1711 RepoName: repoName, 1712 RepoAt: parsedRepoAt, 1713 Description: description, 1714 AddedAt: addedAt, 1715 }, nil 1716} 1717 1718func rolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) pages.RolesInRepo { 1719 if u != nil { 1720 r := s.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.OwnerSlashRepo()) 1721 return pages.RolesInRepo{r} 1722 } else { 1723 return pages.RolesInRepo{} 1724 } 1725}