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