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