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