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