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