forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
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/commitverify" 22 "tangled.sh/tangled.sh/core/appview/config" 23 "tangled.sh/tangled.sh/core/appview/db" 24 "tangled.sh/tangled.sh/core/appview/idresolver" 25 "tangled.sh/tangled.sh/core/appview/oauth" 26 "tangled.sh/tangled.sh/core/appview/pages" 27 "tangled.sh/tangled.sh/core/appview/pages/markup" 28 "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 29 "tangled.sh/tangled.sh/core/appview/reporesolver" 30 "tangled.sh/tangled.sh/core/knotclient" 31 "tangled.sh/tangled.sh/core/patchutil" 32 "tangled.sh/tangled.sh/core/rbac" 33 "tangled.sh/tangled.sh/core/types" 34 35 securejoin "github.com/cyphar/filepath-securejoin" 36 "github.com/go-chi/chi/v5" 37 "github.com/go-git/go-git/v5/plumbing" 38 "github.com/posthog/posthog-go" 39 40 comatproto "github.com/bluesky-social/indigo/api/atproto" 41 lexutil "github.com/bluesky-social/indigo/lex/util" 42) 43 44type Repo struct { 45 repoResolver *reporesolver.RepoResolver 46 idResolver *idresolver.Resolver 47 config *config.Config 48 oauth *oauth.OAuth 49 pages *pages.Pages 50 db *db.DB 51 enforcer *rbac.Enforcer 52 posthog posthog.Client 53} 54 55func New( 56 oauth *oauth.OAuth, 57 repoResolver *reporesolver.RepoResolver, 58 pages *pages.Pages, 59 idResolver *idresolver.Resolver, 60 db *db.DB, 61 config *config.Config, 62 posthog posthog.Client, 63 enforcer *rbac.Enforcer, 64) *Repo { 65 return &Repo{oauth: oauth, 66 repoResolver: repoResolver, 67 pages: pages, 68 idResolver: idResolver, 69 config: config, 70 db: db, 71 posthog: posthog, 72 enforcer: enforcer, 73 } 74} 75 76func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) { 77 ref := chi.URLParam(r, "ref") 78 f, err := rp.repoResolver.Resolve(r) 79 if err != nil { 80 log.Println("failed to fully resolve repo", err) 81 return 82 } 83 84 us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev) 85 if err != nil { 86 log.Printf("failed to create unsigned client for %s", f.Knot) 87 rp.pages.Error503(w) 88 return 89 } 90 91 result, err := us.Index(f.OwnerDid(), f.RepoName, ref) 92 if err != nil { 93 rp.pages.Error503(w) 94 log.Println("failed to reach knotserver", err) 95 return 96 } 97 98 tagMap := make(map[string][]string) 99 for _, tag := range result.Tags { 100 hash := tag.Hash 101 if tag.Tag != nil { 102 hash = tag.Tag.Target.String() 103 } 104 tagMap[hash] = append(tagMap[hash], tag.Name) 105 } 106 107 for _, branch := range result.Branches { 108 hash := branch.Hash 109 tagMap[hash] = append(tagMap[hash], branch.Name) 110 } 111 112 slices.SortFunc(result.Branches, func(a, b types.Branch) int { 113 if a.Name == result.Ref { 114 return -1 115 } 116 if a.IsDefault { 117 return -1 118 } 119 if b.IsDefault { 120 return 1 121 } 122 if a.Commit != nil && b.Commit != nil { 123 if a.Commit.Committer.When.Before(b.Commit.Committer.When) { 124 return 1 125 } else { 126 return -1 127 } 128 } 129 return strings.Compare(a.Name, b.Name) * -1 130 }) 131 132 commitCount := len(result.Commits) 133 branchCount := len(result.Branches) 134 tagCount := len(result.Tags) 135 fileCount := len(result.Files) 136 137 commitCount, branchCount, tagCount = balanceIndexItems(commitCount, branchCount, tagCount, fileCount) 138 commitsTrunc := result.Commits[:min(commitCount, len(result.Commits))] 139 tagsTrunc := result.Tags[:min(tagCount, len(result.Tags))] 140 branchesTrunc := result.Branches[:min(branchCount, len(result.Branches))] 141 142 emails := uniqueEmails(commitsTrunc) 143 emailToDidMap, err := db.GetEmailToDid(rp.db, emails, true) 144 if err != nil { 145 log.Println("failed to get email to did map", err) 146 } 147 148 vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, commitsTrunc) 149 if err != nil { 150 log.Println(err) 151 } 152 153 user := rp.oauth.GetUser(r) 154 repoInfo := f.RepoInfo(user) 155 156 secret, err := db.GetRegistrationKey(rp.db, f.Knot) 157 if err != nil { 158 log.Printf("failed to get registration key for %s: %s", f.Knot, err) 159 rp.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 160 } 161 162 signedClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev) 163 if err != nil { 164 log.Printf("failed to create signed client for %s: %s", f.Knot, err) 165 return 166 } 167 168 var forkInfo *types.ForkInfo 169 if user != nil && (repoInfo.Roles.IsOwner() || repoInfo.Roles.IsCollaborator()) { 170 forkInfo, err = getForkInfo(repoInfo, rp, f, user, signedClient) 171 if err != nil { 172 log.Printf("Failed to fetch fork information: %v", err) 173 return 174 } 175 } 176 177 repoLanguages, err := signedClient.RepoLanguages(f.OwnerDid(), f.RepoName, ref) 178 if err != nil { 179 log.Printf("failed to compute language percentages: %s", err) 180 // non-fatal 181 } 182 183 rp.pages.RepoIndexPage(w, pages.RepoIndexParams{ 184 LoggedInUser: user, 185 RepoInfo: repoInfo, 186 TagMap: tagMap, 187 RepoIndexResponse: *result, 188 CommitsTrunc: commitsTrunc, 189 TagsTrunc: tagsTrunc, 190 ForkInfo: forkInfo, 191 BranchesTrunc: branchesTrunc, 192 EmailToDidOrHandle: emailToDidOrHandle(rp, emailToDidMap), 193 VerifiedCommits: vc, 194 Languages: repoLanguages, 195 }) 196 return 197} 198 199func getForkInfo( 200 repoInfo repoinfo.RepoInfo, 201 rp *Repo, 202 f *reporesolver.ResolvedRepo, 203 user *oauth.User, 204 signedClient *knotclient.SignedClient, 205) (*types.ForkInfo, error) { 206 if user == nil { 207 return nil, nil 208 } 209 210 forkInfo := types.ForkInfo{ 211 IsFork: repoInfo.Source != nil, 212 Status: types.UpToDate, 213 } 214 215 if !forkInfo.IsFork { 216 forkInfo.IsFork = false 217 return &forkInfo, nil 218 } 219 220 us, err := knotclient.NewUnsignedClient(repoInfo.Source.Knot, rp.config.Core.Dev) 221 if err != nil { 222 log.Printf("failed to create unsigned client for %s", repoInfo.Source.Knot) 223 return nil, err 224 } 225 226 result, err := us.Branches(repoInfo.Source.Did, repoInfo.Source.Name) 227 if err != nil { 228 log.Println("failed to reach knotserver", err) 229 return nil, err 230 } 231 232 if !slices.ContainsFunc(result.Branches, func(branch types.Branch) bool { 233 return branch.Name == f.Ref 234 }) { 235 forkInfo.Status = types.MissingBranch 236 return &forkInfo, nil 237 } 238 239 newHiddenRefResp, err := signedClient.NewHiddenRef(user.Did, repoInfo.Name, f.Ref, f.Ref) 240 if err != nil || newHiddenRefResp.StatusCode != http.StatusNoContent { 241 log.Printf("failed to update tracking branch: %s", err) 242 return nil, err 243 } 244 245 hiddenRef := fmt.Sprintf("hidden/%s/%s", f.Ref, f.Ref) 246 247 var status types.AncestorCheckResponse 248 forkSyncableResp, err := signedClient.RepoForkAheadBehind(user.Did, string(f.RepoAt), repoInfo.Name, f.Ref, hiddenRef) 249 if err != nil { 250 log.Printf("failed to check if fork is ahead/behind: %s", err) 251 return nil, err 252 } 253 254 if err := json.NewDecoder(forkSyncableResp.Body).Decode(&status); err != nil { 255 log.Printf("failed to decode fork status: %s", err) 256 return nil, err 257 } 258 259 forkInfo.Status = status.Status 260 return &forkInfo, nil 261} 262 263func (rp *Repo) RepoLog(w http.ResponseWriter, r *http.Request) { 264 f, err := rp.repoResolver.Resolve(r) 265 if err != nil { 266 log.Println("failed to fully resolve repo", err) 267 return 268 } 269 270 page := 1 271 if r.URL.Query().Get("page") != "" { 272 page, err = strconv.Atoi(r.URL.Query().Get("page")) 273 if err != nil { 274 page = 1 275 } 276 } 277 278 ref := chi.URLParam(r, "ref") 279 280 us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev) 281 if err != nil { 282 log.Println("failed to create unsigned client", err) 283 return 284 } 285 286 repolog, err := us.Log(f.OwnerDid(), f.RepoName, ref, page) 287 if err != nil { 288 log.Println("failed to reach knotserver", err) 289 return 290 } 291 292 result, err := us.Tags(f.OwnerDid(), f.RepoName) 293 if err != nil { 294 log.Println("failed to reach knotserver", err) 295 return 296 } 297 298 tagMap := make(map[string][]string) 299 for _, tag := range result.Tags { 300 hash := tag.Hash 301 if tag.Tag != nil { 302 hash = tag.Tag.Target.String() 303 } 304 tagMap[hash] = append(tagMap[hash], tag.Name) 305 } 306 307 user := rp.oauth.GetUser(r) 308 309 emailToDidMap, err := db.GetEmailToDid(rp.db, uniqueEmails(repolog.Commits), true) 310 if err != nil { 311 log.Println("failed to fetch email to did mapping", err) 312 } 313 314 vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, repolog.Commits) 315 if err != nil { 316 log.Println(err) 317 } 318 319 rp.pages.RepoLog(w, pages.RepoLogParams{ 320 LoggedInUser: user, 321 TagMap: tagMap, 322 RepoInfo: f.RepoInfo(user), 323 RepoLogResponse: *repolog, 324 EmailToDidOrHandle: emailToDidOrHandle(rp, emailToDidMap), 325 VerifiedCommits: vc, 326 }) 327 return 328} 329 330func (rp *Repo) RepoDescriptionEdit(w http.ResponseWriter, r *http.Request) { 331 f, err := rp.repoResolver.Resolve(r) 332 if err != nil { 333 log.Println("failed to get repo and knot", err) 334 w.WriteHeader(http.StatusBadRequest) 335 return 336 } 337 338 user := rp.oauth.GetUser(r) 339 rp.pages.EditRepoDescriptionFragment(w, pages.RepoDescriptionParams{ 340 RepoInfo: f.RepoInfo(user), 341 }) 342 return 343} 344 345func (rp *Repo) RepoDescription(w http.ResponseWriter, r *http.Request) { 346 f, err := rp.repoResolver.Resolve(r) 347 if err != nil { 348 log.Println("failed to get repo and knot", err) 349 w.WriteHeader(http.StatusBadRequest) 350 return 351 } 352 353 repoAt := f.RepoAt 354 rkey := repoAt.RecordKey().String() 355 if rkey == "" { 356 log.Println("invalid aturi for repo", err) 357 w.WriteHeader(http.StatusInternalServerError) 358 return 359 } 360 361 user := rp.oauth.GetUser(r) 362 363 switch r.Method { 364 case http.MethodGet: 365 rp.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{ 366 RepoInfo: f.RepoInfo(user), 367 }) 368 return 369 case http.MethodPut: 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.Committer.Email, 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 := commitverify.GetVerifiedCommits(rp.db, 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 VerifiedCommit: vc, 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 && b.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 755// modify the spindle configured for this repo 756func (rp *Repo) EditSpindle(w http.ResponseWriter, r *http.Request) { 757 f, err := rp.repoResolver.Resolve(r) 758 if err != nil { 759 log.Println("failed to get repo and knot", err) 760 w.WriteHeader(http.StatusBadRequest) 761 return 762 } 763 764 repoAt := f.RepoAt 765 rkey := repoAt.RecordKey().String() 766 if rkey == "" { 767 log.Println("invalid aturi for repo", err) 768 w.WriteHeader(http.StatusInternalServerError) 769 return 770 } 771 772 user := rp.oauth.GetUser(r) 773 774 newSpindle := r.FormValue("spindle") 775 client, err := rp.oauth.AuthorizedClient(r) 776 if err != nil { 777 log.Println("failed to get client") 778 rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.") 779 return 780 } 781 782 // ensure that this is a valid spindle for this user 783 validSpindles, err := rp.enforcer.GetSpindlesForUser(user.Did) 784 if err != nil { 785 log.Println("failed to get valid spindles") 786 rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.") 787 return 788 } 789 790 if !slices.Contains(validSpindles, newSpindle) { 791 log.Println("newSpindle not present in validSpindles", "newSpindle", newSpindle, "validSpindles", validSpindles) 792 rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.") 793 return 794 } 795 796 // optimistic update 797 err = db.UpdateSpindle(rp.db, string(repoAt), newSpindle) 798 if err != nil { 799 log.Println("failed to perform update-spindle query", err) 800 rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.") 801 return 802 } 803 804 ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, user.Did, rkey) 805 if err != nil { 806 // failed to get record 807 rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, no record found on PDS.") 808 return 809 } 810 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 811 Collection: tangled.RepoNSID, 812 Repo: user.Did, 813 Rkey: rkey, 814 SwapRecord: ex.Cid, 815 Record: &lexutil.LexiconTypeDecoder{ 816 Val: &tangled.Repo{ 817 Knot: f.Knot, 818 Name: f.RepoName, 819 Owner: user.Did, 820 CreatedAt: f.CreatedAt, 821 Description: &f.Description, 822 Spindle: &newSpindle, 823 }, 824 }, 825 }) 826 827 if err != nil { 828 log.Println("failed to perform update-spindle query", err) 829 // failed to get record 830 rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, unable to save to PDS.") 831 return 832 } 833 834 w.Write(fmt.Append(nil, "spindle set to: ", newSpindle)) 835} 836 837func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) { 838 f, err := rp.repoResolver.Resolve(r) 839 if err != nil { 840 log.Println("failed to get repo and knot", err) 841 return 842 } 843 844 collaborator := r.FormValue("collaborator") 845 if collaborator == "" { 846 http.Error(w, "malformed form", http.StatusBadRequest) 847 return 848 } 849 850 collaboratorIdent, err := rp.idResolver.ResolveIdent(r.Context(), collaborator) 851 if err != nil { 852 w.Write([]byte("failed to resolve collaborator did to a handle")) 853 return 854 } 855 log.Printf("adding %s to %s\n", collaboratorIdent.Handle.String(), f.Knot) 856 857 // TODO: create an atproto record for this 858 859 secret, err := db.GetRegistrationKey(rp.db, f.Knot) 860 if err != nil { 861 log.Printf("no key found for domain %s: %s\n", f.Knot, err) 862 return 863 } 864 865 ksClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev) 866 if err != nil { 867 log.Println("failed to create client to ", f.Knot) 868 return 869 } 870 871 ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String()) 872 if err != nil { 873 log.Printf("failed to make request to %s: %s", f.Knot, err) 874 return 875 } 876 877 if ksResp.StatusCode != http.StatusNoContent { 878 w.Write(fmt.Append(nil, "knotserver failed to add collaborator: ", err)) 879 return 880 } 881 882 tx, err := rp.db.BeginTx(r.Context(), nil) 883 if err != nil { 884 log.Println("failed to start tx") 885 w.Write(fmt.Append(nil, "failed to add collaborator: ", err)) 886 return 887 } 888 defer func() { 889 tx.Rollback() 890 err = rp.enforcer.E.LoadPolicy() 891 if err != nil { 892 log.Println("failed to rollback policies") 893 } 894 }() 895 896 err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo()) 897 if err != nil { 898 w.Write(fmt.Append(nil, "failed to add collaborator: ", err)) 899 return 900 } 901 902 err = db.AddCollaborator(rp.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot) 903 if err != nil { 904 w.Write(fmt.Append(nil, "failed to add collaborator: ", err)) 905 return 906 } 907 908 err = tx.Commit() 909 if err != nil { 910 log.Println("failed to commit changes", err) 911 http.Error(w, err.Error(), http.StatusInternalServerError) 912 return 913 } 914 915 err = rp.enforcer.E.SavePolicy() 916 if err != nil { 917 log.Println("failed to update ACLs", err) 918 http.Error(w, err.Error(), http.StatusInternalServerError) 919 return 920 } 921 922 w.Write(fmt.Append(nil, "added collaborator: ", collaboratorIdent.Handle.String())) 923 924} 925 926func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) { 927 user := rp.oauth.GetUser(r) 928 929 f, err := rp.repoResolver.Resolve(r) 930 if err != nil { 931 log.Println("failed to get repo and knot", err) 932 return 933 } 934 935 // remove record from pds 936 xrpcClient, err := rp.oauth.AuthorizedClient(r) 937 if err != nil { 938 log.Println("failed to get authorized client", err) 939 return 940 } 941 repoRkey := f.RepoAt.RecordKey().String() 942 _, err = xrpcClient.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 943 Collection: tangled.RepoNSID, 944 Repo: user.Did, 945 Rkey: repoRkey, 946 }) 947 if err != nil { 948 log.Printf("failed to delete record: %s", err) 949 rp.pages.Notice(w, "settings-delete", "Failed to delete repository from PDS.") 950 return 951 } 952 log.Println("removed repo record ", f.RepoAt.String()) 953 954 secret, err := db.GetRegistrationKey(rp.db, f.Knot) 955 if err != nil { 956 log.Printf("no key found for domain %s: %s\n", f.Knot, err) 957 return 958 } 959 960 ksClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev) 961 if err != nil { 962 log.Println("failed to create client to ", f.Knot) 963 return 964 } 965 966 ksResp, err := ksClient.RemoveRepo(f.OwnerDid(), f.RepoName) 967 if err != nil { 968 log.Printf("failed to make request to %s: %s", f.Knot, err) 969 return 970 } 971 972 if ksResp.StatusCode != http.StatusNoContent { 973 log.Println("failed to remove repo from knot, continuing anyway ", f.Knot) 974 } else { 975 log.Println("removed repo from knot ", f.Knot) 976 } 977 978 tx, err := rp.db.BeginTx(r.Context(), nil) 979 if err != nil { 980 log.Println("failed to start tx") 981 w.Write(fmt.Append(nil, "failed to add collaborator: ", err)) 982 return 983 } 984 defer func() { 985 tx.Rollback() 986 err = rp.enforcer.E.LoadPolicy() 987 if err != nil { 988 log.Println("failed to rollback policies") 989 } 990 }() 991 992 // remove collaborator RBAC 993 repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot) 994 if err != nil { 995 rp.pages.Notice(w, "settings-delete", "Failed to remove collaborators") 996 return 997 } 998 for _, c := range repoCollaborators { 999 did := c[0] 1000 rp.enforcer.RemoveCollaborator(did, f.Knot, f.DidSlashRepo()) 1001 } 1002 log.Println("removed collaborators") 1003 1004 // remove repo RBAC 1005 err = rp.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.DidSlashRepo()) 1006 if err != nil { 1007 rp.pages.Notice(w, "settings-delete", "Failed to update RBAC rules") 1008 return 1009 } 1010 1011 // remove repo from db 1012 err = db.RemoveRepo(tx, f.OwnerDid(), f.RepoName) 1013 if err != nil { 1014 rp.pages.Notice(w, "settings-delete", "Failed to update appview") 1015 return 1016 } 1017 log.Println("removed repo from db") 1018 1019 err = tx.Commit() 1020 if err != nil { 1021 log.Println("failed to commit changes", err) 1022 http.Error(w, err.Error(), http.StatusInternalServerError) 1023 return 1024 } 1025 1026 err = rp.enforcer.E.SavePolicy() 1027 if err != nil { 1028 log.Println("failed to update ACLs", err) 1029 http.Error(w, err.Error(), http.StatusInternalServerError) 1030 return 1031 } 1032 1033 rp.pages.HxRedirect(w, fmt.Sprintf("/%s", f.OwnerDid())) 1034} 1035 1036func (rp *Repo) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { 1037 f, err := rp.repoResolver.Resolve(r) 1038 if err != nil { 1039 log.Println("failed to get repo and knot", err) 1040 return 1041 } 1042 1043 branch := r.FormValue("branch") 1044 if branch == "" { 1045 http.Error(w, "malformed form", http.StatusBadRequest) 1046 return 1047 } 1048 1049 secret, err := db.GetRegistrationKey(rp.db, f.Knot) 1050 if err != nil { 1051 log.Printf("no key found for domain %s: %s\n", f.Knot, err) 1052 return 1053 } 1054 1055 ksClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev) 1056 if err != nil { 1057 log.Println("failed to create client to ", f.Knot) 1058 return 1059 } 1060 1061 ksResp, err := ksClient.SetDefaultBranch(f.OwnerDid(), f.RepoName, branch) 1062 if err != nil { 1063 log.Printf("failed to make request to %s: %s", f.Knot, err) 1064 return 1065 } 1066 1067 if ksResp.StatusCode != http.StatusNoContent { 1068 rp.pages.Notice(w, "repo-settings", "Failed to set default branch. Try again later.") 1069 return 1070 } 1071 1072 w.Write(fmt.Append(nil, "default branch set to: ", branch)) 1073} 1074 1075func (rp *Repo) RepoSettings(w http.ResponseWriter, r *http.Request) { 1076 f, err := rp.repoResolver.Resolve(r) 1077 if err != nil { 1078 log.Println("failed to get repo and knot", err) 1079 return 1080 } 1081 1082 switch r.Method { 1083 case http.MethodGet: 1084 // for now, this is just pubkeys 1085 user := rp.oauth.GetUser(r) 1086 repoCollaborators, err := f.Collaborators(r.Context()) 1087 if err != nil { 1088 log.Println("failed to get collaborators", err) 1089 } 1090 1091 isCollaboratorInviteAllowed := false 1092 if user != nil { 1093 ok, err := rp.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.DidSlashRepo()) 1094 if err == nil && ok { 1095 isCollaboratorInviteAllowed = true 1096 } 1097 } 1098 1099 us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev) 1100 if err != nil { 1101 log.Println("failed to create unsigned client", err) 1102 return 1103 } 1104 1105 result, err := us.Branches(f.OwnerDid(), f.RepoName) 1106 if err != nil { 1107 log.Println("failed to reach knotserver", err) 1108 return 1109 } 1110 1111 // all spindles that this user is a member of 1112 spindles, err := rp.enforcer.GetSpindlesForUser(user.Did) 1113 if err != nil { 1114 log.Println("failed to fetch spindles", err) 1115 return 1116 } 1117 1118 rp.pages.RepoSettings(w, pages.RepoSettingsParams{ 1119 LoggedInUser: user, 1120 RepoInfo: f.RepoInfo(user), 1121 Collaborators: repoCollaborators, 1122 IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed, 1123 Branches: result.Branches, 1124 Spindles: spindles, 1125 CurrentSpindle: f.Spindle, 1126 }) 1127 } 1128} 1129 1130func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) { 1131 user := rp.oauth.GetUser(r) 1132 f, err := rp.repoResolver.Resolve(r) 1133 if err != nil { 1134 log.Printf("failed to resolve source repo: %v", err) 1135 return 1136 } 1137 1138 switch r.Method { 1139 case http.MethodPost: 1140 secret, err := db.GetRegistrationKey(rp.db, f.Knot) 1141 if err != nil { 1142 rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", f.Knot)) 1143 return 1144 } 1145 1146 client, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev) 1147 if err != nil { 1148 rp.pages.Notice(w, "repo", "Failed to reach knot server.") 1149 return 1150 } 1151 1152 var uri string 1153 if rp.config.Core.Dev { 1154 uri = "http" 1155 } else { 1156 uri = "https" 1157 } 1158 forkName := fmt.Sprintf("%s", f.RepoName) 1159 forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName) 1160 1161 _, err = client.SyncRepoFork(user.Did, forkSourceUrl, forkName, f.Ref) 1162 if err != nil { 1163 rp.pages.Notice(w, "repo", "Failed to sync repository fork.") 1164 return 1165 } 1166 1167 rp.pages.HxRefresh(w) 1168 return 1169 } 1170} 1171 1172func (rp *Repo) ForkRepo(w http.ResponseWriter, r *http.Request) { 1173 user := rp.oauth.GetUser(r) 1174 f, err := rp.repoResolver.Resolve(r) 1175 if err != nil { 1176 log.Printf("failed to resolve source repo: %v", err) 1177 return 1178 } 1179 1180 switch r.Method { 1181 case http.MethodGet: 1182 user := rp.oauth.GetUser(r) 1183 knots, err := rp.enforcer.GetKnotsForUser(user.Did) 1184 if err != nil { 1185 rp.pages.Notice(w, "repo", "Invalid user account.") 1186 return 1187 } 1188 1189 rp.pages.ForkRepo(w, pages.ForkRepoParams{ 1190 LoggedInUser: user, 1191 Knots: knots, 1192 RepoInfo: f.RepoInfo(user), 1193 }) 1194 1195 case http.MethodPost: 1196 1197 knot := r.FormValue("knot") 1198 if knot == "" { 1199 rp.pages.Notice(w, "repo", "Invalid form submission—missing knot domain.") 1200 return 1201 } 1202 1203 ok, err := rp.enforcer.E.Enforce(user.Did, knot, knot, "repo:create") 1204 if err != nil || !ok { 1205 rp.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") 1206 return 1207 } 1208 1209 forkName := fmt.Sprintf("%s", f.RepoName) 1210 1211 // this check is *only* to see if the forked repo name already exists 1212 // in the user's account. 1213 existingRepo, err := db.GetRepo(rp.db, user.Did, f.RepoName) 1214 if err != nil { 1215 if errors.Is(err, sql.ErrNoRows) { 1216 // no existing repo with this name found, we can use the name as is 1217 } else { 1218 log.Println("error fetching existing repo from db", err) 1219 rp.pages.Notice(w, "repo", "Failed to fork this repository. Try again later.") 1220 return 1221 } 1222 } else if existingRepo != nil { 1223 // repo with this name already exists, append random string 1224 forkName = fmt.Sprintf("%s-%s", forkName, randomString(3)) 1225 } 1226 secret, err := db.GetRegistrationKey(rp.db, knot) 1227 if err != nil { 1228 rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", knot)) 1229 return 1230 } 1231 1232 client, err := knotclient.NewSignedClient(knot, secret, rp.config.Core.Dev) 1233 if err != nil { 1234 rp.pages.Notice(w, "repo", "Failed to reach knot server.") 1235 return 1236 } 1237 1238 var uri string 1239 if rp.config.Core.Dev { 1240 uri = "http" 1241 } else { 1242 uri = "https" 1243 } 1244 forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName) 1245 sourceAt := f.RepoAt.String() 1246 1247 rkey := appview.TID() 1248 repo := &db.Repo{ 1249 Did: user.Did, 1250 Name: forkName, 1251 Knot: knot, 1252 Rkey: rkey, 1253 Source: sourceAt, 1254 } 1255 1256 tx, err := rp.db.BeginTx(r.Context(), nil) 1257 if err != nil { 1258 log.Println(err) 1259 rp.pages.Notice(w, "repo", "Failed to save repository information.") 1260 return 1261 } 1262 defer func() { 1263 tx.Rollback() 1264 err = rp.enforcer.E.LoadPolicy() 1265 if err != nil { 1266 log.Println("failed to rollback policies") 1267 } 1268 }() 1269 1270 resp, err := client.ForkRepo(user.Did, forkSourceUrl, forkName) 1271 if err != nil { 1272 rp.pages.Notice(w, "repo", "Failed to create repository on knot server.") 1273 return 1274 } 1275 1276 switch resp.StatusCode { 1277 case http.StatusConflict: 1278 rp.pages.Notice(w, "repo", "A repository with that name already exists.") 1279 return 1280 case http.StatusInternalServerError: 1281 rp.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.") 1282 case http.StatusNoContent: 1283 // continue 1284 } 1285 1286 xrpcClient, err := rp.oauth.AuthorizedClient(r) 1287 if err != nil { 1288 log.Println("failed to get authorized client", err) 1289 rp.pages.Notice(w, "repo", "Failed to create repository.") 1290 return 1291 } 1292 1293 createdAt := time.Now().Format(time.RFC3339) 1294 atresp, err := xrpcClient.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1295 Collection: tangled.RepoNSID, 1296 Repo: user.Did, 1297 Rkey: rkey, 1298 Record: &lexutil.LexiconTypeDecoder{ 1299 Val: &tangled.Repo{ 1300 Knot: repo.Knot, 1301 Name: repo.Name, 1302 CreatedAt: createdAt, 1303 Owner: user.Did, 1304 Source: &sourceAt, 1305 }}, 1306 }) 1307 if err != nil { 1308 log.Printf("failed to create record: %s", err) 1309 rp.pages.Notice(w, "repo", "Failed to announce repository creation.") 1310 return 1311 } 1312 log.Println("created repo record: ", atresp.Uri) 1313 1314 repo.AtUri = atresp.Uri 1315 err = db.AddRepo(tx, repo) 1316 if err != nil { 1317 log.Println(err) 1318 rp.pages.Notice(w, "repo", "Failed to save repository information.") 1319 return 1320 } 1321 1322 // acls 1323 p, _ := securejoin.SecureJoin(user.Did, forkName) 1324 err = rp.enforcer.AddRepo(user.Did, knot, p) 1325 if err != nil { 1326 log.Println(err) 1327 rp.pages.Notice(w, "repo", "Failed to set up repository permissions.") 1328 return 1329 } 1330 1331 err = tx.Commit() 1332 if err != nil { 1333 log.Println("failed to commit changes", err) 1334 http.Error(w, err.Error(), http.StatusInternalServerError) 1335 return 1336 } 1337 1338 err = rp.enforcer.E.SavePolicy() 1339 if err != nil { 1340 log.Println("failed to update ACLs", err) 1341 http.Error(w, err.Error(), http.StatusInternalServerError) 1342 return 1343 } 1344 1345 rp.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, forkName)) 1346 return 1347 } 1348} 1349 1350func (rp *Repo) RepoCompareNew(w http.ResponseWriter, r *http.Request) { 1351 user := rp.oauth.GetUser(r) 1352 f, err := rp.repoResolver.Resolve(r) 1353 if err != nil { 1354 log.Println("failed to get repo and knot", err) 1355 return 1356 } 1357 1358 us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev) 1359 if err != nil { 1360 log.Printf("failed to create unsigned client for %s", f.Knot) 1361 rp.pages.Error503(w) 1362 return 1363 } 1364 1365 result, err := us.Branches(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 branches := result.Branches 1372 sort.Slice(branches, func(i int, j int) bool { 1373 return branches[i].Commit.Committer.When.After(branches[j].Commit.Committer.When) 1374 }) 1375 1376 var defaultBranch string 1377 for _, b := range branches { 1378 if b.IsDefault { 1379 defaultBranch = b.Name 1380 } 1381 } 1382 1383 base := defaultBranch 1384 head := defaultBranch 1385 1386 params := r.URL.Query() 1387 queryBase := params.Get("base") 1388 queryHead := params.Get("head") 1389 if queryBase != "" { 1390 base = queryBase 1391 } 1392 if queryHead != "" { 1393 head = queryHead 1394 } 1395 1396 tags, err := us.Tags(f.OwnerDid(), f.RepoName) 1397 if err != nil { 1398 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1399 log.Println("failed to reach knotserver", err) 1400 return 1401 } 1402 1403 repoinfo := f.RepoInfo(user) 1404 1405 rp.pages.RepoCompareNew(w, pages.RepoCompareNewParams{ 1406 LoggedInUser: user, 1407 RepoInfo: repoinfo, 1408 Branches: branches, 1409 Tags: tags.Tags, 1410 Base: base, 1411 Head: head, 1412 }) 1413} 1414 1415func (rp *Repo) RepoCompare(w http.ResponseWriter, r *http.Request) { 1416 user := rp.oauth.GetUser(r) 1417 f, err := rp.repoResolver.Resolve(r) 1418 if err != nil { 1419 log.Println("failed to get repo and knot", err) 1420 return 1421 } 1422 1423 // if user is navigating to one of 1424 // /compare/{base}/{head} 1425 // /compare/{base}...{head} 1426 base := chi.URLParam(r, "base") 1427 head := chi.URLParam(r, "head") 1428 if base == "" && head == "" { 1429 rest := chi.URLParam(r, "*") // master...feature/xyz 1430 parts := strings.SplitN(rest, "...", 2) 1431 if len(parts) == 2 { 1432 base = parts[0] 1433 head = parts[1] 1434 } 1435 } 1436 1437 base, _ = url.PathUnescape(base) 1438 head, _ = url.PathUnescape(head) 1439 1440 if base == "" || head == "" { 1441 log.Printf("invalid comparison") 1442 rp.pages.Error404(w) 1443 return 1444 } 1445 1446 us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev) 1447 if err != nil { 1448 log.Printf("failed to create unsigned client for %s", f.Knot) 1449 rp.pages.Error503(w) 1450 return 1451 } 1452 1453 branches, err := us.Branches(f.OwnerDid(), f.RepoName) 1454 if err != nil { 1455 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1456 log.Println("failed to reach knotserver", err) 1457 return 1458 } 1459 1460 tags, err := us.Tags(f.OwnerDid(), f.RepoName) 1461 if err != nil { 1462 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1463 log.Println("failed to reach knotserver", err) 1464 return 1465 } 1466 1467 formatPatch, err := us.Compare(f.OwnerDid(), f.RepoName, base, head) 1468 if err != nil { 1469 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1470 log.Println("failed to compare", err) 1471 return 1472 } 1473 diff := patchutil.AsNiceDiff(formatPatch.Patch, base) 1474 1475 repoinfo := f.RepoInfo(user) 1476 1477 rp.pages.RepoCompare(w, pages.RepoCompareParams{ 1478 LoggedInUser: user, 1479 RepoInfo: repoinfo, 1480 Branches: branches.Branches, 1481 Tags: tags.Tags, 1482 Base: base, 1483 Head: head, 1484 Diff: &diff, 1485 }) 1486 1487}