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 == "" { 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 secret, err := db.GetRegistrationKey(s.db, f.Knot) 527 if err != nil { 528 log.Printf("failed to get registration key for %s", f.Knot) 529 s.pages.Notice(w, "pull", "Failed to load pull request. Try again later.") 530 return 531 } 532 533 var mergeCheckResponse types.MergeCheckResponse 534 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 535 if err == nil { 536 resp, err := ksClient.MergeCheck([]byte(pr.Patch), pr.OwnerDid, f.RepoName, pr.TargetBranch) 537 if err != nil { 538 log.Println("failed to check for mergeability:", err) 539 } else { 540 respBody, err := io.ReadAll(resp.Body) 541 if err != nil { 542 log.Println("failed to read merge check response body") 543 } else { 544 err = json.Unmarshal(respBody, &mergeCheckResponse) 545 if err != nil { 546 log.Println("failed to unmarshal merge check response", err) 547 } 548 } 549 } 550 } else { 551 log.Printf("failed to setup signed client for %s; ignoring...", f.Knot) 552 } 553 554 s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ 555 LoggedInUser: user, 556 RepoInfo: f.RepoInfo(s, user), 557 Pull: *pr, 558 Comments: comments, 559 PullOwnerHandle: pullOwnerIdent.Handle.String(), 560 DidHandleMap: didHandleMap, 561 MergeCheck: mergeCheckResponse, 562 }) 563} 564 565func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) { 566 f, err := fullyResolvedRepo(r) 567 if err != nil { 568 log.Println("failed to fully resolve repo", err) 569 return 570 } 571 ref := chi.URLParam(r, "ref") 572 protocol := "http" 573 if !s.config.Dev { 574 protocol = "https" 575 } 576 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref)) 577 if err != nil { 578 log.Println("failed to reach knotserver", err) 579 return 580 } 581 582 body, err := io.ReadAll(resp.Body) 583 if err != nil { 584 log.Printf("Error reading response body: %v", err) 585 return 586 } 587 588 var result types.RepoCommitResponse 589 err = json.Unmarshal(body, &result) 590 if err != nil { 591 log.Println("failed to parse response:", err) 592 return 593 } 594 595 user := s.auth.GetUser(r) 596 s.pages.RepoCommit(w, pages.RepoCommitParams{ 597 LoggedInUser: user, 598 RepoInfo: f.RepoInfo(s, user), 599 RepoCommitResponse: result, 600 }) 601 return 602} 603 604func (s *State) RepoTree(w http.ResponseWriter, r *http.Request) { 605 f, err := fullyResolvedRepo(r) 606 if err != nil { 607 log.Println("failed to fully resolve repo", err) 608 return 609 } 610 611 ref := chi.URLParam(r, "ref") 612 treePath := chi.URLParam(r, "*") 613 protocol := "http" 614 if !s.config.Dev { 615 protocol = "https" 616 } 617 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath)) 618 if err != nil { 619 log.Println("failed to reach knotserver", err) 620 return 621 } 622 623 body, err := io.ReadAll(resp.Body) 624 if err != nil { 625 log.Printf("Error reading response body: %v", err) 626 return 627 } 628 629 var result types.RepoTreeResponse 630 err = json.Unmarshal(body, &result) 631 if err != nil { 632 log.Println("failed to parse response:", err) 633 return 634 } 635 636 user := s.auth.GetUser(r) 637 638 var breadcrumbs [][]string 639 breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/%s/tree/%s", f.OwnerDid(), f.RepoName, ref)}) 640 if treePath != "" { 641 for idx, elem := range strings.Split(treePath, "/") { 642 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)}) 643 } 644 } 645 646 baseTreeLink := path.Join(f.OwnerDid(), f.RepoName, "tree", ref, treePath) 647 baseBlobLink := path.Join(f.OwnerDid(), f.RepoName, "blob", ref, treePath) 648 649 s.pages.RepoTree(w, pages.RepoTreeParams{ 650 LoggedInUser: user, 651 BreadCrumbs: breadcrumbs, 652 BaseTreeLink: baseTreeLink, 653 BaseBlobLink: baseBlobLink, 654 RepoInfo: f.RepoInfo(s, user), 655 RepoTreeResponse: result, 656 }) 657 return 658} 659 660func (s *State) RepoTags(w http.ResponseWriter, r *http.Request) { 661 f, err := fullyResolvedRepo(r) 662 if err != nil { 663 log.Println("failed to get repo and knot", err) 664 return 665 } 666 667 protocol := "http" 668 if !s.config.Dev { 669 protocol = "https" 670 } 671 672 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tags", protocol, f.Knot, f.OwnerDid(), f.RepoName)) 673 if err != nil { 674 log.Println("failed to reach knotserver", err) 675 return 676 } 677 678 body, err := io.ReadAll(resp.Body) 679 if err != nil { 680 log.Printf("Error reading response body: %v", err) 681 return 682 } 683 684 var result types.RepoTagsResponse 685 err = json.Unmarshal(body, &result) 686 if err != nil { 687 log.Println("failed to parse response:", err) 688 return 689 } 690 691 user := s.auth.GetUser(r) 692 s.pages.RepoTags(w, pages.RepoTagsParams{ 693 LoggedInUser: user, 694 RepoInfo: f.RepoInfo(s, user), 695 RepoTagsResponse: result, 696 }) 697 return 698} 699 700func (s *State) RepoBranches(w http.ResponseWriter, r *http.Request) { 701 f, err := fullyResolvedRepo(r) 702 if err != nil { 703 log.Println("failed to get repo and knot", err) 704 return 705 } 706 707 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 708 if err != nil { 709 log.Println("failed to create unsigned client", err) 710 return 711 } 712 713 resp, err := us.Branches(f.OwnerDid(), f.RepoName) 714 if err != nil { 715 log.Println("failed to reach knotserver", err) 716 return 717 } 718 719 body, err := io.ReadAll(resp.Body) 720 if err != nil { 721 log.Printf("Error reading response body: %v", err) 722 return 723 } 724 725 var result types.RepoBranchesResponse 726 err = json.Unmarshal(body, &result) 727 if err != nil { 728 log.Println("failed to parse response:", err) 729 return 730 } 731 732 user := s.auth.GetUser(r) 733 s.pages.RepoBranches(w, pages.RepoBranchesParams{ 734 LoggedInUser: user, 735 RepoInfo: f.RepoInfo(s, user), 736 RepoBranchesResponse: result, 737 }) 738 return 739} 740 741func (s *State) RepoBlob(w http.ResponseWriter, r *http.Request) { 742 f, err := fullyResolvedRepo(r) 743 if err != nil { 744 log.Println("failed to get repo and knot", err) 745 return 746 } 747 748 ref := chi.URLParam(r, "ref") 749 filePath := chi.URLParam(r, "*") 750 protocol := "http" 751 if !s.config.Dev { 752 protocol = "https" 753 } 754 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) 755 if err != nil { 756 log.Println("failed to reach knotserver", err) 757 return 758 } 759 760 body, err := io.ReadAll(resp.Body) 761 if err != nil { 762 log.Printf("Error reading response body: %v", err) 763 return 764 } 765 766 var result types.RepoBlobResponse 767 err = json.Unmarshal(body, &result) 768 if err != nil { 769 log.Println("failed to parse response:", err) 770 return 771 } 772 773 var breadcrumbs [][]string 774 breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/%s/tree/%s", f.OwnerDid(), f.RepoName, ref)}) 775 if filePath != "" { 776 for idx, elem := range strings.Split(filePath, "/") { 777 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)}) 778 } 779 } 780 781 user := s.auth.GetUser(r) 782 s.pages.RepoBlob(w, pages.RepoBlobParams{ 783 LoggedInUser: user, 784 RepoInfo: f.RepoInfo(s, user), 785 RepoBlobResponse: result, 786 BreadCrumbs: breadcrumbs, 787 }) 788 return 789} 790 791func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) { 792 f, err := fullyResolvedRepo(r) 793 if err != nil { 794 log.Println("failed to get repo and knot", err) 795 return 796 } 797 798 collaborator := r.FormValue("collaborator") 799 if collaborator == "" { 800 http.Error(w, "malformed form", http.StatusBadRequest) 801 return 802 } 803 804 collaboratorIdent, err := s.resolver.ResolveIdent(r.Context(), collaborator) 805 if err != nil { 806 w.Write([]byte("failed to resolve collaborator did to a handle")) 807 return 808 } 809 log.Printf("adding %s to %s\n", collaboratorIdent.Handle.String(), f.Knot) 810 811 // TODO: create an atproto record for this 812 813 secret, err := db.GetRegistrationKey(s.db, f.Knot) 814 if err != nil { 815 log.Printf("no key found for domain %s: %s\n", f.Knot, err) 816 return 817 } 818 819 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 820 if err != nil { 821 log.Println("failed to create client to ", f.Knot) 822 return 823 } 824 825 ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String()) 826 if err != nil { 827 log.Printf("failed to make request to %s: %s", f.Knot, err) 828 return 829 } 830 831 if ksResp.StatusCode != http.StatusNoContent { 832 w.Write([]byte(fmt.Sprint("knotserver failed to add collaborator: ", err))) 833 return 834 } 835 836 tx, err := s.db.BeginTx(r.Context(), nil) 837 if err != nil { 838 log.Println("failed to start tx") 839 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 840 return 841 } 842 defer func() { 843 tx.Rollback() 844 err = s.enforcer.E.LoadPolicy() 845 if err != nil { 846 log.Println("failed to rollback policies") 847 } 848 }() 849 850 err = s.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.OwnerSlashRepo()) 851 if err != nil { 852 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 853 return 854 } 855 856 err = db.AddCollaborator(s.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot) 857 if err != nil { 858 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 859 return 860 } 861 862 err = tx.Commit() 863 if err != nil { 864 log.Println("failed to commit changes", err) 865 http.Error(w, err.Error(), http.StatusInternalServerError) 866 return 867 } 868 869 err = s.enforcer.E.SavePolicy() 870 if err != nil { 871 log.Println("failed to update ACLs", err) 872 http.Error(w, err.Error(), http.StatusInternalServerError) 873 return 874 } 875 876 w.Write([]byte(fmt.Sprint("added collaborator: ", collaboratorIdent.Handle.String()))) 877 878} 879 880func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) { 881 f, err := fullyResolvedRepo(r) 882 if err != nil { 883 log.Println("failed to get repo and knot", err) 884 return 885 } 886 887 switch r.Method { 888 case http.MethodGet: 889 // for now, this is just pubkeys 890 user := s.auth.GetUser(r) 891 repoCollaborators, err := f.Collaborators(r.Context(), s) 892 if err != nil { 893 log.Println("failed to get collaborators", err) 894 } 895 896 isCollaboratorInviteAllowed := false 897 if user != nil { 898 ok, err := s.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.OwnerSlashRepo()) 899 if err == nil && ok { 900 isCollaboratorInviteAllowed = true 901 } 902 } 903 904 s.pages.RepoSettings(w, pages.RepoSettingsParams{ 905 LoggedInUser: user, 906 RepoInfo: f.RepoInfo(s, user), 907 Collaborators: repoCollaborators, 908 IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed, 909 }) 910 } 911} 912 913type FullyResolvedRepo struct { 914 Knot string 915 OwnerId identity.Identity 916 RepoName string 917 RepoAt syntax.ATURI 918 Description string 919 AddedAt string 920} 921 922func (f *FullyResolvedRepo) OwnerDid() string { 923 return f.OwnerId.DID.String() 924} 925 926func (f *FullyResolvedRepo) OwnerHandle() string { 927 return f.OwnerId.Handle.String() 928} 929 930func (f *FullyResolvedRepo) OwnerSlashRepo() string { 931 p, _ := securejoin.SecureJoin(f.OwnerDid(), f.RepoName) 932 return p 933} 934 935func (f *FullyResolvedRepo) Collaborators(ctx context.Context, s *State) ([]pages.Collaborator, error) { 936 repoCollaborators, err := s.enforcer.E.GetImplicitUsersForResourceByDomain(f.OwnerSlashRepo(), f.Knot) 937 if err != nil { 938 return nil, err 939 } 940 941 var collaborators []pages.Collaborator 942 for _, item := range repoCollaborators { 943 // currently only two roles: owner and member 944 var role string 945 if item[3] == "repo:owner" { 946 role = "owner" 947 } else if item[3] == "repo:collaborator" { 948 role = "collaborator" 949 } else { 950 continue 951 } 952 953 did := item[0] 954 955 c := pages.Collaborator{ 956 Did: did, 957 Handle: "", 958 Role: role, 959 } 960 collaborators = append(collaborators, c) 961 } 962 963 // populate all collborators with handles 964 identsToResolve := make([]string, len(collaborators)) 965 for i, collab := range collaborators { 966 identsToResolve[i] = collab.Did 967 } 968 969 resolvedIdents := s.resolver.ResolveIdents(ctx, identsToResolve) 970 for i, resolved := range resolvedIdents { 971 if resolved != nil { 972 collaborators[i].Handle = resolved.Handle.String() 973 } 974 } 975 976 return collaborators, nil 977} 978 979func (f *FullyResolvedRepo) RepoInfo(s *State, u *auth.User) pages.RepoInfo { 980 isStarred := false 981 if u != nil { 982 isStarred = db.GetStarStatus(s.db, u.Did, syntax.ATURI(f.RepoAt)) 983 } 984 985 starCount, err := db.GetStarCount(s.db, f.RepoAt) 986 if err != nil { 987 log.Println("failed to get star count for ", f.RepoAt) 988 } 989 issueCount, err := db.GetIssueCount(s.db, f.RepoAt) 990 if err != nil { 991 log.Println("failed to get issue count for ", f.RepoAt) 992 } 993 994 knot := f.Knot 995 if knot == "knot1.tangled.sh" { 996 knot = "tangled.sh" 997 } 998 999 return pages.RepoInfo{ 1000 OwnerDid: f.OwnerDid(), 1001 OwnerHandle: f.OwnerHandle(), 1002 Name: f.RepoName, 1003 RepoAt: f.RepoAt, 1004 Description: f.Description, 1005 IsStarred: isStarred, 1006 Knot: knot, 1007 Roles: rolesInRepo(s, u, f), 1008 Stats: db.RepoStats{ 1009 StarCount: starCount, 1010 IssueCount: issueCount, 1011 }, 1012 } 1013} 1014 1015func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { 1016 user := s.auth.GetUser(r) 1017 f, err := fullyResolvedRepo(r) 1018 if err != nil { 1019 log.Println("failed to get repo and knot", err) 1020 return 1021 } 1022 1023 issueId := chi.URLParam(r, "issue") 1024 issueIdInt, err := strconv.Atoi(issueId) 1025 if err != nil { 1026 http.Error(w, "bad issue id", http.StatusBadRequest) 1027 log.Println("failed to parse issue id", err) 1028 return 1029 } 1030 1031 issue, comments, err := db.GetIssueWithComments(s.db, f.RepoAt, issueIdInt) 1032 if err != nil { 1033 log.Println("failed to get issue and comments", err) 1034 s.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 1035 return 1036 } 1037 1038 issueOwnerIdent, err := s.resolver.ResolveIdent(r.Context(), issue.OwnerDid) 1039 if err != nil { 1040 log.Println("failed to resolve issue owner", err) 1041 } 1042 1043 identsToResolve := make([]string, len(comments)) 1044 for i, comment := range comments { 1045 identsToResolve[i] = comment.OwnerDid 1046 } 1047 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 1048 didHandleMap := make(map[string]string) 1049 for _, identity := range resolvedIds { 1050 if !identity.Handle.IsInvalidHandle() { 1051 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 1052 } else { 1053 didHandleMap[identity.DID.String()] = identity.DID.String() 1054 } 1055 } 1056 1057 s.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{ 1058 LoggedInUser: user, 1059 RepoInfo: f.RepoInfo(s, user), 1060 Issue: *issue, 1061 Comments: comments, 1062 1063 IssueOwnerHandle: issueOwnerIdent.Handle.String(), 1064 DidHandleMap: didHandleMap, 1065 }) 1066 1067} 1068 1069func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) { 1070 user := s.auth.GetUser(r) 1071 f, err := fullyResolvedRepo(r) 1072 if err != nil { 1073 log.Println("failed to get repo and knot", err) 1074 return 1075 } 1076 1077 issueId := chi.URLParam(r, "issue") 1078 issueIdInt, err := strconv.Atoi(issueId) 1079 if err != nil { 1080 http.Error(w, "bad issue id", http.StatusBadRequest) 1081 log.Println("failed to parse issue id", err) 1082 return 1083 } 1084 1085 issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt) 1086 if err != nil { 1087 log.Println("failed to get issue", err) 1088 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 1089 return 1090 } 1091 1092 collaborators, err := f.Collaborators(r.Context(), s) 1093 if err != nil { 1094 log.Println("failed to fetch repo collaborators: %w", err) 1095 } 1096 isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool { 1097 return user.Did == collab.Did 1098 }) 1099 isIssueOwner := user.Did == issue.OwnerDid 1100 1101 // TODO: make this more granular 1102 if isIssueOwner || isCollaborator { 1103 1104 closed := tangled.RepoIssueStateClosed 1105 1106 client, _ := s.auth.AuthorizedClient(r) 1107 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1108 Collection: tangled.RepoIssueStateNSID, 1109 Repo: user.Did, 1110 Rkey: s.TID(), 1111 Record: &lexutil.LexiconTypeDecoder{ 1112 Val: &tangled.RepoIssueState{ 1113 Issue: issue.IssueAt, 1114 State: &closed, 1115 }, 1116 }, 1117 }) 1118 1119 if err != nil { 1120 log.Println("failed to update issue state", err) 1121 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 1122 return 1123 } 1124 1125 err := db.CloseIssue(s.db, f.RepoAt, issueIdInt) 1126 if err != nil { 1127 log.Println("failed to close issue", err) 1128 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 1129 return 1130 } 1131 1132 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt)) 1133 return 1134 } else { 1135 log.Println("user is not permitted to close issue") 1136 http.Error(w, "for biden", http.StatusUnauthorized) 1137 return 1138 } 1139} 1140 1141func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) { 1142 user := s.auth.GetUser(r) 1143 f, err := fullyResolvedRepo(r) 1144 if err != nil { 1145 log.Println("failed to get repo and knot", err) 1146 return 1147 } 1148 1149 issueId := chi.URLParam(r, "issue") 1150 issueIdInt, err := strconv.Atoi(issueId) 1151 if err != nil { 1152 http.Error(w, "bad issue id", http.StatusBadRequest) 1153 log.Println("failed to parse issue id", err) 1154 return 1155 } 1156 1157 issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt) 1158 if err != nil { 1159 log.Println("failed to get issue", err) 1160 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 1161 return 1162 } 1163 1164 collaborators, err := f.Collaborators(r.Context(), s) 1165 if err != nil { 1166 log.Println("failed to fetch repo collaborators: %w", err) 1167 } 1168 isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool { 1169 return user.Did == collab.Did 1170 }) 1171 isIssueOwner := user.Did == issue.OwnerDid 1172 1173 if isCollaborator || isIssueOwner { 1174 err := db.ReopenIssue(s.db, f.RepoAt, issueIdInt) 1175 if err != nil { 1176 log.Println("failed to reopen issue", err) 1177 s.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.") 1178 return 1179 } 1180 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt)) 1181 return 1182 } else { 1183 log.Println("user is not the owner of the repo") 1184 http.Error(w, "forbidden", http.StatusUnauthorized) 1185 return 1186 } 1187} 1188 1189func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) { 1190 user := s.auth.GetUser(r) 1191 f, err := fullyResolvedRepo(r) 1192 if err != nil { 1193 log.Println("failed to get repo and knot", err) 1194 return 1195 } 1196 1197 issueId := chi.URLParam(r, "issue") 1198 issueIdInt, err := strconv.Atoi(issueId) 1199 if err != nil { 1200 http.Error(w, "bad issue id", http.StatusBadRequest) 1201 log.Println("failed to parse issue id", err) 1202 return 1203 } 1204 1205 switch r.Method { 1206 case http.MethodPost: 1207 body := r.FormValue("body") 1208 if body == "" { 1209 s.pages.Notice(w, "issue", "Body is required") 1210 return 1211 } 1212 1213 commentId := rand.IntN(1000000) 1214 1215 err := db.NewComment(s.db, &db.Comment{ 1216 OwnerDid: user.Did, 1217 RepoAt: f.RepoAt, 1218 Issue: issueIdInt, 1219 CommentId: commentId, 1220 Body: body, 1221 }) 1222 if err != nil { 1223 log.Println("failed to create comment", err) 1224 s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1225 return 1226 } 1227 1228 createdAt := time.Now().Format(time.RFC3339) 1229 commentIdInt64 := int64(commentId) 1230 ownerDid := user.Did 1231 issueAt, err := db.GetIssueAt(s.db, f.RepoAt, issueIdInt) 1232 if err != nil { 1233 log.Println("failed to get issue at", err) 1234 s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1235 return 1236 } 1237 1238 atUri := f.RepoAt.String() 1239 client, _ := s.auth.AuthorizedClient(r) 1240 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1241 Collection: tangled.RepoIssueCommentNSID, 1242 Repo: user.Did, 1243 Rkey: s.TID(), 1244 Record: &lexutil.LexiconTypeDecoder{ 1245 Val: &tangled.RepoIssueComment{ 1246 Repo: &atUri, 1247 Issue: issueAt, 1248 CommentId: &commentIdInt64, 1249 Owner: &ownerDid, 1250 Body: &body, 1251 CreatedAt: &createdAt, 1252 }, 1253 }, 1254 }) 1255 if err != nil { 1256 log.Println("failed to create comment", err) 1257 s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1258 return 1259 } 1260 1261 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issueIdInt, commentId)) 1262 return 1263 } 1264} 1265 1266func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) { 1267 params := r.URL.Query() 1268 state := params.Get("state") 1269 isOpen := true 1270 switch state { 1271 case "open": 1272 isOpen = true 1273 case "closed": 1274 isOpen = false 1275 default: 1276 isOpen = true 1277 } 1278 1279 user := s.auth.GetUser(r) 1280 f, err := fullyResolvedRepo(r) 1281 if err != nil { 1282 log.Println("failed to get repo and knot", err) 1283 return 1284 } 1285 1286 issues, err := db.GetIssues(s.db, f.RepoAt, isOpen) 1287 if err != nil { 1288 log.Println("failed to get issues", err) 1289 s.pages.Notice(w, "issues", "Failed to load issues. Try again later.") 1290 return 1291 } 1292 1293 identsToResolve := make([]string, len(issues)) 1294 for i, issue := range issues { 1295 identsToResolve[i] = issue.OwnerDid 1296 } 1297 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 1298 didHandleMap := make(map[string]string) 1299 for _, identity := range resolvedIds { 1300 if !identity.Handle.IsInvalidHandle() { 1301 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 1302 } else { 1303 didHandleMap[identity.DID.String()] = identity.DID.String() 1304 } 1305 } 1306 1307 s.pages.RepoIssues(w, pages.RepoIssuesParams{ 1308 LoggedInUser: s.auth.GetUser(r), 1309 RepoInfo: f.RepoInfo(s, user), 1310 Issues: issues, 1311 DidHandleMap: didHandleMap, 1312 FilteringByOpen: isOpen, 1313 }) 1314 return 1315} 1316 1317func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) { 1318 user := s.auth.GetUser(r) 1319 1320 f, err := fullyResolvedRepo(r) 1321 if err != nil { 1322 log.Println("failed to get repo and knot", err) 1323 return 1324 } 1325 1326 switch r.Method { 1327 case http.MethodGet: 1328 s.pages.RepoNewIssue(w, pages.RepoNewIssueParams{ 1329 LoggedInUser: user, 1330 RepoInfo: f.RepoInfo(s, user), 1331 }) 1332 case http.MethodPost: 1333 title := r.FormValue("title") 1334 body := r.FormValue("body") 1335 1336 if title == "" || body == "" { 1337 s.pages.Notice(w, "issues", "Title and body are required") 1338 return 1339 } 1340 1341 tx, err := s.db.BeginTx(r.Context(), nil) 1342 if err != nil { 1343 s.pages.Notice(w, "issues", "Failed to create issue, try again later") 1344 return 1345 } 1346 1347 err = db.NewIssue(tx, &db.Issue{ 1348 RepoAt: f.RepoAt, 1349 Title: title, 1350 Body: body, 1351 OwnerDid: user.Did, 1352 }) 1353 if err != nil { 1354 log.Println("failed to create issue", err) 1355 s.pages.Notice(w, "issues", "Failed to create issue.") 1356 return 1357 } 1358 1359 issueId, err := db.GetIssueId(s.db, f.RepoAt) 1360 if err != nil { 1361 log.Println("failed to get issue id", err) 1362 s.pages.Notice(w, "issues", "Failed to create issue.") 1363 return 1364 } 1365 1366 client, _ := s.auth.AuthorizedClient(r) 1367 atUri := f.RepoAt.String() 1368 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1369 Collection: tangled.RepoIssueNSID, 1370 Repo: user.Did, 1371 Rkey: s.TID(), 1372 Record: &lexutil.LexiconTypeDecoder{ 1373 Val: &tangled.RepoIssue{ 1374 Repo: atUri, 1375 Title: title, 1376 Body: &body, 1377 Owner: user.Did, 1378 IssueId: int64(issueId), 1379 }, 1380 }, 1381 }) 1382 if err != nil { 1383 log.Println("failed to create issue", err) 1384 s.pages.Notice(w, "issues", "Failed to create issue.") 1385 return 1386 } 1387 1388 err = db.SetIssueAt(s.db, f.RepoAt, issueId, resp.Uri) 1389 if err != nil { 1390 log.Println("failed to set issue at", err) 1391 s.pages.Notice(w, "issues", "Failed to create issue.") 1392 return 1393 } 1394 1395 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueId)) 1396 return 1397 } 1398} 1399 1400func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) { 1401 user := s.auth.GetUser(r) 1402 f, err := fullyResolvedRepo(r) 1403 if err != nil { 1404 log.Println("failed to get repo and knot", err) 1405 return 1406 } 1407 1408 pulls, err := db.GetPulls(s.db, f.RepoAt) 1409 if err != nil { 1410 log.Println("failed to get pulls", err) 1411 s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.") 1412 return 1413 } 1414 1415 identsToResolve := make([]string, len(pulls)) 1416 for i, pull := range pulls { 1417 identsToResolve[i] = pull.OwnerDid 1418 } 1419 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 1420 didHandleMap := make(map[string]string) 1421 for _, identity := range resolvedIds { 1422 if !identity.Handle.IsInvalidHandle() { 1423 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 1424 } else { 1425 didHandleMap[identity.DID.String()] = identity.DID.String() 1426 } 1427 } 1428 1429 s.pages.RepoPulls(w, pages.RepoPullsParams{ 1430 LoggedInUser: s.auth.GetUser(r), 1431 RepoInfo: f.RepoInfo(s, user), 1432 Pulls: pulls, 1433 DidHandleMap: didHandleMap, 1434 }) 1435 return 1436} 1437 1438func fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) { 1439 repoName := chi.URLParam(r, "repo") 1440 knot, ok := r.Context().Value("knot").(string) 1441 if !ok { 1442 log.Println("malformed middleware") 1443 return nil, fmt.Errorf("malformed middleware") 1444 } 1445 id, ok := r.Context().Value("resolvedId").(identity.Identity) 1446 if !ok { 1447 log.Println("malformed middleware") 1448 return nil, fmt.Errorf("malformed middleware") 1449 } 1450 1451 repoAt, ok := r.Context().Value("repoAt").(string) 1452 if !ok { 1453 log.Println("malformed middleware") 1454 return nil, fmt.Errorf("malformed middleware") 1455 } 1456 1457 parsedRepoAt, err := syntax.ParseATURI(repoAt) 1458 if err != nil { 1459 log.Println("malformed repo at-uri") 1460 return nil, fmt.Errorf("malformed middleware") 1461 } 1462 1463 // pass through values from the middleware 1464 description, ok := r.Context().Value("repoDescription").(string) 1465 addedAt, ok := r.Context().Value("repoAddedAt").(string) 1466 1467 return &FullyResolvedRepo{ 1468 Knot: knot, 1469 OwnerId: id, 1470 RepoName: repoName, 1471 RepoAt: parsedRepoAt, 1472 Description: description, 1473 AddedAt: addedAt, 1474 }, nil 1475} 1476 1477func rolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) pages.RolesInRepo { 1478 if u != nil { 1479 r := s.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.OwnerSlashRepo()) 1480 return pages.RolesInRepo{r} 1481 } else { 1482 return pages.RolesInRepo{} 1483 } 1484}