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