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