Monorepo for Tangled tangled.org

appview: pulls: refactor pulls into its own package

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.sh>

anirudh.fi d4332d6d e1ab0187

verified
Changed files
+69 -2142
appview
middleware
pulls
state
+2 -2
appview/middleware/middleware.go
··· 24 24 type Middleware struct { 25 25 oauth *oauth.OAuth 26 26 db *db.DB 27 - enforcer rbac.Enforcer 27 + enforcer *rbac.Enforcer 28 28 repoResolver *reporesolver.RepoResolver 29 29 resolver *appview.Resolver 30 30 pages *pages.Pages 31 31 } 32 32 33 - func New(oauth *oauth.OAuth, db *db.DB, enforcer rbac.Enforcer, repoResolver *reporesolver.RepoResolver, resolver *appview.Resolver, pages *pages.Pages) Middleware { 33 + func New(oauth *oauth.OAuth, db *db.DB, enforcer *rbac.Enforcer, repoResolver *reporesolver.RepoResolver, resolver *appview.Resolver, pages *pages.Pages) Middleware { 34 34 return Middleware{ 35 35 oauth: oauth, 36 36 db: db,
+59
appview/pulls/router.go
··· 1 + package pulls 2 + 3 + import ( 4 + "net/http" 5 + 6 + "github.com/go-chi/chi/v5" 7 + "tangled.sh/tangled.sh/core/appview/middleware" 8 + ) 9 + 10 + func (s *Pulls) Router(mw *middleware.Middleware) http.Handler { 11 + r := chi.NewRouter() 12 + r.Get("/", s.RepoPulls) 13 + r.With(middleware.AuthMiddleware(s.oauth)).Route("/new", func(r chi.Router) { 14 + r.Get("/", s.NewPull) 15 + r.Get("/patch-upload", s.PatchUploadFragment) 16 + r.Post("/validate-patch", s.ValidatePatch) 17 + r.Get("/compare-branches", s.CompareBranchesFragment) 18 + r.Get("/compare-forks", s.CompareForksFragment) 19 + r.Get("/fork-branches", s.CompareForksBranchesFragment) 20 + r.Post("/", s.NewPull) 21 + }) 22 + 23 + r.Route("/{pull}", func(r chi.Router) { 24 + r.Use(mw.ResolvePull()) 25 + r.Get("/", s.RepoSinglePull) 26 + 27 + r.Route("/round/{round}", func(r chi.Router) { 28 + r.Get("/", s.RepoPullPatch) 29 + r.Get("/interdiff", s.RepoPullInterdiff) 30 + r.Get("/actions", s.PullActions) 31 + r.With(middleware.AuthMiddleware(s.oauth)).Route("/comment", func(r chi.Router) { 32 + r.Get("/", s.PullComment) 33 + r.Post("/", s.PullComment) 34 + }) 35 + }) 36 + 37 + r.Route("/round/{round}.patch", func(r chi.Router) { 38 + r.Get("/", s.RepoPullPatchRaw) 39 + }) 40 + 41 + r.Group(func(r chi.Router) { 42 + r.Use(middleware.AuthMiddleware(s.oauth)) 43 + r.Route("/resubmit", func(r chi.Router) { 44 + r.Get("/", s.ResubmitPull) 45 + r.Post("/", s.ResubmitPull) 46 + }) 47 + r.Post("/close", s.ClosePull) 48 + r.Post("/reopen", s.ReopenPull) 49 + // collaborators only 50 + r.Group(func(r chi.Router) { 51 + r.Use(mw.RepoPermissionMiddleware("repo:push")) 52 + r.Post("/merge", s.MergePull) 53 + // maybe lock, etc. 54 + }) 55 + }) 56 + }) 57 + return r 58 + 59 + }
-2093
appview/state/pull.go
··· 1 - package state 2 - 3 - import ( 4 - "database/sql" 5 - "encoding/json" 6 - "errors" 7 - "fmt" 8 - "io" 9 - "log" 10 - "net/http" 11 - "sort" 12 - "strconv" 13 - "strings" 14 - "time" 15 - 16 - "tangled.sh/tangled.sh/core/api/tangled" 17 - "tangled.sh/tangled.sh/core/appview" 18 - "tangled.sh/tangled.sh/core/appview/db" 19 - "tangled.sh/tangled.sh/core/appview/oauth" 20 - "tangled.sh/tangled.sh/core/appview/pages" 21 - "tangled.sh/tangled.sh/core/appview/reporesolver" 22 - "tangled.sh/tangled.sh/core/knotclient" 23 - "tangled.sh/tangled.sh/core/patchutil" 24 - "tangled.sh/tangled.sh/core/types" 25 - 26 - "github.com/bluekeyes/go-gitdiff/gitdiff" 27 - comatproto "github.com/bluesky-social/indigo/api/atproto" 28 - "github.com/bluesky-social/indigo/atproto/syntax" 29 - lexutil "github.com/bluesky-social/indigo/lex/util" 30 - "github.com/go-chi/chi/v5" 31 - "github.com/google/uuid" 32 - "github.com/posthog/posthog-go" 33 - ) 34 - 35 - // htmx fragment 36 - func (s *State) PullActions(w http.ResponseWriter, r *http.Request) { 37 - switch r.Method { 38 - case http.MethodGet: 39 - user := s.oauth.GetUser(r) 40 - f, err := s.repoResolver.Resolve(r) 41 - if err != nil { 42 - log.Println("failed to get repo and knot", err) 43 - return 44 - } 45 - 46 - pull, ok := r.Context().Value("pull").(*db.Pull) 47 - if !ok { 48 - log.Println("failed to get pull") 49 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 50 - return 51 - } 52 - 53 - // can be nil if this pull is not stacked 54 - stack, _ := r.Context().Value("stack").(db.Stack) 55 - 56 - roundNumberStr := chi.URLParam(r, "round") 57 - roundNumber, err := strconv.Atoi(roundNumberStr) 58 - if err != nil { 59 - roundNumber = pull.LastRoundNumber() 60 - } 61 - if roundNumber >= len(pull.Submissions) { 62 - http.Error(w, "bad round id", http.StatusBadRequest) 63 - log.Println("failed to parse round id", err) 64 - return 65 - } 66 - 67 - mergeCheckResponse := s.mergeCheck(f, pull, stack) 68 - resubmitResult := pages.Unknown 69 - if user.Did == pull.OwnerDid { 70 - resubmitResult = s.resubmitCheck(f, pull, stack) 71 - } 72 - 73 - s.pages.PullActionsFragment(w, pages.PullActionsParams{ 74 - LoggedInUser: user, 75 - RepoInfo: f.RepoInfo(user), 76 - Pull: pull, 77 - RoundNumber: roundNumber, 78 - MergeCheck: mergeCheckResponse, 79 - ResubmitCheck: resubmitResult, 80 - Stack: stack, 81 - }) 82 - return 83 - } 84 - } 85 - 86 - func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) { 87 - user := s.oauth.GetUser(r) 88 - f, err := s.repoResolver.Resolve(r) 89 - if err != nil { 90 - log.Println("failed to get repo and knot", err) 91 - return 92 - } 93 - 94 - pull, ok := r.Context().Value("pull").(*db.Pull) 95 - if !ok { 96 - log.Println("failed to get pull") 97 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 98 - return 99 - } 100 - 101 - // can be nil if this pull is not stacked 102 - stack, _ := r.Context().Value("stack").(db.Stack) 103 - abandonedPulls, _ := r.Context().Value("abandonedPulls").([]*db.Pull) 104 - 105 - totalIdents := 1 106 - for _, submission := range pull.Submissions { 107 - totalIdents += len(submission.Comments) 108 - } 109 - 110 - identsToResolve := make([]string, totalIdents) 111 - 112 - // populate idents 113 - identsToResolve[0] = pull.OwnerDid 114 - idx := 1 115 - for _, submission := range pull.Submissions { 116 - for _, comment := range submission.Comments { 117 - identsToResolve[idx] = comment.OwnerDid 118 - idx += 1 119 - } 120 - } 121 - 122 - resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 123 - didHandleMap := make(map[string]string) 124 - for _, identity := range resolvedIds { 125 - if !identity.Handle.IsInvalidHandle() { 126 - didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 127 - } else { 128 - didHandleMap[identity.DID.String()] = identity.DID.String() 129 - } 130 - } 131 - 132 - mergeCheckResponse := s.mergeCheck(f, pull, stack) 133 - resubmitResult := pages.Unknown 134 - if user != nil && user.Did == pull.OwnerDid { 135 - resubmitResult = s.resubmitCheck(f, pull, stack) 136 - } 137 - 138 - s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ 139 - LoggedInUser: user, 140 - RepoInfo: f.RepoInfo(user), 141 - DidHandleMap: didHandleMap, 142 - Pull: pull, 143 - Stack: stack, 144 - AbandonedPulls: abandonedPulls, 145 - MergeCheck: mergeCheckResponse, 146 - ResubmitCheck: resubmitResult, 147 - }) 148 - } 149 - 150 - func (s *State) mergeCheck(f *reporesolver.ResolvedRepo, pull *db.Pull, stack db.Stack) types.MergeCheckResponse { 151 - if pull.State == db.PullMerged { 152 - return types.MergeCheckResponse{} 153 - } 154 - 155 - secret, err := db.GetRegistrationKey(s.db, f.Knot) 156 - if err != nil { 157 - log.Printf("failed to get registration key: %v", err) 158 - return types.MergeCheckResponse{ 159 - Error: "failed to check merge status: this knot is unregistered", 160 - } 161 - } 162 - 163 - ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 164 - if err != nil { 165 - log.Printf("failed to setup signed client for %s; ignoring: %v", f.Knot, err) 166 - return types.MergeCheckResponse{ 167 - Error: "failed to check merge status", 168 - } 169 - } 170 - 171 - patch := pull.LatestPatch() 172 - if pull.IsStacked() { 173 - // combine patches of substack 174 - subStack := stack.Below(pull) 175 - // collect the portion of the stack that is mergeable 176 - mergeable := subStack.Mergeable() 177 - // combine each patch 178 - patch = mergeable.CombinedPatch() 179 - } 180 - 181 - resp, err := ksClient.MergeCheck([]byte(patch), f.OwnerDid(), f.RepoName, pull.TargetBranch) 182 - if err != nil { 183 - log.Println("failed to check for mergeability:", err) 184 - return types.MergeCheckResponse{ 185 - Error: "failed to check merge status", 186 - } 187 - } 188 - switch resp.StatusCode { 189 - case 404: 190 - return types.MergeCheckResponse{ 191 - Error: "failed to check merge status: this knot does not support PRs", 192 - } 193 - case 400: 194 - return types.MergeCheckResponse{ 195 - Error: "failed to check merge status: does this knot support PRs?", 196 - } 197 - } 198 - 199 - respBody, err := io.ReadAll(resp.Body) 200 - if err != nil { 201 - log.Println("failed to read merge check response body") 202 - return types.MergeCheckResponse{ 203 - Error: "failed to check merge status: knot is not speaking the right language", 204 - } 205 - } 206 - defer resp.Body.Close() 207 - 208 - var mergeCheckResponse types.MergeCheckResponse 209 - err = json.Unmarshal(respBody, &mergeCheckResponse) 210 - if err != nil { 211 - log.Println("failed to unmarshal merge check response", err) 212 - return types.MergeCheckResponse{ 213 - Error: "failed to check merge status: knot is not speaking the right language", 214 - } 215 - } 216 - 217 - return mergeCheckResponse 218 - } 219 - 220 - func (s *State) resubmitCheck(f *reporesolver.ResolvedRepo, pull *db.Pull, stack db.Stack) pages.ResubmitResult { 221 - if pull.State == db.PullMerged || pull.State == db.PullDeleted || pull.PullSource == nil { 222 - return pages.Unknown 223 - } 224 - 225 - var knot, ownerDid, repoName string 226 - 227 - if pull.PullSource.RepoAt != nil { 228 - // fork-based pulls 229 - sourceRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String()) 230 - if err != nil { 231 - log.Println("failed to get source repo", err) 232 - return pages.Unknown 233 - } 234 - 235 - knot = sourceRepo.Knot 236 - ownerDid = sourceRepo.Did 237 - repoName = sourceRepo.Name 238 - } else { 239 - // pulls within the same repo 240 - knot = f.Knot 241 - ownerDid = f.OwnerDid() 242 - repoName = f.RepoName 243 - } 244 - 245 - us, err := knotclient.NewUnsignedClient(knot, s.config.Core.Dev) 246 - if err != nil { 247 - log.Printf("failed to setup client for %s; ignoring: %v", knot, err) 248 - return pages.Unknown 249 - } 250 - 251 - result, err := us.Branch(ownerDid, repoName, pull.PullSource.Branch) 252 - if err != nil { 253 - log.Println("failed to reach knotserver", err) 254 - return pages.Unknown 255 - } 256 - 257 - latestSourceRev := pull.Submissions[pull.LastRoundNumber()].SourceRev 258 - 259 - if pull.IsStacked() && stack != nil { 260 - top := stack[0] 261 - latestSourceRev = top.Submissions[top.LastRoundNumber()].SourceRev 262 - } 263 - 264 - log.Println(latestSourceRev, result.Branch.Hash) 265 - 266 - if latestSourceRev != result.Branch.Hash { 267 - return pages.ShouldResubmit 268 - } 269 - 270 - return pages.ShouldNotResubmit 271 - } 272 - 273 - func (s *State) RepoPullPatch(w http.ResponseWriter, r *http.Request) { 274 - user := s.oauth.GetUser(r) 275 - f, err := s.repoResolver.Resolve(r) 276 - if err != nil { 277 - log.Println("failed to get repo and knot", err) 278 - return 279 - } 280 - 281 - pull, ok := r.Context().Value("pull").(*db.Pull) 282 - if !ok { 283 - log.Println("failed to get pull") 284 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 285 - return 286 - } 287 - 288 - stack, _ := r.Context().Value("stack").(db.Stack) 289 - 290 - roundId := chi.URLParam(r, "round") 291 - roundIdInt, err := strconv.Atoi(roundId) 292 - if err != nil || roundIdInt >= len(pull.Submissions) { 293 - http.Error(w, "bad round id", http.StatusBadRequest) 294 - log.Println("failed to parse round id", err) 295 - return 296 - } 297 - 298 - identsToResolve := []string{pull.OwnerDid} 299 - resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 300 - didHandleMap := make(map[string]string) 301 - for _, identity := range resolvedIds { 302 - if !identity.Handle.IsInvalidHandle() { 303 - didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 304 - } else { 305 - didHandleMap[identity.DID.String()] = identity.DID.String() 306 - } 307 - } 308 - 309 - patch := pull.Submissions[roundIdInt].Patch 310 - diff := patchutil.AsNiceDiff(patch, pull.TargetBranch) 311 - 312 - s.pages.RepoPullPatchPage(w, pages.RepoPullPatchParams{ 313 - LoggedInUser: user, 314 - DidHandleMap: didHandleMap, 315 - RepoInfo: f.RepoInfo(user), 316 - Pull: pull, 317 - Stack: stack, 318 - Round: roundIdInt, 319 - Submission: pull.Submissions[roundIdInt], 320 - Diff: &diff, 321 - }) 322 - 323 - } 324 - 325 - func (s *State) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) { 326 - user := s.oauth.GetUser(r) 327 - 328 - f, err := s.repoResolver.Resolve(r) 329 - if err != nil { 330 - log.Println("failed to get repo and knot", err) 331 - return 332 - } 333 - 334 - pull, ok := r.Context().Value("pull").(*db.Pull) 335 - if !ok { 336 - log.Println("failed to get pull") 337 - s.pages.Notice(w, "pull-error", "Failed to get pull.") 338 - return 339 - } 340 - 341 - roundId := chi.URLParam(r, "round") 342 - roundIdInt, err := strconv.Atoi(roundId) 343 - if err != nil || roundIdInt >= len(pull.Submissions) { 344 - http.Error(w, "bad round id", http.StatusBadRequest) 345 - log.Println("failed to parse round id", err) 346 - return 347 - } 348 - 349 - if roundIdInt == 0 { 350 - http.Error(w, "bad round id", http.StatusBadRequest) 351 - log.Println("cannot interdiff initial submission") 352 - return 353 - } 354 - 355 - identsToResolve := []string{pull.OwnerDid} 356 - resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 357 - didHandleMap := make(map[string]string) 358 - for _, identity := range resolvedIds { 359 - if !identity.Handle.IsInvalidHandle() { 360 - didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 361 - } else { 362 - didHandleMap[identity.DID.String()] = identity.DID.String() 363 - } 364 - } 365 - 366 - currentPatch, err := patchutil.AsDiff(pull.Submissions[roundIdInt].Patch) 367 - if err != nil { 368 - log.Println("failed to interdiff; current patch malformed") 369 - s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; current patch is invalid.") 370 - return 371 - } 372 - 373 - previousPatch, err := patchutil.AsDiff(pull.Submissions[roundIdInt-1].Patch) 374 - if err != nil { 375 - log.Println("failed to interdiff; previous patch malformed") 376 - s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; previous patch is invalid.") 377 - return 378 - } 379 - 380 - interdiff := patchutil.Interdiff(previousPatch, currentPatch) 381 - 382 - s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{ 383 - LoggedInUser: s.oauth.GetUser(r), 384 - RepoInfo: f.RepoInfo(user), 385 - Pull: pull, 386 - Round: roundIdInt, 387 - DidHandleMap: didHandleMap, 388 - Interdiff: interdiff, 389 - }) 390 - return 391 - } 392 - 393 - func (s *State) RepoPullPatchRaw(w http.ResponseWriter, r *http.Request) { 394 - pull, ok := r.Context().Value("pull").(*db.Pull) 395 - if !ok { 396 - log.Println("failed to get pull") 397 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 398 - return 399 - } 400 - 401 - roundId := chi.URLParam(r, "round") 402 - roundIdInt, err := strconv.Atoi(roundId) 403 - if err != nil || roundIdInt >= len(pull.Submissions) { 404 - http.Error(w, "bad round id", http.StatusBadRequest) 405 - log.Println("failed to parse round id", err) 406 - return 407 - } 408 - 409 - identsToResolve := []string{pull.OwnerDid} 410 - resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 411 - didHandleMap := make(map[string]string) 412 - for _, identity := range resolvedIds { 413 - if !identity.Handle.IsInvalidHandle() { 414 - didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 415 - } else { 416 - didHandleMap[identity.DID.String()] = identity.DID.String() 417 - } 418 - } 419 - 420 - w.Header().Set("Content-Type", "text/plain") 421 - w.Write([]byte(pull.Submissions[roundIdInt].Patch)) 422 - } 423 - 424 - func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) { 425 - user := s.oauth.GetUser(r) 426 - params := r.URL.Query() 427 - 428 - state := db.PullOpen 429 - switch params.Get("state") { 430 - case "closed": 431 - state = db.PullClosed 432 - case "merged": 433 - state = db.PullMerged 434 - } 435 - 436 - f, err := s.repoResolver.Resolve(r) 437 - if err != nil { 438 - log.Println("failed to get repo and knot", err) 439 - return 440 - } 441 - 442 - pulls, err := db.GetPulls( 443 - s.db, 444 - db.FilterEq("repo_at", f.RepoAt), 445 - db.FilterEq("state", state), 446 - ) 447 - if err != nil { 448 - log.Println("failed to get pulls", err) 449 - s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.") 450 - return 451 - } 452 - 453 - for _, p := range pulls { 454 - var pullSourceRepo *db.Repo 455 - if p.PullSource != nil { 456 - if p.PullSource.RepoAt != nil { 457 - pullSourceRepo, err = db.GetRepoByAtUri(s.db, p.PullSource.RepoAt.String()) 458 - if err != nil { 459 - log.Printf("failed to get repo by at uri: %v", err) 460 - continue 461 - } else { 462 - p.PullSource.Repo = pullSourceRepo 463 - } 464 - } 465 - } 466 - } 467 - 468 - identsToResolve := make([]string, len(pulls)) 469 - for i, pull := range pulls { 470 - identsToResolve[i] = pull.OwnerDid 471 - } 472 - resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 473 - didHandleMap := make(map[string]string) 474 - for _, identity := range resolvedIds { 475 - if !identity.Handle.IsInvalidHandle() { 476 - didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 477 - } else { 478 - didHandleMap[identity.DID.String()] = identity.DID.String() 479 - } 480 - } 481 - 482 - s.pages.RepoPulls(w, pages.RepoPullsParams{ 483 - LoggedInUser: s.oauth.GetUser(r), 484 - RepoInfo: f.RepoInfo(user), 485 - Pulls: pulls, 486 - DidHandleMap: didHandleMap, 487 - FilteringBy: state, 488 - }) 489 - return 490 - } 491 - 492 - func (s *State) PullComment(w http.ResponseWriter, r *http.Request) { 493 - user := s.oauth.GetUser(r) 494 - f, err := s.repoResolver.Resolve(r) 495 - if err != nil { 496 - log.Println("failed to get repo and knot", err) 497 - return 498 - } 499 - 500 - pull, ok := r.Context().Value("pull").(*db.Pull) 501 - if !ok { 502 - log.Println("failed to get pull") 503 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 504 - return 505 - } 506 - 507 - roundNumberStr := chi.URLParam(r, "round") 508 - roundNumber, err := strconv.Atoi(roundNumberStr) 509 - if err != nil || roundNumber >= len(pull.Submissions) { 510 - http.Error(w, "bad round id", http.StatusBadRequest) 511 - log.Println("failed to parse round id", err) 512 - return 513 - } 514 - 515 - switch r.Method { 516 - case http.MethodGet: 517 - s.pages.PullNewCommentFragment(w, pages.PullNewCommentParams{ 518 - LoggedInUser: user, 519 - RepoInfo: f.RepoInfo(user), 520 - Pull: pull, 521 - RoundNumber: roundNumber, 522 - }) 523 - return 524 - case http.MethodPost: 525 - body := r.FormValue("body") 526 - if body == "" { 527 - s.pages.Notice(w, "pull", "Comment body is required") 528 - return 529 - } 530 - 531 - // Start a transaction 532 - tx, err := s.db.BeginTx(r.Context(), nil) 533 - if err != nil { 534 - log.Println("failed to start transaction", err) 535 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 536 - return 537 - } 538 - defer tx.Rollback() 539 - 540 - createdAt := time.Now().Format(time.RFC3339) 541 - ownerDid := user.Did 542 - 543 - pullAt, err := db.GetPullAt(s.db, f.RepoAt, pull.PullId) 544 - if err != nil { 545 - log.Println("failed to get pull at", err) 546 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 547 - return 548 - } 549 - 550 - atUri := f.RepoAt.String() 551 - client, err := s.oauth.AuthorizedClient(r) 552 - if err != nil { 553 - log.Println("failed to get authorized client", err) 554 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 555 - return 556 - } 557 - atResp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 558 - Collection: tangled.RepoPullCommentNSID, 559 - Repo: user.Did, 560 - Rkey: appview.TID(), 561 - Record: &lexutil.LexiconTypeDecoder{ 562 - Val: &tangled.RepoPullComment{ 563 - Repo: &atUri, 564 - Pull: string(pullAt), 565 - Owner: &ownerDid, 566 - Body: body, 567 - CreatedAt: createdAt, 568 - }, 569 - }, 570 - }) 571 - if err != nil { 572 - log.Println("failed to create pull comment", err) 573 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 574 - return 575 - } 576 - 577 - // Create the pull comment in the database with the commentAt field 578 - commentId, err := db.NewPullComment(tx, &db.PullComment{ 579 - OwnerDid: user.Did, 580 - RepoAt: f.RepoAt.String(), 581 - PullId: pull.PullId, 582 - Body: body, 583 - CommentAt: atResp.Uri, 584 - SubmissionId: pull.Submissions[roundNumber].ID, 585 - }) 586 - if err != nil { 587 - log.Println("failed to create pull comment", err) 588 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 589 - return 590 - } 591 - 592 - // Commit the transaction 593 - if err = tx.Commit(); err != nil { 594 - log.Println("failed to commit transaction", err) 595 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 596 - return 597 - } 598 - 599 - if !s.config.Core.Dev { 600 - err = s.posthog.Enqueue(posthog.Capture{ 601 - DistinctId: user.Did, 602 - Event: "new_pull_comment", 603 - Properties: posthog.Properties{"repo_at": f.RepoAt.String(), "pull_id": pull.PullId}, 604 - }) 605 - if err != nil { 606 - log.Println("failed to enqueue posthog event:", err) 607 - } 608 - } 609 - 610 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", f.OwnerSlashRepo(), pull.PullId, commentId)) 611 - return 612 - } 613 - } 614 - 615 - func (s *State) NewPull(w http.ResponseWriter, r *http.Request) { 616 - user := s.oauth.GetUser(r) 617 - f, err := s.repoResolver.Resolve(r) 618 - if err != nil { 619 - log.Println("failed to get repo and knot", err) 620 - return 621 - } 622 - 623 - switch r.Method { 624 - case http.MethodGet: 625 - us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 626 - if err != nil { 627 - log.Printf("failed to create unsigned client for %s", f.Knot) 628 - s.pages.Error503(w) 629 - return 630 - } 631 - 632 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 633 - if err != nil { 634 - log.Println("failed to fetch branches", err) 635 - return 636 - } 637 - 638 - // can be one of "patch", "branch" or "fork" 639 - strategy := r.URL.Query().Get("strategy") 640 - // ignored if strategy is "patch" 641 - sourceBranch := r.URL.Query().Get("sourceBranch") 642 - targetBranch := r.URL.Query().Get("targetBranch") 643 - 644 - s.pages.RepoNewPull(w, pages.RepoNewPullParams{ 645 - LoggedInUser: user, 646 - RepoInfo: f.RepoInfo(user), 647 - Branches: result.Branches, 648 - Strategy: strategy, 649 - SourceBranch: sourceBranch, 650 - TargetBranch: targetBranch, 651 - Title: r.URL.Query().Get("title"), 652 - Body: r.URL.Query().Get("body"), 653 - }) 654 - 655 - case http.MethodPost: 656 - title := r.FormValue("title") 657 - body := r.FormValue("body") 658 - targetBranch := r.FormValue("targetBranch") 659 - fromFork := r.FormValue("fork") 660 - sourceBranch := r.FormValue("sourceBranch") 661 - patch := r.FormValue("patch") 662 - 663 - if targetBranch == "" { 664 - s.pages.Notice(w, "pull", "Target branch is required.") 665 - return 666 - } 667 - 668 - // Determine PR type based on input parameters 669 - isPushAllowed := f.RepoInfo(user).Roles.IsPushAllowed() 670 - isBranchBased := isPushAllowed && sourceBranch != "" && fromFork == "" 671 - isForkBased := fromFork != "" && sourceBranch != "" 672 - isPatchBased := patch != "" && !isBranchBased && !isForkBased 673 - isStacked := r.FormValue("isStacked") == "on" 674 - 675 - if isPatchBased && !patchutil.IsFormatPatch(patch) { 676 - if title == "" { 677 - s.pages.Notice(w, "pull", "Title is required for git-diff patches.") 678 - return 679 - } 680 - } 681 - 682 - // Validate we have at least one valid PR creation method 683 - if !isBranchBased && !isPatchBased && !isForkBased { 684 - s.pages.Notice(w, "pull", "Neither source branch nor patch supplied.") 685 - return 686 - } 687 - 688 - // Can't mix branch-based and patch-based approaches 689 - if isBranchBased && patch != "" { 690 - s.pages.Notice(w, "pull", "Cannot select both patch and source branch.") 691 - return 692 - } 693 - 694 - us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 695 - if err != nil { 696 - log.Printf("failed to create unsigned client to %s: %v", f.Knot, err) 697 - s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.") 698 - return 699 - } 700 - 701 - caps, err := us.Capabilities() 702 - if err != nil { 703 - log.Println("error fetching knot caps", f.Knot, err) 704 - s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.") 705 - return 706 - } 707 - 708 - if !caps.PullRequests.FormatPatch { 709 - s.pages.Notice(w, "pull", "This knot doesn't support format-patch. Unfortunately, there is no fallback for now.") 710 - return 711 - } 712 - 713 - // Handle the PR creation based on the type 714 - if isBranchBased { 715 - if !caps.PullRequests.BranchSubmissions { 716 - s.pages.Notice(w, "pull", "This knot doesn't support branch-based pull requests. Try another way?") 717 - return 718 - } 719 - s.handleBranchBasedPull(w, r, f, user, title, body, targetBranch, sourceBranch, isStacked) 720 - } else if isForkBased { 721 - if !caps.PullRequests.ForkSubmissions { 722 - s.pages.Notice(w, "pull", "This knot doesn't support fork-based pull requests. Try another way?") 723 - return 724 - } 725 - s.handleForkBasedPull(w, r, f, user, fromFork, title, body, targetBranch, sourceBranch, isStacked) 726 - } else if isPatchBased { 727 - if !caps.PullRequests.PatchSubmissions { 728 - s.pages.Notice(w, "pull", "This knot doesn't support patch-based pull requests. Send your patch over email.") 729 - return 730 - } 731 - s.handlePatchBasedPull(w, r, f, user, title, body, targetBranch, patch, isStacked) 732 - } 733 - return 734 - } 735 - } 736 - 737 - func (s *State) handleBranchBasedPull( 738 - w http.ResponseWriter, 739 - r *http.Request, 740 - f *reporesolver.ResolvedRepo, 741 - user *oauth.User, 742 - title, 743 - body, 744 - targetBranch, 745 - sourceBranch string, 746 - isStacked bool, 747 - ) { 748 - pullSource := &db.PullSource{ 749 - Branch: sourceBranch, 750 - } 751 - recordPullSource := &tangled.RepoPull_Source{ 752 - Branch: sourceBranch, 753 - } 754 - 755 - // Generate a patch using /compare 756 - ksClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 757 - if err != nil { 758 - log.Printf("failed to create signed client for %s: %s", f.Knot, err) 759 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 760 - return 761 - } 762 - 763 - comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch) 764 - if err != nil { 765 - log.Println("failed to compare", err) 766 - s.pages.Notice(w, "pull", err.Error()) 767 - return 768 - } 769 - 770 - sourceRev := comparison.Rev2 771 - patch := comparison.Patch 772 - 773 - if !patchutil.IsPatchValid(patch) { 774 - s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 775 - return 776 - } 777 - 778 - s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource, isStacked) 779 - } 780 - 781 - func (s *State) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, f *reporesolver.ResolvedRepo, user *oauth.User, title, body, targetBranch, patch string, isStacked bool) { 782 - if !patchutil.IsPatchValid(patch) { 783 - s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 784 - return 785 - } 786 - 787 - s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, "", nil, nil, isStacked) 788 - } 789 - 790 - func (s *State) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *reporesolver.ResolvedRepo, user *oauth.User, forkRepo string, title, body, targetBranch, sourceBranch string, isStacked bool) { 791 - fork, err := db.GetForkByDid(s.db, user.Did, forkRepo) 792 - if errors.Is(err, sql.ErrNoRows) { 793 - s.pages.Notice(w, "pull", "No such fork.") 794 - return 795 - } else if err != nil { 796 - log.Println("failed to fetch fork:", err) 797 - s.pages.Notice(w, "pull", "Failed to fetch fork.") 798 - return 799 - } 800 - 801 - secret, err := db.GetRegistrationKey(s.db, fork.Knot) 802 - if err != nil { 803 - log.Println("failed to fetch registration key:", err) 804 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 805 - return 806 - } 807 - 808 - sc, err := knotclient.NewSignedClient(fork.Knot, secret, s.config.Core.Dev) 809 - if err != nil { 810 - log.Println("failed to create signed client:", err) 811 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 812 - return 813 - } 814 - 815 - us, err := knotclient.NewUnsignedClient(fork.Knot, s.config.Core.Dev) 816 - if err != nil { 817 - log.Println("failed to create unsigned client:", err) 818 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 819 - return 820 - } 821 - 822 - resp, err := sc.NewHiddenRef(user.Did, fork.Name, sourceBranch, targetBranch) 823 - if err != nil { 824 - log.Println("failed to create hidden ref:", err, resp.StatusCode) 825 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 826 - return 827 - } 828 - 829 - switch resp.StatusCode { 830 - case 404: 831 - case 400: 832 - s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.") 833 - return 834 - } 835 - 836 - hiddenRef := fmt.Sprintf("hidden/%s/%s", sourceBranch, targetBranch) 837 - // We're now comparing the sourceBranch (on the fork) against the hiddenRef which is tracking 838 - // the targetBranch on the target repository. This code is a bit confusing, but here's an example: 839 - // hiddenRef: hidden/feature-1/main (on repo-fork) 840 - // targetBranch: main (on repo-1) 841 - // sourceBranch: feature-1 (on repo-fork) 842 - comparison, err := us.Compare(user.Did, fork.Name, hiddenRef, sourceBranch) 843 - if err != nil { 844 - log.Println("failed to compare across branches", err) 845 - s.pages.Notice(w, "pull", err.Error()) 846 - return 847 - } 848 - 849 - sourceRev := comparison.Rev2 850 - patch := comparison.Patch 851 - 852 - if !patchutil.IsPatchValid(patch) { 853 - s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 854 - return 855 - } 856 - 857 - forkAtUri, err := syntax.ParseATURI(fork.AtUri) 858 - if err != nil { 859 - log.Println("failed to parse fork AT URI", err) 860 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 861 - return 862 - } 863 - 864 - s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, &db.PullSource{ 865 - Branch: sourceBranch, 866 - RepoAt: &forkAtUri, 867 - }, &tangled.RepoPull_Source{Branch: sourceBranch, Repo: &fork.AtUri}, isStacked) 868 - } 869 - 870 - func (s *State) createPullRequest( 871 - w http.ResponseWriter, 872 - r *http.Request, 873 - f *reporesolver.ResolvedRepo, 874 - user *oauth.User, 875 - title, body, targetBranch string, 876 - patch string, 877 - sourceRev string, 878 - pullSource *db.PullSource, 879 - recordPullSource *tangled.RepoPull_Source, 880 - isStacked bool, 881 - ) { 882 - if isStacked { 883 - // creates a series of PRs, each linking to the previous, identified by jj's change-id 884 - s.createStackedPulLRequest( 885 - w, 886 - r, 887 - f, 888 - user, 889 - targetBranch, 890 - patch, 891 - sourceRev, 892 - pullSource, 893 - ) 894 - return 895 - } 896 - 897 - client, err := s.oauth.AuthorizedClient(r) 898 - if err != nil { 899 - log.Println("failed to get authorized client", err) 900 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 901 - return 902 - } 903 - 904 - tx, err := s.db.BeginTx(r.Context(), nil) 905 - if err != nil { 906 - log.Println("failed to start tx") 907 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 908 - return 909 - } 910 - defer tx.Rollback() 911 - 912 - // We've already checked earlier if it's diff-based and title is empty, 913 - // so if it's still empty now, it's intentionally skipped owing to format-patch. 914 - if title == "" { 915 - formatPatches, err := patchutil.ExtractPatches(patch) 916 - if err != nil { 917 - s.pages.Notice(w, "pull", fmt.Sprintf("Failed to extract patches: %v", err)) 918 - return 919 - } 920 - if len(formatPatches) == 0 { 921 - s.pages.Notice(w, "pull", "No patches found in the supplied format-patch.") 922 - return 923 - } 924 - 925 - title = formatPatches[0].Title 926 - body = formatPatches[0].Body 927 - } 928 - 929 - rkey := appview.TID() 930 - initialSubmission := db.PullSubmission{ 931 - Patch: patch, 932 - SourceRev: sourceRev, 933 - } 934 - err = db.NewPull(tx, &db.Pull{ 935 - Title: title, 936 - Body: body, 937 - TargetBranch: targetBranch, 938 - OwnerDid: user.Did, 939 - RepoAt: f.RepoAt, 940 - Rkey: rkey, 941 - Submissions: []*db.PullSubmission{ 942 - &initialSubmission, 943 - }, 944 - PullSource: pullSource, 945 - }) 946 - if err != nil { 947 - log.Println("failed to create pull request", err) 948 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 949 - return 950 - } 951 - pullId, err := db.NextPullId(tx, f.RepoAt) 952 - if err != nil { 953 - log.Println("failed to get pull id", err) 954 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 955 - return 956 - } 957 - 958 - _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 959 - Collection: tangled.RepoPullNSID, 960 - Repo: user.Did, 961 - Rkey: rkey, 962 - Record: &lexutil.LexiconTypeDecoder{ 963 - Val: &tangled.RepoPull{ 964 - Title: title, 965 - PullId: int64(pullId), 966 - TargetRepo: string(f.RepoAt), 967 - TargetBranch: targetBranch, 968 - Patch: patch, 969 - Source: recordPullSource, 970 - }, 971 - }, 972 - }) 973 - if err != nil { 974 - log.Println("failed to create pull request", err) 975 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 976 - return 977 - } 978 - 979 - if err = tx.Commit(); err != nil { 980 - log.Println("failed to create pull request", err) 981 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 982 - return 983 - } 984 - 985 - if !s.config.Core.Dev { 986 - err = s.posthog.Enqueue(posthog.Capture{ 987 - DistinctId: user.Did, 988 - Event: "new_pull", 989 - Properties: posthog.Properties{"repo_at": f.RepoAt.String(), "pull_id": pullId}, 990 - }) 991 - if err != nil { 992 - log.Println("failed to enqueue posthog event:", err) 993 - } 994 - } 995 - 996 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId)) 997 - } 998 - 999 - func (s *State) createStackedPulLRequest( 1000 - w http.ResponseWriter, 1001 - r *http.Request, 1002 - f *reporesolver.ResolvedRepo, 1003 - user *oauth.User, 1004 - targetBranch string, 1005 - patch string, 1006 - sourceRev string, 1007 - pullSource *db.PullSource, 1008 - ) { 1009 - // run some necessary checks for stacked-prs first 1010 - 1011 - // must be branch or fork based 1012 - if sourceRev == "" { 1013 - log.Println("stacked PR from patch-based pull") 1014 - s.pages.Notice(w, "pull", "Stacking is only supported on branch and fork based pull-requests.") 1015 - return 1016 - } 1017 - 1018 - formatPatches, err := patchutil.ExtractPatches(patch) 1019 - if err != nil { 1020 - log.Println("failed to extract patches", err) 1021 - s.pages.Notice(w, "pull", fmt.Sprintf("Failed to extract patches: %v", err)) 1022 - return 1023 - } 1024 - 1025 - // must have atleast 1 patch to begin with 1026 - if len(formatPatches) == 0 { 1027 - log.Println("empty patches") 1028 - s.pages.Notice(w, "pull", "No patches found in the generated format-patch.") 1029 - return 1030 - } 1031 - 1032 - // build a stack out of this patch 1033 - stackId := uuid.New() 1034 - stack, err := newStack(f, user, targetBranch, patch, pullSource, stackId.String()) 1035 - if err != nil { 1036 - log.Println("failed to create stack", err) 1037 - s.pages.Notice(w, "pull", fmt.Sprintf("Failed to create stack: %v", err)) 1038 - return 1039 - } 1040 - 1041 - client, err := s.oauth.AuthorizedClient(r) 1042 - if err != nil { 1043 - log.Println("failed to get authorized client", err) 1044 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1045 - return 1046 - } 1047 - 1048 - // apply all record creations at once 1049 - var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem 1050 - for _, p := range stack { 1051 - record := p.AsRecord() 1052 - write := comatproto.RepoApplyWrites_Input_Writes_Elem{ 1053 - RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{ 1054 - Collection: tangled.RepoPullNSID, 1055 - Rkey: &p.Rkey, 1056 - Value: &lexutil.LexiconTypeDecoder{ 1057 - Val: &record, 1058 - }, 1059 - }, 1060 - } 1061 - writes = append(writes, &write) 1062 - } 1063 - _, err = client.RepoApplyWrites(r.Context(), &comatproto.RepoApplyWrites_Input{ 1064 - Repo: user.Did, 1065 - Writes: writes, 1066 - }) 1067 - if err != nil { 1068 - log.Println("failed to create stacked pull request", err) 1069 - s.pages.Notice(w, "pull", "Failed to create stacked pull request. Try again later.") 1070 - return 1071 - } 1072 - 1073 - // create all pulls at once 1074 - tx, err := s.db.BeginTx(r.Context(), nil) 1075 - if err != nil { 1076 - log.Println("failed to start tx") 1077 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1078 - return 1079 - } 1080 - defer tx.Rollback() 1081 - 1082 - for _, p := range stack { 1083 - err = db.NewPull(tx, p) 1084 - if err != nil { 1085 - log.Println("failed to create pull request", err) 1086 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1087 - return 1088 - } 1089 - } 1090 - 1091 - if err = tx.Commit(); err != nil { 1092 - log.Println("failed to create pull request", err) 1093 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1094 - return 1095 - } 1096 - 1097 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls", f.OwnerSlashRepo())) 1098 - } 1099 - 1100 - func (s *State) ValidatePatch(w http.ResponseWriter, r *http.Request) { 1101 - _, err := s.repoResolver.Resolve(r) 1102 - if err != nil { 1103 - log.Println("failed to get repo and knot", err) 1104 - return 1105 - } 1106 - 1107 - patch := r.FormValue("patch") 1108 - if patch == "" { 1109 - s.pages.Notice(w, "patch-error", "Patch is required.") 1110 - return 1111 - } 1112 - 1113 - if patch == "" || !patchutil.IsPatchValid(patch) { 1114 - s.pages.Notice(w, "patch-error", "Invalid patch format. Please provide a valid git diff or format-patch.") 1115 - return 1116 - } 1117 - 1118 - if patchutil.IsFormatPatch(patch) { 1119 - s.pages.Notice(w, "patch-preview", "git-format-patch detected. Title and description are optional; if left out, they will be extracted from the first commit.") 1120 - } else { 1121 - s.pages.Notice(w, "patch-preview", "Regular git-diff detected. Please provide a title and description.") 1122 - } 1123 - } 1124 - 1125 - func (s *State) PatchUploadFragment(w http.ResponseWriter, r *http.Request) { 1126 - user := s.oauth.GetUser(r) 1127 - f, err := s.repoResolver.Resolve(r) 1128 - if err != nil { 1129 - log.Println("failed to get repo and knot", err) 1130 - return 1131 - } 1132 - 1133 - s.pages.PullPatchUploadFragment(w, pages.PullPatchUploadParams{ 1134 - RepoInfo: f.RepoInfo(user), 1135 - }) 1136 - } 1137 - 1138 - func (s *State) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) { 1139 - user := s.oauth.GetUser(r) 1140 - f, err := s.repoResolver.Resolve(r) 1141 - if err != nil { 1142 - log.Println("failed to get repo and knot", err) 1143 - return 1144 - } 1145 - 1146 - us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 1147 - if err != nil { 1148 - log.Printf("failed to create unsigned client for %s", f.Knot) 1149 - s.pages.Error503(w) 1150 - return 1151 - } 1152 - 1153 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 1154 - if err != nil { 1155 - log.Println("failed to reach knotserver", err) 1156 - return 1157 - } 1158 - 1159 - branches := result.Branches 1160 - sort.Slice(branches, func(i int, j int) bool { 1161 - return branches[i].Commit.Committer.When.After(branches[j].Commit.Committer.When) 1162 - }) 1163 - 1164 - withoutDefault := []types.Branch{} 1165 - for _, b := range branches { 1166 - if b.IsDefault { 1167 - continue 1168 - } 1169 - withoutDefault = append(withoutDefault, b) 1170 - } 1171 - 1172 - s.pages.PullCompareBranchesFragment(w, pages.PullCompareBranchesParams{ 1173 - RepoInfo: f.RepoInfo(user), 1174 - Branches: withoutDefault, 1175 - }) 1176 - } 1177 - 1178 - func (s *State) CompareForksFragment(w http.ResponseWriter, r *http.Request) { 1179 - user := s.oauth.GetUser(r) 1180 - f, err := s.repoResolver.Resolve(r) 1181 - if err != nil { 1182 - log.Println("failed to get repo and knot", err) 1183 - return 1184 - } 1185 - 1186 - forks, err := db.GetForksByDid(s.db, user.Did) 1187 - if err != nil { 1188 - log.Println("failed to get forks", err) 1189 - return 1190 - } 1191 - 1192 - s.pages.PullCompareForkFragment(w, pages.PullCompareForkParams{ 1193 - RepoInfo: f.RepoInfo(user), 1194 - Forks: forks, 1195 - Selected: r.URL.Query().Get("fork"), 1196 - }) 1197 - } 1198 - 1199 - func (s *State) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) { 1200 - user := s.oauth.GetUser(r) 1201 - 1202 - f, err := s.repoResolver.Resolve(r) 1203 - if err != nil { 1204 - log.Println("failed to get repo and knot", err) 1205 - return 1206 - } 1207 - 1208 - forkVal := r.URL.Query().Get("fork") 1209 - 1210 - // fork repo 1211 - repo, err := db.GetRepo(s.db, user.Did, forkVal) 1212 - if err != nil { 1213 - log.Println("failed to get repo", user.Did, forkVal) 1214 - return 1215 - } 1216 - 1217 - sourceBranchesClient, err := knotclient.NewUnsignedClient(repo.Knot, s.config.Core.Dev) 1218 - if err != nil { 1219 - log.Printf("failed to create unsigned client for %s", repo.Knot) 1220 - s.pages.Error503(w) 1221 - return 1222 - } 1223 - 1224 - sourceResult, err := sourceBranchesClient.Branches(user.Did, repo.Name) 1225 - if err != nil { 1226 - log.Println("failed to reach knotserver for source branches", err) 1227 - return 1228 - } 1229 - 1230 - targetBranchesClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 1231 - if err != nil { 1232 - log.Printf("failed to create unsigned client for target knot %s", f.Knot) 1233 - s.pages.Error503(w) 1234 - return 1235 - } 1236 - 1237 - targetResult, err := targetBranchesClient.Branches(f.OwnerDid(), f.RepoName) 1238 - if err != nil { 1239 - log.Println("failed to reach knotserver for target branches", err) 1240 - return 1241 - } 1242 - 1243 - sourceBranches := sourceResult.Branches 1244 - sort.Slice(sourceBranches, func(i int, j int) bool { 1245 - return sourceBranches[i].Commit.Committer.When.After(sourceBranches[j].Commit.Committer.When) 1246 - }) 1247 - 1248 - s.pages.PullCompareForkBranchesFragment(w, pages.PullCompareForkBranchesParams{ 1249 - RepoInfo: f.RepoInfo(user), 1250 - SourceBranches: sourceBranches, 1251 - TargetBranches: targetResult.Branches, 1252 - }) 1253 - } 1254 - 1255 - func (s *State) ResubmitPull(w http.ResponseWriter, r *http.Request) { 1256 - user := s.oauth.GetUser(r) 1257 - f, err := s.repoResolver.Resolve(r) 1258 - if err != nil { 1259 - log.Println("failed to get repo and knot", err) 1260 - return 1261 - } 1262 - 1263 - pull, ok := r.Context().Value("pull").(*db.Pull) 1264 - if !ok { 1265 - log.Println("failed to get pull") 1266 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1267 - return 1268 - } 1269 - 1270 - switch r.Method { 1271 - case http.MethodGet: 1272 - s.pages.PullResubmitFragment(w, pages.PullResubmitParams{ 1273 - RepoInfo: f.RepoInfo(user), 1274 - Pull: pull, 1275 - }) 1276 - return 1277 - case http.MethodPost: 1278 - if pull.IsPatchBased() { 1279 - s.resubmitPatch(w, r) 1280 - return 1281 - } else if pull.IsBranchBased() { 1282 - s.resubmitBranch(w, r) 1283 - return 1284 - } else if pull.IsForkBased() { 1285 - s.resubmitFork(w, r) 1286 - return 1287 - } 1288 - } 1289 - } 1290 - 1291 - func (s *State) resubmitPatch(w http.ResponseWriter, r *http.Request) { 1292 - user := s.oauth.GetUser(r) 1293 - 1294 - pull, ok := r.Context().Value("pull").(*db.Pull) 1295 - if !ok { 1296 - log.Println("failed to get pull") 1297 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1298 - return 1299 - } 1300 - 1301 - f, err := s.repoResolver.Resolve(r) 1302 - if err != nil { 1303 - log.Println("failed to get repo and knot", err) 1304 - return 1305 - } 1306 - 1307 - if user.Did != pull.OwnerDid { 1308 - log.Println("unauthorized user") 1309 - w.WriteHeader(http.StatusUnauthorized) 1310 - return 1311 - } 1312 - 1313 - patch := r.FormValue("patch") 1314 - 1315 - s.resubmitPullHelper(w, r, f, user, pull, patch, "") 1316 - } 1317 - 1318 - func (s *State) resubmitBranch(w http.ResponseWriter, r *http.Request) { 1319 - user := s.oauth.GetUser(r) 1320 - 1321 - pull, ok := r.Context().Value("pull").(*db.Pull) 1322 - if !ok { 1323 - log.Println("failed to get pull") 1324 - s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.") 1325 - return 1326 - } 1327 - 1328 - f, err := s.repoResolver.Resolve(r) 1329 - if err != nil { 1330 - log.Println("failed to get repo and knot", err) 1331 - return 1332 - } 1333 - 1334 - if user.Did != pull.OwnerDid { 1335 - log.Println("unauthorized user") 1336 - w.WriteHeader(http.StatusUnauthorized) 1337 - return 1338 - } 1339 - 1340 - if !f.RepoInfo(user).Roles.IsPushAllowed() { 1341 - log.Println("unauthorized user") 1342 - w.WriteHeader(http.StatusUnauthorized) 1343 - return 1344 - } 1345 - 1346 - ksClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 1347 - if err != nil { 1348 - log.Printf("failed to create client for %s: %s", f.Knot, err) 1349 - s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1350 - return 1351 - } 1352 - 1353 - comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.PullSource.Branch) 1354 - if err != nil { 1355 - log.Printf("compare request failed: %s", err) 1356 - s.pages.Notice(w, "resubmit-error", err.Error()) 1357 - return 1358 - } 1359 - 1360 - sourceRev := comparison.Rev2 1361 - patch := comparison.Patch 1362 - 1363 - s.resubmitPullHelper(w, r, f, user, pull, patch, sourceRev) 1364 - } 1365 - 1366 - func (s *State) resubmitFork(w http.ResponseWriter, r *http.Request) { 1367 - user := s.oauth.GetUser(r) 1368 - 1369 - pull, ok := r.Context().Value("pull").(*db.Pull) 1370 - if !ok { 1371 - log.Println("failed to get pull") 1372 - s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.") 1373 - return 1374 - } 1375 - 1376 - f, err := s.repoResolver.Resolve(r) 1377 - if err != nil { 1378 - log.Println("failed to get repo and knot", err) 1379 - return 1380 - } 1381 - 1382 - if user.Did != pull.OwnerDid { 1383 - log.Println("unauthorized user") 1384 - w.WriteHeader(http.StatusUnauthorized) 1385 - return 1386 - } 1387 - 1388 - forkRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String()) 1389 - if err != nil { 1390 - log.Println("failed to get source repo", err) 1391 - s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1392 - return 1393 - } 1394 - 1395 - // extract patch by performing compare 1396 - ksClient, err := knotclient.NewUnsignedClient(forkRepo.Knot, s.config.Core.Dev) 1397 - if err != nil { 1398 - log.Printf("failed to create client for %s: %s", forkRepo.Knot, err) 1399 - s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1400 - return 1401 - } 1402 - 1403 - secret, err := db.GetRegistrationKey(s.db, forkRepo.Knot) 1404 - if err != nil { 1405 - log.Printf("failed to get registration key for %s: %s", forkRepo.Knot, err) 1406 - s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1407 - return 1408 - } 1409 - 1410 - // update the hidden tracking branch to latest 1411 - signedClient, err := knotclient.NewSignedClient(forkRepo.Knot, secret, s.config.Core.Dev) 1412 - if err != nil { 1413 - log.Printf("failed to create signed client for %s: %s", forkRepo.Knot, err) 1414 - s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1415 - return 1416 - } 1417 - 1418 - resp, err := signedClient.NewHiddenRef(forkRepo.Did, forkRepo.Name, pull.PullSource.Branch, pull.TargetBranch) 1419 - if err != nil || resp.StatusCode != http.StatusNoContent { 1420 - log.Printf("failed to update tracking branch: %s", err) 1421 - s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1422 - return 1423 - } 1424 - 1425 - hiddenRef := fmt.Sprintf("hidden/%s/%s", pull.PullSource.Branch, pull.TargetBranch) 1426 - comparison, err := ksClient.Compare(forkRepo.Did, forkRepo.Name, hiddenRef, pull.PullSource.Branch) 1427 - if err != nil { 1428 - log.Printf("failed to compare branches: %s", err) 1429 - s.pages.Notice(w, "resubmit-error", err.Error()) 1430 - return 1431 - } 1432 - 1433 - sourceRev := comparison.Rev2 1434 - patch := comparison.Patch 1435 - 1436 - s.resubmitPullHelper(w, r, f, user, pull, patch, sourceRev) 1437 - } 1438 - 1439 - // validate a resubmission against a pull request 1440 - func validateResubmittedPatch(pull *db.Pull, patch string) error { 1441 - if patch == "" { 1442 - return fmt.Errorf("Patch is empty.") 1443 - } 1444 - 1445 - if patch == pull.LatestPatch() { 1446 - return fmt.Errorf("Patch is identical to previous submission.") 1447 - } 1448 - 1449 - if !patchutil.IsPatchValid(patch) { 1450 - return fmt.Errorf("Invalid patch format. Please provide a valid diff.") 1451 - } 1452 - 1453 - return nil 1454 - } 1455 - 1456 - func (s *State) resubmitPullHelper( 1457 - w http.ResponseWriter, 1458 - r *http.Request, 1459 - f *reporesolver.ResolvedRepo, 1460 - user *oauth.User, 1461 - pull *db.Pull, 1462 - patch string, 1463 - sourceRev string, 1464 - ) { 1465 - if pull.IsStacked() { 1466 - log.Println("resubmitting stacked PR") 1467 - s.resubmitStackedPullHelper(w, r, f, user, pull, patch, pull.StackId) 1468 - return 1469 - } 1470 - 1471 - if err := validateResubmittedPatch(pull, patch); err != nil { 1472 - s.pages.Notice(w, "resubmit-error", err.Error()) 1473 - return 1474 - } 1475 - 1476 - // validate sourceRev if branch/fork based 1477 - if pull.IsBranchBased() || pull.IsForkBased() { 1478 - if sourceRev == pull.Submissions[pull.LastRoundNumber()].SourceRev { 1479 - s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.") 1480 - return 1481 - } 1482 - } 1483 - 1484 - tx, err := s.db.BeginTx(r.Context(), nil) 1485 - if err != nil { 1486 - log.Println("failed to start tx") 1487 - s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1488 - return 1489 - } 1490 - defer tx.Rollback() 1491 - 1492 - err = db.ResubmitPull(tx, pull, patch, sourceRev) 1493 - if err != nil { 1494 - log.Println("failed to create pull request", err) 1495 - s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1496 - return 1497 - } 1498 - client, err := s.oauth.AuthorizedClient(r) 1499 - if err != nil { 1500 - log.Println("failed to authorize client") 1501 - s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1502 - return 1503 - } 1504 - 1505 - ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1506 - if err != nil { 1507 - // failed to get record 1508 - s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 1509 - return 1510 - } 1511 - 1512 - var recordPullSource *tangled.RepoPull_Source 1513 - if pull.IsBranchBased() { 1514 - recordPullSource = &tangled.RepoPull_Source{ 1515 - Branch: pull.PullSource.Branch, 1516 - } 1517 - } 1518 - if pull.IsForkBased() { 1519 - repoAt := pull.PullSource.RepoAt.String() 1520 - recordPullSource = &tangled.RepoPull_Source{ 1521 - Branch: pull.PullSource.Branch, 1522 - Repo: &repoAt, 1523 - } 1524 - } 1525 - 1526 - _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1527 - Collection: tangled.RepoPullNSID, 1528 - Repo: user.Did, 1529 - Rkey: pull.Rkey, 1530 - SwapRecord: ex.Cid, 1531 - Record: &lexutil.LexiconTypeDecoder{ 1532 - Val: &tangled.RepoPull{ 1533 - Title: pull.Title, 1534 - PullId: int64(pull.PullId), 1535 - TargetRepo: string(f.RepoAt), 1536 - TargetBranch: pull.TargetBranch, 1537 - Patch: patch, // new patch 1538 - Source: recordPullSource, 1539 - }, 1540 - }, 1541 - }) 1542 - if err != nil { 1543 - log.Println("failed to update record", err) 1544 - s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 1545 - return 1546 - } 1547 - 1548 - if err = tx.Commit(); err != nil { 1549 - log.Println("failed to commit transaction", err) 1550 - s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.") 1551 - return 1552 - } 1553 - 1554 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 1555 - return 1556 - } 1557 - 1558 - func (s *State) resubmitStackedPullHelper( 1559 - w http.ResponseWriter, 1560 - r *http.Request, 1561 - f *reporesolver.ResolvedRepo, 1562 - user *oauth.User, 1563 - pull *db.Pull, 1564 - patch string, 1565 - stackId string, 1566 - ) { 1567 - targetBranch := pull.TargetBranch 1568 - 1569 - origStack, _ := r.Context().Value("stack").(db.Stack) 1570 - newStack, err := newStack(f, user, targetBranch, patch, pull.PullSource, stackId) 1571 - if err != nil { 1572 - log.Println("failed to create resubmitted stack", err) 1573 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1574 - return 1575 - } 1576 - 1577 - // find the diff between the stacks, first, map them by changeId 1578 - origById := make(map[string]*db.Pull) 1579 - newById := make(map[string]*db.Pull) 1580 - for _, p := range origStack { 1581 - origById[p.ChangeId] = p 1582 - } 1583 - for _, p := range newStack { 1584 - newById[p.ChangeId] = p 1585 - } 1586 - 1587 - // commits that got deleted: corresponding pull is closed 1588 - // commits that got added: new pull is created 1589 - // commits that got updated: corresponding pull is resubmitted & new round begins 1590 - // 1591 - // for commits that were unchanged: no changes, parent-change-id is updated as necessary 1592 - additions := make(map[string]*db.Pull) 1593 - deletions := make(map[string]*db.Pull) 1594 - unchanged := make(map[string]struct{}) 1595 - updated := make(map[string]struct{}) 1596 - 1597 - // pulls in orignal stack but not in new one 1598 - for _, op := range origStack { 1599 - if _, ok := newById[op.ChangeId]; !ok { 1600 - deletions[op.ChangeId] = op 1601 - } 1602 - } 1603 - 1604 - // pulls in new stack but not in original one 1605 - for _, np := range newStack { 1606 - if _, ok := origById[np.ChangeId]; !ok { 1607 - additions[np.ChangeId] = np 1608 - } 1609 - } 1610 - 1611 - // NOTE: this loop can be written in any of above blocks, 1612 - // but is written separately in the interest of simpler code 1613 - for _, np := range newStack { 1614 - if op, ok := origById[np.ChangeId]; ok { 1615 - // pull exists in both stacks 1616 - // TODO: can we avoid reparse? 1617 - origFiles, origHeaderStr, _ := gitdiff.Parse(strings.NewReader(op.LatestPatch())) 1618 - newFiles, newHeaderStr, _ := gitdiff.Parse(strings.NewReader(np.LatestPatch())) 1619 - 1620 - origHeader, _ := gitdiff.ParsePatchHeader(origHeaderStr) 1621 - newHeader, _ := gitdiff.ParsePatchHeader(newHeaderStr) 1622 - 1623 - patchutil.SortPatch(newFiles) 1624 - patchutil.SortPatch(origFiles) 1625 - 1626 - // text content of patch may be identical, but a jj rebase might have forwarded it 1627 - // 1628 - // we still need to update the hash in submission.Patch and submission.SourceRev 1629 - if patchutil.Equal(newFiles, origFiles) && 1630 - origHeader.Title == newHeader.Title && 1631 - origHeader.Body == newHeader.Body { 1632 - unchanged[op.ChangeId] = struct{}{} 1633 - } else { 1634 - updated[op.ChangeId] = struct{}{} 1635 - } 1636 - } 1637 - } 1638 - 1639 - tx, err := s.db.Begin() 1640 - if err != nil { 1641 - log.Println("failed to start transaction", err) 1642 - s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 1643 - return 1644 - } 1645 - defer tx.Rollback() 1646 - 1647 - // pds updates to make 1648 - var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem 1649 - 1650 - // deleted pulls are marked as deleted in the DB 1651 - for _, p := range deletions { 1652 - err := db.DeletePull(tx, p.RepoAt, p.PullId) 1653 - if err != nil { 1654 - log.Println("failed to delete pull", err, p.PullId) 1655 - s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 1656 - return 1657 - } 1658 - writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 1659 - RepoApplyWrites_Delete: &comatproto.RepoApplyWrites_Delete{ 1660 - Collection: tangled.RepoPullNSID, 1661 - Rkey: p.Rkey, 1662 - }, 1663 - }) 1664 - } 1665 - 1666 - // new pulls are created 1667 - for _, p := range additions { 1668 - err := db.NewPull(tx, p) 1669 - if err != nil { 1670 - log.Println("failed to create pull", err, p.PullId) 1671 - s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 1672 - return 1673 - } 1674 - 1675 - record := p.AsRecord() 1676 - writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 1677 - RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{ 1678 - Collection: tangled.RepoPullNSID, 1679 - Rkey: &p.Rkey, 1680 - Value: &lexutil.LexiconTypeDecoder{ 1681 - Val: &record, 1682 - }, 1683 - }, 1684 - }) 1685 - } 1686 - 1687 - // updated pulls are, well, updated; to start a new round 1688 - for id := range updated { 1689 - op, _ := origById[id] 1690 - np, _ := newById[id] 1691 - 1692 - submission := np.Submissions[np.LastRoundNumber()] 1693 - 1694 - // resubmit the old pull 1695 - err := db.ResubmitPull(tx, op, submission.Patch, submission.SourceRev) 1696 - 1697 - if err != nil { 1698 - log.Println("failed to update pull", err, op.PullId) 1699 - s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 1700 - return 1701 - } 1702 - 1703 - record := op.AsRecord() 1704 - record.Patch = submission.Patch 1705 - 1706 - writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 1707 - RepoApplyWrites_Update: &comatproto.RepoApplyWrites_Update{ 1708 - Collection: tangled.RepoPullNSID, 1709 - Rkey: op.Rkey, 1710 - Value: &lexutil.LexiconTypeDecoder{ 1711 - Val: &record, 1712 - }, 1713 - }, 1714 - }) 1715 - } 1716 - 1717 - // unchanged pulls are edited without starting a new round 1718 - // 1719 - // update source-revs & patches without advancing rounds 1720 - for changeId := range unchanged { 1721 - op, _ := origById[changeId] 1722 - np, _ := newById[changeId] 1723 - 1724 - origSubmission := op.Submissions[op.LastRoundNumber()] 1725 - newSubmission := np.Submissions[np.LastRoundNumber()] 1726 - 1727 - log.Println("moving unchanged change id : ", changeId) 1728 - 1729 - err := db.UpdatePull( 1730 - tx, 1731 - newSubmission.Patch, 1732 - newSubmission.SourceRev, 1733 - db.FilterEq("id", origSubmission.ID), 1734 - ) 1735 - 1736 - if err != nil { 1737 - log.Println("failed to update pull", err, op.PullId) 1738 - s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 1739 - return 1740 - } 1741 - 1742 - record := op.AsRecord() 1743 - record.Patch = newSubmission.Patch 1744 - 1745 - writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 1746 - RepoApplyWrites_Update: &comatproto.RepoApplyWrites_Update{ 1747 - Collection: tangled.RepoPullNSID, 1748 - Rkey: op.Rkey, 1749 - Value: &lexutil.LexiconTypeDecoder{ 1750 - Val: &record, 1751 - }, 1752 - }, 1753 - }) 1754 - } 1755 - 1756 - // update parent-change-id relations for the entire stack 1757 - for _, p := range newStack { 1758 - err := db.SetPullParentChangeId( 1759 - tx, 1760 - p.ParentChangeId, 1761 - // these should be enough filters to be unique per-stack 1762 - db.FilterEq("repo_at", p.RepoAt.String()), 1763 - db.FilterEq("owner_did", p.OwnerDid), 1764 - db.FilterEq("change_id", p.ChangeId), 1765 - ) 1766 - 1767 - if err != nil { 1768 - log.Println("failed to update pull", err, p.PullId) 1769 - s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 1770 - return 1771 - } 1772 - } 1773 - 1774 - err = tx.Commit() 1775 - if err != nil { 1776 - log.Println("failed to resubmit pull", err) 1777 - s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 1778 - return 1779 - } 1780 - 1781 - client, err := s.oauth.AuthorizedClient(r) 1782 - if err != nil { 1783 - log.Println("failed to authorize client") 1784 - s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1785 - return 1786 - } 1787 - 1788 - _, err = client.RepoApplyWrites(r.Context(), &comatproto.RepoApplyWrites_Input{ 1789 - Repo: user.Did, 1790 - Writes: writes, 1791 - }) 1792 - if err != nil { 1793 - log.Println("failed to create stacked pull request", err) 1794 - s.pages.Notice(w, "pull", "Failed to create stacked pull request. Try again later.") 1795 - return 1796 - } 1797 - 1798 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 1799 - return 1800 - } 1801 - 1802 - func (s *State) MergePull(w http.ResponseWriter, r *http.Request) { 1803 - f, err := s.repoResolver.Resolve(r) 1804 - if err != nil { 1805 - log.Println("failed to resolve repo:", err) 1806 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1807 - return 1808 - } 1809 - 1810 - pull, ok := r.Context().Value("pull").(*db.Pull) 1811 - if !ok { 1812 - log.Println("failed to get pull") 1813 - s.pages.Notice(w, "pull-merge-error", "Failed to merge patch. Try again later.") 1814 - return 1815 - } 1816 - 1817 - var pullsToMerge db.Stack 1818 - pullsToMerge = append(pullsToMerge, pull) 1819 - if pull.IsStacked() { 1820 - stack, ok := r.Context().Value("stack").(db.Stack) 1821 - if !ok { 1822 - log.Println("failed to get stack") 1823 - s.pages.Notice(w, "pull-merge-error", "Failed to merge patch. Try again later.") 1824 - return 1825 - } 1826 - 1827 - // combine patches of substack 1828 - subStack := stack.StrictlyBelow(pull) 1829 - // collect the portion of the stack that is mergeable 1830 - mergeable := subStack.Mergeable() 1831 - // add to total patch 1832 - pullsToMerge = append(pullsToMerge, mergeable...) 1833 - } 1834 - 1835 - patch := pullsToMerge.CombinedPatch() 1836 - 1837 - secret, err := db.GetRegistrationKey(s.db, f.Knot) 1838 - if err != nil { 1839 - log.Printf("no registration key found for domain %s: %s\n", f.Knot, err) 1840 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1841 - return 1842 - } 1843 - 1844 - ident, err := s.resolver.ResolveIdent(r.Context(), pull.OwnerDid) 1845 - if err != nil { 1846 - log.Printf("resolving identity: %s", err) 1847 - w.WriteHeader(http.StatusNotFound) 1848 - return 1849 - } 1850 - 1851 - email, err := db.GetPrimaryEmail(s.db, pull.OwnerDid) 1852 - if err != nil { 1853 - log.Printf("failed to get primary email: %s", err) 1854 - } 1855 - 1856 - ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 1857 - if err != nil { 1858 - log.Printf("failed to create signed client for %s: %s", f.Knot, err) 1859 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1860 - return 1861 - } 1862 - 1863 - // Merge the pull request 1864 - resp, err := ksClient.Merge([]byte(patch), f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.Title, pull.Body, ident.Handle.String(), email.Address) 1865 - if err != nil { 1866 - log.Printf("failed to merge pull request: %s", err) 1867 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1868 - return 1869 - } 1870 - 1871 - if resp.StatusCode != http.StatusOK { 1872 - log.Printf("knotserver returned non-OK status code for merge: %d", resp.StatusCode) 1873 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1874 - return 1875 - } 1876 - 1877 - tx, err := s.db.Begin() 1878 - if err != nil { 1879 - log.Println("failed to start transcation", err) 1880 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1881 - return 1882 - } 1883 - defer tx.Rollback() 1884 - 1885 - for _, p := range pullsToMerge { 1886 - err := db.MergePull(tx, f.RepoAt, p.PullId) 1887 - if err != nil { 1888 - log.Printf("failed to update pull request status in database: %s", err) 1889 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1890 - return 1891 - } 1892 - } 1893 - 1894 - err = tx.Commit() 1895 - if err != nil { 1896 - // TODO: this is unsound, we should also revert the merge from the knotserver here 1897 - log.Printf("failed to update pull request status in database: %s", err) 1898 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1899 - return 1900 - } 1901 - 1902 - s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId)) 1903 - } 1904 - 1905 - func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) { 1906 - user := s.oauth.GetUser(r) 1907 - 1908 - f, err := s.repoResolver.Resolve(r) 1909 - if err != nil { 1910 - log.Println("malformed middleware") 1911 - return 1912 - } 1913 - 1914 - pull, ok := r.Context().Value("pull").(*db.Pull) 1915 - if !ok { 1916 - log.Println("failed to get pull") 1917 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1918 - return 1919 - } 1920 - 1921 - // auth filter: only owner or collaborators can close 1922 - roles := f.RolesInRepo(user) 1923 - isCollaborator := roles.IsCollaborator() 1924 - isPullAuthor := user.Did == pull.OwnerDid 1925 - isCloseAllowed := isCollaborator || isPullAuthor 1926 - if !isCloseAllowed { 1927 - log.Println("failed to close pull") 1928 - s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.") 1929 - return 1930 - } 1931 - 1932 - // Start a transaction 1933 - tx, err := s.db.BeginTx(r.Context(), nil) 1934 - if err != nil { 1935 - log.Println("failed to start transaction", err) 1936 - s.pages.Notice(w, "pull-close", "Failed to close pull.") 1937 - return 1938 - } 1939 - defer tx.Rollback() 1940 - 1941 - var pullsToClose []*db.Pull 1942 - pullsToClose = append(pullsToClose, pull) 1943 - 1944 - // if this PR is stacked, then we want to close all PRs below this one on the stack 1945 - if pull.IsStacked() { 1946 - stack := r.Context().Value("stack").(db.Stack) 1947 - subStack := stack.StrictlyBelow(pull) 1948 - pullsToClose = append(pullsToClose, subStack...) 1949 - } 1950 - 1951 - for _, p := range pullsToClose { 1952 - // Close the pull in the database 1953 - err = db.ClosePull(tx, f.RepoAt, p.PullId) 1954 - if err != nil { 1955 - log.Println("failed to close pull", err) 1956 - s.pages.Notice(w, "pull-close", "Failed to close pull.") 1957 - return 1958 - } 1959 - } 1960 - 1961 - // Commit the transaction 1962 - if err = tx.Commit(); err != nil { 1963 - log.Println("failed to commit transaction", err) 1964 - s.pages.Notice(w, "pull-close", "Failed to close pull.") 1965 - return 1966 - } 1967 - 1968 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 1969 - return 1970 - } 1971 - 1972 - func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) { 1973 - user := s.oauth.GetUser(r) 1974 - 1975 - f, err := s.repoResolver.Resolve(r) 1976 - if err != nil { 1977 - log.Println("failed to resolve repo", err) 1978 - s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1979 - return 1980 - } 1981 - 1982 - pull, ok := r.Context().Value("pull").(*db.Pull) 1983 - if !ok { 1984 - log.Println("failed to get pull") 1985 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1986 - return 1987 - } 1988 - 1989 - // auth filter: only owner or collaborators can close 1990 - roles := f.RolesInRepo(user) 1991 - isCollaborator := roles.IsCollaborator() 1992 - isPullAuthor := user.Did == pull.OwnerDid 1993 - isCloseAllowed := isCollaborator || isPullAuthor 1994 - if !isCloseAllowed { 1995 - log.Println("failed to close pull") 1996 - s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.") 1997 - return 1998 - } 1999 - 2000 - // Start a transaction 2001 - tx, err := s.db.BeginTx(r.Context(), nil) 2002 - if err != nil { 2003 - log.Println("failed to start transaction", err) 2004 - s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 2005 - return 2006 - } 2007 - defer tx.Rollback() 2008 - 2009 - var pullsToReopen []*db.Pull 2010 - pullsToReopen = append(pullsToReopen, pull) 2011 - 2012 - // if this PR is stacked, then we want to reopen all PRs above this one on the stack 2013 - if pull.IsStacked() { 2014 - stack := r.Context().Value("stack").(db.Stack) 2015 - subStack := stack.StrictlyAbove(pull) 2016 - pullsToReopen = append(pullsToReopen, subStack...) 2017 - } 2018 - 2019 - for _, p := range pullsToReopen { 2020 - // Close the pull in the database 2021 - err = db.ReopenPull(tx, f.RepoAt, p.PullId) 2022 - if err != nil { 2023 - log.Println("failed to close pull", err) 2024 - s.pages.Notice(w, "pull-close", "Failed to close pull.") 2025 - return 2026 - } 2027 - } 2028 - 2029 - // Commit the transaction 2030 - if err = tx.Commit(); err != nil { 2031 - log.Println("failed to commit transaction", err) 2032 - s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 2033 - return 2034 - } 2035 - 2036 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 2037 - return 2038 - } 2039 - 2040 - func newStack(f *reporesolver.ResolvedRepo, user *oauth.User, targetBranch, patch string, pullSource *db.PullSource, stackId string) (db.Stack, error) { 2041 - formatPatches, err := patchutil.ExtractPatches(patch) 2042 - if err != nil { 2043 - return nil, fmt.Errorf("Failed to extract patches: %v", err) 2044 - } 2045 - 2046 - // must have atleast 1 patch to begin with 2047 - if len(formatPatches) == 0 { 2048 - return nil, fmt.Errorf("No patches found in the generated format-patch.") 2049 - } 2050 - 2051 - // the stack is identified by a UUID 2052 - var stack db.Stack 2053 - parentChangeId := "" 2054 - for _, fp := range formatPatches { 2055 - // all patches must have a jj change-id 2056 - changeId, err := fp.ChangeId() 2057 - if err != nil { 2058 - return nil, fmt.Errorf("Stacking is only supported if all patches contain a change-id commit header.") 2059 - } 2060 - 2061 - title := fp.Title 2062 - body := fp.Body 2063 - rkey := appview.TID() 2064 - 2065 - initialSubmission := db.PullSubmission{ 2066 - Patch: fp.Raw, 2067 - SourceRev: fp.SHA, 2068 - } 2069 - pull := db.Pull{ 2070 - Title: title, 2071 - Body: body, 2072 - TargetBranch: targetBranch, 2073 - OwnerDid: user.Did, 2074 - RepoAt: f.RepoAt, 2075 - Rkey: rkey, 2076 - Submissions: []*db.PullSubmission{ 2077 - &initialSubmission, 2078 - }, 2079 - PullSource: pullSource, 2080 - Created: time.Now(), 2081 - 2082 - StackId: stackId, 2083 - ChangeId: changeId, 2084 - ParentChangeId: parentChangeId, 2085 - } 2086 - 2087 - stack = append(stack, &pull) 2088 - 2089 - parentChangeId = changeId 2090 - } 2091 - 2092 - return stack, nil 2093 - }
+8 -47
appview/state/router.go
··· 8 8 "github.com/gorilla/sessions" 9 9 "tangled.sh/tangled.sh/core/appview/middleware" 10 10 oauthhandler "tangled.sh/tangled.sh/core/appview/oauth/handler" 11 + "tangled.sh/tangled.sh/core/appview/pulls" 11 12 "tangled.sh/tangled.sh/core/appview/settings" 12 13 "tangled.sh/tangled.sh/core/appview/state/userutil" 13 14 ) ··· 138 139 r.Get("/*", s.RepoCompare) 139 140 }) 140 141 141 - r.Route("/pulls", func(r chi.Router) { 142 - r.Get("/", s.RepoPulls) 143 - r.With(middleware.AuthMiddleware(s.oauth)).Route("/new", func(r chi.Router) { 144 - r.Get("/", s.NewPull) 145 - r.Get("/patch-upload", s.PatchUploadFragment) 146 - r.Post("/validate-patch", s.ValidatePatch) 147 - r.Get("/compare-branches", s.CompareBranchesFragment) 148 - r.Get("/compare-forks", s.CompareForksFragment) 149 - r.Get("/fork-branches", s.CompareForksBranchesFragment) 150 - r.Post("/", s.NewPull) 151 - }) 152 - 153 - r.Route("/{pull}", func(r chi.Router) { 154 - r.Use(mw.ResolvePull()) 155 - r.Get("/", s.RepoSinglePull) 156 - 157 - r.Route("/round/{round}", func(r chi.Router) { 158 - r.Get("/", s.RepoPullPatch) 159 - r.Get("/interdiff", s.RepoPullInterdiff) 160 - r.Get("/actions", s.PullActions) 161 - r.With(middleware.AuthMiddleware(s.oauth)).Route("/comment", func(r chi.Router) { 162 - r.Get("/", s.PullComment) 163 - r.Post("/", s.PullComment) 164 - }) 165 - }) 166 - 167 - r.Route("/round/{round}.patch", func(r chi.Router) { 168 - r.Get("/", s.RepoPullPatchRaw) 169 - }) 170 - 171 - r.Group(func(r chi.Router) { 172 - r.Use(middleware.AuthMiddleware(s.oauth)) 173 - r.Route("/resubmit", func(r chi.Router) { 174 - r.Get("/", s.ResubmitPull) 175 - r.Post("/", s.ResubmitPull) 176 - }) 177 - r.Post("/close", s.ClosePull) 178 - r.Post("/reopen", s.ReopenPull) 179 - // collaborators only 180 - r.Group(func(r chi.Router) { 181 - r.Use(mw.RepoPermissionMiddleware("repo:push")) 182 - r.Post("/merge", s.MergePull) 183 - // maybe lock, etc. 184 - }) 185 - }) 186 - }) 187 - }) 142 + r.Mount("/pulls", s.PullsRouter(mw)) 188 143 189 144 // These routes get proxied to the knot 190 145 r.Get("/info/refs", s.InfoRefs) ··· 272 227 273 228 r.Mount("/settings", s.SettingsRouter()) 274 229 r.Mount("/", s.OAuthRouter()) 230 + 275 231 r.Get("/keys/{user}", s.Keys) 276 232 277 233 r.NotFound(func(w http.ResponseWriter, r *http.Request) { ··· 305 261 306 262 return settings.Router() 307 263 } 264 + 265 + func (s *State) PullsRouter(mw *middleware.Middleware) http.Handler { 266 + pulls := pulls.New(s.oauth, s.repoResolver, s.pages, s.resolver, s.db, s.config) 267 + return pulls.Router(mw) 268 + }