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