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