this repo has no description
1package state 2 3import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "log" 8 "net/http" 9 "strconv" 10 "time" 11 12 "github.com/sotangled/tangled/api/tangled" 13 "github.com/sotangled/tangled/appview/db" 14 "github.com/sotangled/tangled/appview/pages" 15 "github.com/sotangled/tangled/types" 16 17 comatproto "github.com/bluesky-social/indigo/api/atproto" 18 lexutil "github.com/bluesky-social/indigo/lex/util" 19) 20 21func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) { 22 user := s.auth.GetUser(r) 23 f, err := fullyResolvedRepo(r) 24 if err != nil { 25 log.Println("failed to get repo and knot", err) 26 return 27 } 28 29 pull, ok := r.Context().Value("pull").(*db.Pull) 30 if !ok { 31 log.Println("failed to get pull") 32 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 33 return 34 } 35 36 totalIdents := 1 37 for _, submission := range pull.Submissions { 38 totalIdents += len(submission.Comments) 39 } 40 41 identsToResolve := make([]string, totalIdents) 42 43 // populate idents 44 identsToResolve[0] = pull.OwnerDid 45 idx := 1 46 for _, submission := range pull.Submissions { 47 for _, comment := range submission.Comments { 48 identsToResolve[idx] = comment.OwnerDid 49 idx += 1 50 } 51 } 52 53 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 54 didHandleMap := make(map[string]string) 55 for _, identity := range resolvedIds { 56 if !identity.Handle.IsInvalidHandle() { 57 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 58 } else { 59 didHandleMap[identity.DID.String()] = identity.DID.String() 60 } 61 } 62 63 var mergeCheckResponse types.MergeCheckResponse 64 65 // Only perform merge check if the pull request is not already merged 66 if pull.State != db.PullMerged { 67 secret, err := db.GetRegistrationKey(s.db, f.Knot) 68 if err != nil { 69 log.Printf("failed to get registration key for %s", f.Knot) 70 s.pages.Notice(w, "pull", "Failed to load pull request. Try again later.") 71 return 72 } 73 74 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 75 if err == nil { 76 resp, err := ksClient.MergeCheck([]byte(pull.LatestPatch()), pull.OwnerDid, f.RepoName, pull.TargetBranch) 77 if err != nil { 78 log.Println("failed to check for mergeability:", err) 79 } else { 80 respBody, err := io.ReadAll(resp.Body) 81 if err != nil { 82 log.Println("failed to read merge check response body") 83 } else { 84 err = json.Unmarshal(respBody, &mergeCheckResponse) 85 if err != nil { 86 log.Println("failed to unmarshal merge check response", err) 87 } 88 } 89 } 90 } else { 91 log.Printf("failed to setup signed client for %s; ignoring...", f.Knot) 92 } 93 } 94 95 s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ 96 LoggedInUser: user, 97 RepoInfo: f.RepoInfo(s, user), 98 DidHandleMap: didHandleMap, 99 Pull: *pull, 100 MergeCheck: mergeCheckResponse, 101 }) 102} 103 104func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) { 105 user := s.auth.GetUser(r) 106 params := r.URL.Query() 107 108 state := db.PullOpen 109 switch params.Get("state") { 110 case "closed": 111 state = db.PullClosed 112 case "merged": 113 state = db.PullMerged 114 } 115 116 f, err := fullyResolvedRepo(r) 117 if err != nil { 118 log.Println("failed to get repo and knot", err) 119 return 120 } 121 122 pulls, err := db.GetPulls(s.db, f.RepoAt, state) 123 if err != nil { 124 log.Println("failed to get pulls", err) 125 s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.") 126 return 127 } 128 129 identsToResolve := make([]string, len(pulls)) 130 for i, pull := range pulls { 131 identsToResolve[i] = pull.OwnerDid 132 } 133 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 134 didHandleMap := make(map[string]string) 135 for _, identity := range resolvedIds { 136 if !identity.Handle.IsInvalidHandle() { 137 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 138 } else { 139 didHandleMap[identity.DID.String()] = identity.DID.String() 140 } 141 } 142 143 s.pages.RepoPulls(w, pages.RepoPullsParams{ 144 LoggedInUser: s.auth.GetUser(r), 145 RepoInfo: f.RepoInfo(s, user), 146 Pulls: pulls, 147 DidHandleMap: didHandleMap, 148 FilteringBy: state, 149 }) 150 return 151} 152 153func (s *State) PullComment(w http.ResponseWriter, r *http.Request) { 154 user := s.auth.GetUser(r) 155 f, err := fullyResolvedRepo(r) 156 if err != nil { 157 log.Println("failed to get repo and knot", err) 158 return 159 } 160 161 pull, ok := r.Context().Value("pull").(*db.Pull) 162 if !ok { 163 log.Println("failed to get pull") 164 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 165 return 166 } 167 168 switch r.Method { 169 case http.MethodPost: 170 body := r.FormValue("body") 171 if body == "" { 172 s.pages.Notice(w, "pull", "Comment body is required") 173 return 174 } 175 176 submissionIdstr := r.FormValue("submissionId") 177 submissionId, err := strconv.Atoi(submissionIdstr) 178 if err != nil { 179 s.pages.Notice(w, "pull", "Invalid comment submission.") 180 return 181 } 182 183 // Start a transaction 184 tx, err := s.db.BeginTx(r.Context(), nil) 185 if err != nil { 186 log.Println("failed to start transaction", err) 187 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 188 return 189 } 190 defer tx.Rollback() 191 192 createdAt := time.Now().Format(time.RFC3339) 193 ownerDid := user.Did 194 195 pullAt, err := db.GetPullAt(s.db, f.RepoAt, pull.PullId) 196 if err != nil { 197 log.Println("failed to get pull at", err) 198 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 199 return 200 } 201 202 atUri := f.RepoAt.String() 203 client, _ := s.auth.AuthorizedClient(r) 204 atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 205 Collection: tangled.RepoPullCommentNSID, 206 Repo: user.Did, 207 Rkey: s.TID(), 208 Record: &lexutil.LexiconTypeDecoder{ 209 Val: &tangled.RepoPullComment{ 210 Repo: &atUri, 211 Pull: pullAt, 212 Owner: &ownerDid, 213 Body: &body, 214 CreatedAt: &createdAt, 215 }, 216 }, 217 }) 218 if err != nil { 219 log.Println("failed to create pull comment", err) 220 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 221 return 222 } 223 224 // Create the pull comment in the database with the commentAt field 225 commentId, err := db.NewPullComment(tx, &db.PullComment{ 226 OwnerDid: user.Did, 227 RepoAt: f.RepoAt.String(), 228 PullId: pull.PullId, 229 Body: body, 230 CommentAt: atResp.Uri, 231 SubmissionId: submissionId, 232 }) 233 if err != nil { 234 log.Println("failed to create pull comment", err) 235 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 236 return 237 } 238 239 // Commit the transaction 240 if err = tx.Commit(); err != nil { 241 log.Println("failed to commit transaction", err) 242 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 243 return 244 } 245 246 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", f.OwnerSlashRepo(), pull.PullId, commentId)) 247 return 248 } 249} 250 251func (s *State) NewPull(w http.ResponseWriter, r *http.Request) { 252 user := s.auth.GetUser(r) 253 f, err := fullyResolvedRepo(r) 254 if err != nil { 255 log.Println("failed to get repo and knot", err) 256 return 257 } 258 259 switch r.Method { 260 case http.MethodGet: 261 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 262 if err != nil { 263 log.Printf("failed to create unsigned client for %s", f.Knot) 264 s.pages.Error503(w) 265 return 266 } 267 268 resp, err := us.Branches(f.OwnerDid(), f.RepoName) 269 if err != nil { 270 log.Println("failed to reach knotserver", err) 271 return 272 } 273 274 body, err := io.ReadAll(resp.Body) 275 if err != nil { 276 log.Printf("Error reading response body: %v", err) 277 return 278 } 279 280 var result types.RepoBranchesResponse 281 err = json.Unmarshal(body, &result) 282 if err != nil { 283 log.Println("failed to parse response:", err) 284 return 285 } 286 287 s.pages.RepoNewPull(w, pages.RepoNewPullParams{ 288 LoggedInUser: user, 289 RepoInfo: f.RepoInfo(s, user), 290 Branches: result.Branches, 291 }) 292 case http.MethodPost: 293 title := r.FormValue("title") 294 body := r.FormValue("body") 295 targetBranch := r.FormValue("targetBranch") 296 patch := r.FormValue("patch") 297 298 if title == "" || body == "" || patch == "" || targetBranch == "" { 299 s.pages.Notice(w, "pull", "Title, body and patch diff are required.") 300 return 301 } 302 303 tx, err := s.db.BeginTx(r.Context(), nil) 304 if err != nil { 305 log.Println("failed to start tx") 306 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 307 return 308 } 309 defer tx.Rollback() 310 311 rkey := s.TID() 312 initialSubmission := db.PullSubmission{ 313 Patch: patch, 314 } 315 err = db.NewPull(tx, &db.Pull{ 316 Title: title, 317 Body: body, 318 TargetBranch: targetBranch, 319 OwnerDid: user.Did, 320 RepoAt: f.RepoAt, 321 Rkey: rkey, 322 Submissions: []*db.PullSubmission{ 323 &initialSubmission, 324 }, 325 }) 326 if err != nil { 327 log.Println("failed to create pull request", err) 328 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 329 return 330 } 331 client, _ := s.auth.AuthorizedClient(r) 332 pullId, err := db.NextPullId(s.db, f.RepoAt) 333 if err != nil { 334 log.Println("failed to get pull id", err) 335 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 336 return 337 } 338 339 atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 340 Collection: tangled.RepoPullNSID, 341 Repo: user.Did, 342 Rkey: rkey, 343 Record: &lexutil.LexiconTypeDecoder{ 344 Val: &tangled.RepoPull{ 345 Title: title, 346 PullId: int64(pullId), 347 TargetRepo: string(f.RepoAt), 348 TargetBranch: targetBranch, 349 Patch: patch, 350 }, 351 }, 352 }) 353 354 err = db.SetPullAt(s.db, f.RepoAt, pullId, atResp.Uri) 355 if err != nil { 356 log.Println("failed to get pull id", err) 357 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 358 return 359 } 360 361 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId)) 362 return 363 } 364} 365 366func (s *State) ResubmitPull(w http.ResponseWriter, r *http.Request) { 367 user := s.auth.GetUser(r) 368 f, err := fullyResolvedRepo(r) 369 if err != nil { 370 log.Println("failed to get repo and knot", err) 371 return 372 } 373 374 pull, ok := r.Context().Value("pull").(*db.Pull) 375 if !ok { 376 log.Println("failed to get pull") 377 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 378 return 379 } 380 381 switch r.Method { 382 case http.MethodPost: 383 patch := r.FormValue("patch") 384 385 if patch == "" { 386 s.pages.Notice(w, "resubmit-error", "Patch is empty.") 387 return 388 } 389 390 if patch == pull.LatestPatch() { 391 s.pages.Notice(w, "resubmit-error", "Patch is identical to previous submission.") 392 return 393 } 394 395 tx, err := s.db.BeginTx(r.Context(), nil) 396 if err != nil { 397 log.Println("failed to start tx") 398 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 399 return 400 } 401 defer tx.Rollback() 402 403 err = db.ResubmitPull(tx, pull, patch) 404 if err != nil { 405 log.Println("failed to create pull request", err) 406 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 407 return 408 } 409 client, _ := s.auth.AuthorizedClient(r) 410 411 ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) 412 if err != nil { 413 // failed to get record 414 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 415 return 416 } 417 418 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 419 Collection: tangled.RepoPullNSID, 420 Repo: user.Did, 421 Rkey: pull.Rkey, 422 SwapRecord: ex.Cid, 423 Record: &lexutil.LexiconTypeDecoder{ 424 Val: &tangled.RepoPull{ 425 Title: pull.Title, 426 PullId: int64(pull.PullId), 427 TargetRepo: string(f.RepoAt), 428 TargetBranch: pull.TargetBranch, 429 Patch: patch, // new patch 430 }, 431 }, 432 }) 433 if err != nil { 434 log.Println("failed to update record", err) 435 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 436 return 437 } 438 439 if err = tx.Commit(); err != nil { 440 log.Println("failed to commit transaction", err) 441 s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.") 442 return 443 } 444 445 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 446 return 447 } 448} 449 450func (s *State) MergePull(w http.ResponseWriter, r *http.Request) { 451 user := s.auth.GetUser(r) 452 f, err := fullyResolvedRepo(r) 453 if err != nil { 454 log.Println("failed to resolve repo:", err) 455 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 456 return 457 } 458 459 pull, ok := r.Context().Value("pull").(*db.Pull) 460 if !ok { 461 log.Println("failed to get pull") 462 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 463 return 464 } 465 466 secret, err := db.GetRegistrationKey(s.db, f.Knot) 467 if err != nil { 468 log.Printf("no registration key found for domain %s: %s\n", f.Knot, err) 469 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 470 return 471 } 472 473 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 474 if err != nil { 475 log.Printf("failed to create signed client for %s: %s", f.Knot, err) 476 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 477 return 478 } 479 480 // Merge the pull request 481 resp, err := ksClient.Merge([]byte(pull.LatestPatch()), user.Did, f.RepoName, pull.TargetBranch) 482 if err != nil { 483 log.Printf("failed to merge pull request: %s", err) 484 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 485 return 486 } 487 488 if resp.StatusCode == http.StatusOK { 489 err := db.MergePull(s.db, f.RepoAt, pull.PullId) 490 if err != nil { 491 log.Printf("failed to update pull request status in database: %s", err) 492 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 493 return 494 } 495 s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId)) 496 } else { 497 log.Printf("knotserver returned non-OK status code for merge: %d", resp.StatusCode) 498 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 499 } 500} 501 502func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) { 503 user := s.auth.GetUser(r) 504 505 f, err := fullyResolvedRepo(r) 506 if err != nil { 507 log.Println("malformed middleware") 508 return 509 } 510 511 pull, ok := r.Context().Value("pull").(*db.Pull) 512 if !ok { 513 log.Println("failed to get pull") 514 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 515 return 516 } 517 518 // auth filter: only owner or collaborators can close 519 roles := RolesInRepo(s, user, f) 520 isCollaborator := roles.IsCollaborator() 521 isPullAuthor := user.Did == pull.OwnerDid 522 isCloseAllowed := isCollaborator || isPullAuthor 523 if !isCloseAllowed { 524 log.Println("failed to close pull") 525 s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.") 526 return 527 } 528 529 // Start a transaction 530 tx, err := s.db.BeginTx(r.Context(), nil) 531 if err != nil { 532 log.Println("failed to start transaction", err) 533 s.pages.Notice(w, "pull-close", "Failed to close pull.") 534 return 535 } 536 537 // Close the pull in the database 538 err = db.ClosePull(tx, f.RepoAt, pull.PullId) 539 if err != nil { 540 log.Println("failed to close pull", err) 541 s.pages.Notice(w, "pull-close", "Failed to close pull.") 542 return 543 } 544 545 // Commit the transaction 546 if err = tx.Commit(); err != nil { 547 log.Println("failed to commit transaction", err) 548 s.pages.Notice(w, "pull-close", "Failed to close pull.") 549 return 550 } 551 552 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 553 return 554} 555 556func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) { 557 user := s.auth.GetUser(r) 558 559 f, err := fullyResolvedRepo(r) 560 if err != nil { 561 log.Println("failed to resolve repo", err) 562 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 563 return 564 } 565 566 pull, ok := r.Context().Value("pull").(*db.Pull) 567 if !ok { 568 log.Println("failed to get pull") 569 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 570 return 571 } 572 573 // auth filter: only owner or collaborators can close 574 roles := RolesInRepo(s, user, f) 575 isCollaborator := roles.IsCollaborator() 576 isPullAuthor := user.Did == pull.OwnerDid 577 isCloseAllowed := isCollaborator || isPullAuthor 578 if !isCloseAllowed { 579 log.Println("failed to close pull") 580 s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.") 581 return 582 } 583 584 // Start a transaction 585 tx, err := s.db.BeginTx(r.Context(), nil) 586 if err != nil { 587 log.Println("failed to start transaction", err) 588 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 589 return 590 } 591 592 // Reopen the pull in the database 593 err = db.ReopenPull(tx, f.RepoAt, pull.PullId) 594 if err != nil { 595 log.Println("failed to reopen pull", err) 596 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 597 return 598 } 599 600 // Commit the transaction 601 if err = tx.Commit(); err != nil { 602 log.Println("failed to commit transaction", err) 603 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 604 return 605 } 606 607 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 608 return 609}