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 client, err := rp.oauth.AuthorizedClient(r) 661 if err != nil { 662 fail("Failed to authorize. Try again later.", err) 663 return 664 } 665 666 // ensure that this is a valid spindle for this user 667 validSpindles, err := rp.enforcer.GetSpindlesForUser(user.Did) 668 if err != nil { 669 fail("Failed to find spindles. Try again later.", err) 670 return 671 } 672 673 if !slices.Contains(validSpindles, newSpindle) { 674 fail("Failed to configure spindle.", fmt.Errorf("%s is not a valid spindle: %q", newSpindle, validSpindles)) 675 return 676 } 677 678 // optimistic update 679 err = db.UpdateSpindle(rp.db, string(repoAt), newSpindle) 680 if err != nil { 681 fail("Failed to update spindle. Try again later.", err) 682 return 683 } 684 685 ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, user.Did, rkey) 686 if err != nil { 687 fail("Failed to update spindle, no record found on PDS.", err) 688 return 689 } 690 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 691 Collection: tangled.RepoNSID, 692 Repo: user.Did, 693 Rkey: rkey, 694 SwapRecord: ex.Cid, 695 Record: &lexutil.LexiconTypeDecoder{ 696 Val: &tangled.Repo{ 697 Knot: f.Knot, 698 Name: f.RepoName, 699 Owner: user.Did, 700 CreatedAt: f.CreatedAt, 701 Description: &f.Description, 702 Spindle: &newSpindle, 703 }, 704 }, 705 }) 706 707 if err != nil { 708 fail("Failed to update spindle, unable to save to PDS.", err) 709 return 710 } 711 712 // add this spindle to spindle stream 713 rp.spindlestream.AddSource( 714 context.Background(), 715 eventconsumer.NewSpindleSource(newSpindle), 716 ) 717 718 rp.pages.HxRefresh(w) 719} 720 721func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) { 722 user := rp.oauth.GetUser(r) 723 l := rp.logger.With("handler", "AddCollaborator") 724 l = l.With("did", user.Did) 725 l = l.With("handle", user.Handle) 726 727 f, err := rp.repoResolver.Resolve(r) 728 if err != nil { 729 l.Error("failed to get repo and knot", "err", err) 730 return 731 } 732 733 errorId := "add-collaborator-error" 734 fail := func(msg string, err error) { 735 l.Error(msg, "err", err) 736 rp.pages.Notice(w, errorId, msg) 737 } 738 739 collaborator := r.FormValue("collaborator") 740 if collaborator == "" { 741 fail("Invalid form.", nil) 742 return 743 } 744 745 // remove a single leading `@`, to make @handle work with ResolveIdent 746 collaborator = strings.TrimPrefix(collaborator, "@") 747 748 collaboratorIdent, err := rp.idResolver.ResolveIdent(r.Context(), collaborator) 749 if err != nil { 750 fail(fmt.Sprintf("'%s' is not a valid DID/handle.", collaborator), err) 751 return 752 } 753 754 if collaboratorIdent.DID.String() == user.Did { 755 fail("You seem to be adding yourself as a collaborator.", nil) 756 return 757 } 758 l = l.With("collaborator", collaboratorIdent.Handle) 759 l = l.With("knot", f.Knot) 760 761 // announce this relation into the firehose, store into owners' pds 762 client, err := rp.oauth.AuthorizedClient(r) 763 if err != nil { 764 fail("Failed to write to PDS.", err) 765 return 766 } 767 768 // emit a record 769 currentUser := rp.oauth.GetUser(r) 770 rkey := tid.TID() 771 createdAt := time.Now() 772 resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 773 Collection: tangled.RepoCollaboratorNSID, 774 Repo: currentUser.Did, 775 Rkey: rkey, 776 Record: &lexutil.LexiconTypeDecoder{ 777 Val: &tangled.RepoCollaborator{ 778 Subject: collaboratorIdent.DID.String(), 779 Repo: string(f.RepoAt), 780 CreatedAt: createdAt.Format(time.RFC3339), 781 }}, 782 }) 783 // invalid record 784 if err != nil { 785 fail("Failed to write record to PDS.", err) 786 return 787 } 788 l = l.With("at-uri", resp.Uri) 789 l.Info("wrote record to PDS") 790 791 l.Info("adding to knot") 792 secret, err := db.GetRegistrationKey(rp.db, f.Knot) 793 if err != nil { 794 fail("Failed to add to knot.", err) 795 return 796 } 797 798 ksClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev) 799 if err != nil { 800 fail("Failed to add to knot.", err) 801 return 802 } 803 804 ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String()) 805 if err != nil { 806 fail("Knot was unreachable.", err) 807 return 808 } 809 810 if ksResp.StatusCode != http.StatusNoContent { 811 fail(fmt.Sprintf("Knot returned unexpected status code: %d.", ksResp.StatusCode), nil) 812 return 813 } 814 815 tx, err := rp.db.BeginTx(r.Context(), nil) 816 if err != nil { 817 fail("Failed to add collaborator.", err) 818 return 819 } 820 defer func() { 821 tx.Rollback() 822 err = rp.enforcer.E.LoadPolicy() 823 if err != nil { 824 fail("Failed to add collaborator.", err) 825 } 826 }() 827 828 err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo()) 829 if err != nil { 830 fail("Failed to add collaborator permissions.", err) 831 return 832 } 833 834 err = db.AddCollaborator(rp.db, db.Collaborator{ 835 Did: syntax.DID(currentUser.Did), 836 Rkey: rkey, 837 SubjectDid: collaboratorIdent.DID, 838 RepoAt: f.RepoAt, 839 Created: createdAt, 840 }) 841 if err != nil { 842 fail("Failed to add collaborator.", err) 843 return 844 } 845 846 err = tx.Commit() 847 if err != nil { 848 fail("Failed to add collaborator.", err) 849 return 850 } 851 852 err = rp.enforcer.E.SavePolicy() 853 if err != nil { 854 fail("Failed to update collaborator permissions.", err) 855 return 856 } 857 858 rp.pages.HxRefresh(w) 859} 860 861func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) { 862 user := rp.oauth.GetUser(r) 863 864 f, err := rp.repoResolver.Resolve(r) 865 if err != nil { 866 log.Println("failed to get repo and knot", err) 867 return 868 } 869 870 // remove record from pds 871 xrpcClient, err := rp.oauth.AuthorizedClient(r) 872 if err != nil { 873 log.Println("failed to get authorized client", err) 874 return 875 } 876 repoRkey := f.RepoAt.RecordKey().String() 877 _, err = xrpcClient.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 878 Collection: tangled.RepoNSID, 879 Repo: user.Did, 880 Rkey: repoRkey, 881 }) 882 if err != nil { 883 log.Printf("failed to delete record: %s", err) 884 rp.pages.Notice(w, "settings-delete", "Failed to delete repository from PDS.") 885 return 886 } 887 log.Println("removed repo record ", f.RepoAt.String()) 888 889 secret, err := db.GetRegistrationKey(rp.db, f.Knot) 890 if err != nil { 891 log.Printf("no key found for domain %s: %s\n", f.Knot, err) 892 return 893 } 894 895 ksClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev) 896 if err != nil { 897 log.Println("failed to create client to ", f.Knot) 898 return 899 } 900 901 ksResp, err := ksClient.RemoveRepo(f.OwnerDid(), f.RepoName) 902 if err != nil { 903 log.Printf("failed to make request to %s: %s", f.Knot, err) 904 return 905 } 906 907 if ksResp.StatusCode != http.StatusNoContent { 908 log.Println("failed to remove repo from knot, continuing anyway ", f.Knot) 909 } else { 910 log.Println("removed repo from knot ", f.Knot) 911 } 912 913 tx, err := rp.db.BeginTx(r.Context(), nil) 914 if err != nil { 915 log.Println("failed to start tx") 916 w.Write(fmt.Append(nil, "failed to add collaborator: ", err)) 917 return 918 } 919 defer func() { 920 tx.Rollback() 921 err = rp.enforcer.E.LoadPolicy() 922 if err != nil { 923 log.Println("failed to rollback policies") 924 } 925 }() 926 927 // remove collaborator RBAC 928 repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot) 929 if err != nil { 930 rp.pages.Notice(w, "settings-delete", "Failed to remove collaborators") 931 return 932 } 933 for _, c := range repoCollaborators { 934 did := c[0] 935 rp.enforcer.RemoveCollaborator(did, f.Knot, f.DidSlashRepo()) 936 } 937 log.Println("removed collaborators") 938 939 // remove repo RBAC 940 err = rp.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.DidSlashRepo()) 941 if err != nil { 942 rp.pages.Notice(w, "settings-delete", "Failed to update RBAC rules") 943 return 944 } 945 946 // remove repo from db 947 err = db.RemoveRepo(tx, f.OwnerDid(), f.RepoName) 948 if err != nil { 949 rp.pages.Notice(w, "settings-delete", "Failed to update appview") 950 return 951 } 952 log.Println("removed repo from db") 953 954 err = tx.Commit() 955 if err != nil { 956 log.Println("failed to commit changes", err) 957 http.Error(w, err.Error(), http.StatusInternalServerError) 958 return 959 } 960 961 err = rp.enforcer.E.SavePolicy() 962 if err != nil { 963 log.Println("failed to update ACLs", err) 964 http.Error(w, err.Error(), http.StatusInternalServerError) 965 return 966 } 967 968 rp.pages.HxRedirect(w, fmt.Sprintf("/%s", f.OwnerDid())) 969} 970 971func (rp *Repo) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { 972 f, err := rp.repoResolver.Resolve(r) 973 if err != nil { 974 log.Println("failed to get repo and knot", err) 975 return 976 } 977 978 branch := r.FormValue("branch") 979 if branch == "" { 980 http.Error(w, "malformed form", http.StatusBadRequest) 981 return 982 } 983 984 secret, err := db.GetRegistrationKey(rp.db, f.Knot) 985 if err != nil { 986 log.Printf("no key found for domain %s: %s\n", f.Knot, err) 987 return 988 } 989 990 ksClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev) 991 if err != nil { 992 log.Println("failed to create client to ", f.Knot) 993 return 994 } 995 996 ksResp, err := ksClient.SetDefaultBranch(f.OwnerDid(), f.RepoName, branch) 997 if err != nil { 998 log.Printf("failed to make request to %s: %s", f.Knot, err) 999 return 1000 } 1001 1002 if ksResp.StatusCode != http.StatusNoContent { 1003 rp.pages.Notice(w, "repo-settings", "Failed to set default branch. Try again later.") 1004 return 1005 } 1006 1007 w.Write(fmt.Append(nil, "default branch set to: ", branch)) 1008} 1009 1010func (rp *Repo) Secrets(w http.ResponseWriter, r *http.Request) { 1011 user := rp.oauth.GetUser(r) 1012 l := rp.logger.With("handler", "Secrets") 1013 l = l.With("handle", user.Handle) 1014 l = l.With("did", user.Did) 1015 1016 f, err := rp.repoResolver.Resolve(r) 1017 if err != nil { 1018 log.Println("failed to get repo and knot", err) 1019 return 1020 } 1021 1022 if f.Spindle == "" { 1023 log.Println("empty spindle cannot add/rm secret", err) 1024 return 1025 } 1026 1027 lxm := tangled.RepoAddSecretNSID 1028 if r.Method == http.MethodDelete { 1029 lxm = tangled.RepoRemoveSecretNSID 1030 } 1031 1032 spindleClient, err := rp.oauth.ServiceClient( 1033 r, 1034 oauth.WithService(f.Spindle), 1035 oauth.WithLxm(lxm), 1036 oauth.WithDev(rp.config.Core.Dev), 1037 ) 1038 if err != nil { 1039 log.Println("failed to create spindle client", err) 1040 return 1041 } 1042 1043 key := r.FormValue("key") 1044 if key == "" { 1045 w.WriteHeader(http.StatusBadRequest) 1046 return 1047 } 1048 1049 switch r.Method { 1050 case http.MethodPut: 1051 errorId := "add-secret-error" 1052 1053 value := r.FormValue("value") 1054 if value == "" { 1055 w.WriteHeader(http.StatusBadRequest) 1056 return 1057 } 1058 1059 err = tangled.RepoAddSecret( 1060 r.Context(), 1061 spindleClient, 1062 &tangled.RepoAddSecret_Input{ 1063 Repo: f.RepoAt.String(), 1064 Key: key, 1065 Value: value, 1066 }, 1067 ) 1068 if err != nil { 1069 l.Error("Failed to add secret.", "err", err) 1070 rp.pages.Notice(w, errorId, "Failed to add secret.") 1071 return 1072 } 1073 1074 case http.MethodDelete: 1075 errorId := "operation-error" 1076 1077 err = tangled.RepoRemoveSecret( 1078 r.Context(), 1079 spindleClient, 1080 &tangled.RepoRemoveSecret_Input{ 1081 Repo: f.RepoAt.String(), 1082 Key: key, 1083 }, 1084 ) 1085 if err != nil { 1086 l.Error("Failed to delete secret.", "err", err) 1087 rp.pages.Notice(w, errorId, "Failed to delete secret.") 1088 return 1089 } 1090 } 1091 1092 rp.pages.HxRefresh(w) 1093} 1094 1095type tab = map[string]any 1096 1097var ( 1098 // would be great to have ordered maps right about now 1099 settingsTabs []tab = []tab{ 1100 {"Name": "general", "Icon": "sliders-horizontal"}, 1101 {"Name": "access", "Icon": "users"}, 1102 {"Name": "pipelines", "Icon": "layers-2"}, 1103 } 1104) 1105 1106func (rp *Repo) RepoSettings(w http.ResponseWriter, r *http.Request) { 1107 tabVal := r.URL.Query().Get("tab") 1108 if tabVal == "" { 1109 tabVal = "general" 1110 } 1111 1112 switch tabVal { 1113 case "general": 1114 rp.generalSettings(w, r) 1115 1116 case "access": 1117 rp.accessSettings(w, r) 1118 1119 case "pipelines": 1120 rp.pipelineSettings(w, r) 1121 } 1122 1123 // user := rp.oauth.GetUser(r) 1124 // repoCollaborators, err := f.Collaborators(r.Context()) 1125 // if err != nil { 1126 // log.Println("failed to get collaborators", err) 1127 // } 1128 1129 // isCollaboratorInviteAllowed := false 1130 // if user != nil { 1131 // ok, err := rp.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.DidSlashRepo()) 1132 // if err == nil && ok { 1133 // isCollaboratorInviteAllowed = true 1134 // } 1135 // } 1136 1137 // us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev) 1138 // if err != nil { 1139 // log.Println("failed to create unsigned client", err) 1140 // return 1141 // } 1142 1143 // result, err := us.Branches(f.OwnerDid(), f.RepoName) 1144 // if err != nil { 1145 // log.Println("failed to reach knotserver", err) 1146 // return 1147 // } 1148 1149 // // all spindles that this user is a member of 1150 // spindles, err := rp.enforcer.GetSpindlesForUser(user.Did) 1151 // if err != nil { 1152 // log.Println("failed to fetch spindles", err) 1153 // return 1154 // } 1155 1156 // var secrets []*tangled.RepoListSecrets_Secret 1157 // if f.Spindle != "" { 1158 // if spindleClient, err := rp.oauth.ServiceClient( 1159 // r, 1160 // oauth.WithService(f.Spindle), 1161 // oauth.WithLxm(tangled.RepoListSecretsNSID), 1162 // oauth.WithDev(rp.config.Core.Dev), 1163 // ); err != nil { 1164 // log.Println("failed to create spindle client", err) 1165 // } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt.String()); err != nil { 1166 // log.Println("failed to fetch secrets", err) 1167 // } else { 1168 // secrets = resp.Secrets 1169 // } 1170 // } 1171 1172 // rp.pages.RepoSettings(w, pages.RepoSettingsParams{ 1173 // LoggedInUser: user, 1174 // RepoInfo: f.RepoInfo(user), 1175 // Collaborators: repoCollaborators, 1176 // IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed, 1177 // Branches: result.Branches, 1178 // Spindles: spindles, 1179 // CurrentSpindle: f.Spindle, 1180 // Secrets: secrets, 1181 // }) 1182} 1183 1184func (rp *Repo) generalSettings(w http.ResponseWriter, r *http.Request) { 1185 f, err := rp.repoResolver.Resolve(r) 1186 user := rp.oauth.GetUser(r) 1187 1188 us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev) 1189 if err != nil { 1190 log.Println("failed to create unsigned client", err) 1191 return 1192 } 1193 1194 result, err := us.Branches(f.OwnerDid(), f.RepoName) 1195 if err != nil { 1196 log.Println("failed to reach knotserver", err) 1197 return 1198 } 1199 1200 rp.pages.RepoGeneralSettings(w, pages.RepoGeneralSettingsParams{ 1201 LoggedInUser: user, 1202 RepoInfo: f.RepoInfo(user), 1203 Branches: result.Branches, 1204 Tabs: settingsTabs, 1205 Tab: "general", 1206 }) 1207} 1208 1209func (rp *Repo) accessSettings(w http.ResponseWriter, r *http.Request) { 1210 f, err := rp.repoResolver.Resolve(r) 1211 user := rp.oauth.GetUser(r) 1212 1213 repoCollaborators, err := f.Collaborators(r.Context()) 1214 if err != nil { 1215 log.Println("failed to get collaborators", err) 1216 } 1217 1218 rp.pages.RepoAccessSettings(w, pages.RepoAccessSettingsParams{ 1219 LoggedInUser: user, 1220 RepoInfo: f.RepoInfo(user), 1221 Tabs: settingsTabs, 1222 Tab: "access", 1223 Collaborators: repoCollaborators, 1224 }) 1225} 1226 1227func (rp *Repo) pipelineSettings(w http.ResponseWriter, r *http.Request) { 1228 f, err := rp.repoResolver.Resolve(r) 1229 user := rp.oauth.GetUser(r) 1230 1231 // all spindles that the repo owner is a member of 1232 spindles, err := rp.enforcer.GetSpindlesForUser(f.OwnerDid()) 1233 if err != nil { 1234 log.Println("failed to fetch spindles", err) 1235 return 1236 } 1237 1238 var secrets []*tangled.RepoListSecrets_Secret 1239 if f.Spindle != "" { 1240 if spindleClient, err := rp.oauth.ServiceClient( 1241 r, 1242 oauth.WithService(f.Spindle), 1243 oauth.WithLxm(tangled.RepoListSecretsNSID), 1244 oauth.WithDev(rp.config.Core.Dev), 1245 ); err != nil { 1246 log.Println("failed to create spindle client", err) 1247 } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt.String()); err != nil { 1248 log.Println("failed to fetch secrets", err) 1249 } else { 1250 secrets = resp.Secrets 1251 } 1252 } 1253 1254 slices.SortFunc(secrets, func(a, b *tangled.RepoListSecrets_Secret) int { 1255 return strings.Compare(a.Key, b.Key) 1256 }) 1257 1258 var dids []string 1259 for _, s := range secrets { 1260 dids = append(dids, s.CreatedBy) 1261 } 1262 resolvedIdents := rp.idResolver.ResolveIdents(r.Context(), dids) 1263 1264 // convert to a more manageable form 1265 var niceSecret []map[string]any 1266 for id, s := range secrets { 1267 when, _ := time.Parse(time.RFC3339, s.CreatedAt) 1268 niceSecret = append(niceSecret, map[string]any{ 1269 "Id": id, 1270 "Key": s.Key, 1271 "CreatedAt": when, 1272 "CreatedBy": resolvedIdents[id].Handle.String(), 1273 }) 1274 } 1275 1276 rp.pages.RepoPipelineSettings(w, pages.RepoPipelineSettingsParams{ 1277 LoggedInUser: user, 1278 RepoInfo: f.RepoInfo(user), 1279 Tabs: settingsTabs, 1280 Tab: "pipelines", 1281 Spindles: spindles, 1282 CurrentSpindle: f.Spindle, 1283 Secrets: niceSecret, 1284 }) 1285} 1286 1287func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) { 1288 user := rp.oauth.GetUser(r) 1289 f, err := rp.repoResolver.Resolve(r) 1290 if err != nil { 1291 log.Printf("failed to resolve source repo: %v", err) 1292 return 1293 } 1294 1295 switch r.Method { 1296 case http.MethodPost: 1297 secret, err := db.GetRegistrationKey(rp.db, f.Knot) 1298 if err != nil { 1299 rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", f.Knot)) 1300 return 1301 } 1302 1303 client, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev) 1304 if err != nil { 1305 rp.pages.Notice(w, "repo", "Failed to reach knot server.") 1306 return 1307 } 1308 1309 var uri string 1310 if rp.config.Core.Dev { 1311 uri = "http" 1312 } else { 1313 uri = "https" 1314 } 1315 forkName := fmt.Sprintf("%s", f.RepoName) 1316 forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName) 1317 1318 _, err = client.SyncRepoFork(user.Did, forkSourceUrl, forkName, f.Ref) 1319 if err != nil { 1320 rp.pages.Notice(w, "repo", "Failed to sync repository fork.") 1321 return 1322 } 1323 1324 rp.pages.HxRefresh(w) 1325 return 1326 } 1327} 1328 1329func (rp *Repo) ForkRepo(w http.ResponseWriter, r *http.Request) { 1330 user := rp.oauth.GetUser(r) 1331 f, err := rp.repoResolver.Resolve(r) 1332 if err != nil { 1333 log.Printf("failed to resolve source repo: %v", err) 1334 return 1335 } 1336 1337 switch r.Method { 1338 case http.MethodGet: 1339 user := rp.oauth.GetUser(r) 1340 knots, err := rp.enforcer.GetKnotsForUser(user.Did) 1341 if err != nil { 1342 rp.pages.Notice(w, "repo", "Invalid user account.") 1343 return 1344 } 1345 1346 rp.pages.ForkRepo(w, pages.ForkRepoParams{ 1347 LoggedInUser: user, 1348 Knots: knots, 1349 RepoInfo: f.RepoInfo(user), 1350 }) 1351 1352 case http.MethodPost: 1353 1354 knot := r.FormValue("knot") 1355 if knot == "" { 1356 rp.pages.Notice(w, "repo", "Invalid form submission—missing knot domain.") 1357 return 1358 } 1359 1360 ok, err := rp.enforcer.E.Enforce(user.Did, knot, knot, "repo:create") 1361 if err != nil || !ok { 1362 rp.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") 1363 return 1364 } 1365 1366 forkName := fmt.Sprintf("%s", f.RepoName) 1367 1368 // this check is *only* to see if the forked repo name already exists 1369 // in the user's account. 1370 existingRepo, err := db.GetRepo(rp.db, user.Did, f.RepoName) 1371 if err != nil { 1372 if errors.Is(err, sql.ErrNoRows) { 1373 // no existing repo with this name found, we can use the name as is 1374 } else { 1375 log.Println("error fetching existing repo from db", err) 1376 rp.pages.Notice(w, "repo", "Failed to fork this repository. Try again later.") 1377 return 1378 } 1379 } else if existingRepo != nil { 1380 // repo with this name already exists, append random string 1381 forkName = fmt.Sprintf("%s-%s", forkName, randomString(3)) 1382 } 1383 secret, err := db.GetRegistrationKey(rp.db, knot) 1384 if err != nil { 1385 rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", knot)) 1386 return 1387 } 1388 1389 client, err := knotclient.NewSignedClient(knot, secret, rp.config.Core.Dev) 1390 if err != nil { 1391 rp.pages.Notice(w, "repo", "Failed to reach knot server.") 1392 return 1393 } 1394 1395 var uri string 1396 if rp.config.Core.Dev { 1397 uri = "http" 1398 } else { 1399 uri = "https" 1400 } 1401 forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName) 1402 sourceAt := f.RepoAt.String() 1403 1404 rkey := tid.TID() 1405 repo := &db.Repo{ 1406 Did: user.Did, 1407 Name: forkName, 1408 Knot: knot, 1409 Rkey: rkey, 1410 Source: sourceAt, 1411 } 1412 1413 tx, err := rp.db.BeginTx(r.Context(), nil) 1414 if err != nil { 1415 log.Println(err) 1416 rp.pages.Notice(w, "repo", "Failed to save repository information.") 1417 return 1418 } 1419 defer func() { 1420 tx.Rollback() 1421 err = rp.enforcer.E.LoadPolicy() 1422 if err != nil { 1423 log.Println("failed to rollback policies") 1424 } 1425 }() 1426 1427 resp, err := client.ForkRepo(user.Did, forkSourceUrl, forkName) 1428 if err != nil { 1429 rp.pages.Notice(w, "repo", "Failed to create repository on knot server.") 1430 return 1431 } 1432 1433 switch resp.StatusCode { 1434 case http.StatusConflict: 1435 rp.pages.Notice(w, "repo", "A repository with that name already exists.") 1436 return 1437 case http.StatusInternalServerError: 1438 rp.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.") 1439 case http.StatusNoContent: 1440 // continue 1441 } 1442 1443 xrpcClient, err := rp.oauth.AuthorizedClient(r) 1444 if err != nil { 1445 log.Println("failed to get authorized client", err) 1446 rp.pages.Notice(w, "repo", "Failed to create repository.") 1447 return 1448 } 1449 1450 createdAt := time.Now().Format(time.RFC3339) 1451 atresp, err := xrpcClient.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1452 Collection: tangled.RepoNSID, 1453 Repo: user.Did, 1454 Rkey: rkey, 1455 Record: &lexutil.LexiconTypeDecoder{ 1456 Val: &tangled.Repo{ 1457 Knot: repo.Knot, 1458 Name: repo.Name, 1459 CreatedAt: createdAt, 1460 Owner: user.Did, 1461 Source: &sourceAt, 1462 }}, 1463 }) 1464 if err != nil { 1465 log.Printf("failed to create record: %s", err) 1466 rp.pages.Notice(w, "repo", "Failed to announce repository creation.") 1467 return 1468 } 1469 log.Println("created repo record: ", atresp.Uri) 1470 1471 repo.AtUri = atresp.Uri 1472 err = db.AddRepo(tx, repo) 1473 if err != nil { 1474 log.Println(err) 1475 rp.pages.Notice(w, "repo", "Failed to save repository information.") 1476 return 1477 } 1478 1479 // acls 1480 p, _ := securejoin.SecureJoin(user.Did, forkName) 1481 err = rp.enforcer.AddRepo(user.Did, knot, p) 1482 if err != nil { 1483 log.Println(err) 1484 rp.pages.Notice(w, "repo", "Failed to set up repository permissions.") 1485 return 1486 } 1487 1488 err = tx.Commit() 1489 if err != nil { 1490 log.Println("failed to commit changes", err) 1491 http.Error(w, err.Error(), http.StatusInternalServerError) 1492 return 1493 } 1494 1495 err = rp.enforcer.E.SavePolicy() 1496 if err != nil { 1497 log.Println("failed to update ACLs", err) 1498 http.Error(w, err.Error(), http.StatusInternalServerError) 1499 return 1500 } 1501 1502 rp.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, forkName)) 1503 return 1504 } 1505} 1506 1507func (rp *Repo) RepoCompareNew(w http.ResponseWriter, r *http.Request) { 1508 user := rp.oauth.GetUser(r) 1509 f, err := rp.repoResolver.Resolve(r) 1510 if err != nil { 1511 log.Println("failed to get repo and knot", err) 1512 return 1513 } 1514 1515 us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev) 1516 if err != nil { 1517 log.Printf("failed to create unsigned client for %s", f.Knot) 1518 rp.pages.Error503(w) 1519 return 1520 } 1521 1522 result, err := us.Branches(f.OwnerDid(), f.RepoName) 1523 if err != nil { 1524 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1525 log.Println("failed to reach knotserver", err) 1526 return 1527 } 1528 branches := result.Branches 1529 1530 sortBranches(branches) 1531 1532 var defaultBranch string 1533 for _, b := range branches { 1534 if b.IsDefault { 1535 defaultBranch = b.Name 1536 } 1537 } 1538 1539 base := defaultBranch 1540 head := defaultBranch 1541 1542 params := r.URL.Query() 1543 queryBase := params.Get("base") 1544 queryHead := params.Get("head") 1545 if queryBase != "" { 1546 base = queryBase 1547 } 1548 if queryHead != "" { 1549 head = queryHead 1550 } 1551 1552 tags, err := us.Tags(f.OwnerDid(), f.RepoName) 1553 if err != nil { 1554 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1555 log.Println("failed to reach knotserver", err) 1556 return 1557 } 1558 1559 repoinfo := f.RepoInfo(user) 1560 1561 rp.pages.RepoCompareNew(w, pages.RepoCompareNewParams{ 1562 LoggedInUser: user, 1563 RepoInfo: repoinfo, 1564 Branches: branches, 1565 Tags: tags.Tags, 1566 Base: base, 1567 Head: head, 1568 }) 1569} 1570 1571func (rp *Repo) RepoCompare(w http.ResponseWriter, r *http.Request) { 1572 user := rp.oauth.GetUser(r) 1573 f, err := rp.repoResolver.Resolve(r) 1574 if err != nil { 1575 log.Println("failed to get repo and knot", err) 1576 return 1577 } 1578 1579 var diffOpts types.DiffOpts 1580 if d := r.URL.Query().Get("diff"); d == "split" { 1581 diffOpts.Split = true 1582 } 1583 1584 // if user is navigating to one of 1585 // /compare/{base}/{head} 1586 // /compare/{base}...{head} 1587 base := chi.URLParam(r, "base") 1588 head := chi.URLParam(r, "head") 1589 if base == "" && head == "" { 1590 rest := chi.URLParam(r, "*") // master...feature/xyz 1591 parts := strings.SplitN(rest, "...", 2) 1592 if len(parts) == 2 { 1593 base = parts[0] 1594 head = parts[1] 1595 } 1596 } 1597 1598 base, _ = url.PathUnescape(base) 1599 head, _ = url.PathUnescape(head) 1600 1601 if base == "" || head == "" { 1602 log.Printf("invalid comparison") 1603 rp.pages.Error404(w) 1604 return 1605 } 1606 1607 us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev) 1608 if err != nil { 1609 log.Printf("failed to create unsigned client for %s", f.Knot) 1610 rp.pages.Error503(w) 1611 return 1612 } 1613 1614 branches, err := us.Branches(f.OwnerDid(), f.RepoName) 1615 if err != nil { 1616 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1617 log.Println("failed to reach knotserver", err) 1618 return 1619 } 1620 1621 tags, err := us.Tags(f.OwnerDid(), f.RepoName) 1622 if err != nil { 1623 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1624 log.Println("failed to reach knotserver", err) 1625 return 1626 } 1627 1628 formatPatch, err := us.Compare(f.OwnerDid(), f.RepoName, base, head) 1629 if err != nil { 1630 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1631 log.Println("failed to compare", err) 1632 return 1633 } 1634 diff := patchutil.AsNiceDiff(formatPatch.Patch, base) 1635 1636 repoinfo := f.RepoInfo(user) 1637 1638 rp.pages.RepoCompare(w, pages.RepoCompareParams{ 1639 LoggedInUser: user, 1640 RepoInfo: repoinfo, 1641 Branches: branches.Branches, 1642 Tags: tags.Tags, 1643 Base: base, 1644 Head: head, 1645 Diff: &diff, 1646 DiffOpts: diffOpts, 1647 }) 1648 1649}