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