package pulls import ( "context" "fmt" "net/http" "strconv" "tangled.org/core/api/tangled" "tangled.org/core/appview/db" "tangled.org/core/appview/models" "tangled.org/core/appview/pages" "tangled.org/core/xrpc/xrpcclient" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/go-chi/chi/v5" ) // htmx fragment func (s *Pulls) PullActions(w http.ResponseWriter, r *http.Request) { l := s.logger.With("handler", "PullActions") switch r.Method { case http.MethodGet: user := s.oauth.GetMultiAccountUser(r) if user != nil { l = l.With("user", user.Did) } f, err := s.repoResolver.Resolve(r) if err != nil { l.Error("failed to get repo and knot", "err", err) return } pull, ok := r.Context().Value("pull").(*models.Pull) if !ok { l.Error("failed to get pull") s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") return } l = l.With("pull_id", pull.PullId, "pull_owner", pull.OwnerDid) versionNumber, err := strconv.Atoi(chi.URLParam(r, "version")) if err != nil { versionNumber = pull.LatestVersionNumber() } if versionNumber >= len(pull.Versions) { http.Error(w, "bad round id", http.StatusBadRequest) l.Error("failed to parse round id", "err", err, "round_number", versionNumber) return } isLastVersion := versionNumber == pull.LatestVersionNumber() var workflowsChanged bool var changedWorkflows []string hasPipeline := false if isLastVersion && f.Spindle != "" { pipelines := fetchPipelines(r.Context(), l, f, []string{pull.LatestSha()}) _, hasPipeline = pipelines[pull.LatestSha()] if pull.IsForkBased() && !hasPipeline { latest := pull.LatestVersion() changedWorkflows, err = func(ctx context.Context) ([]string, error) { base, err := s.resolveRev(ctx, pull.RepoDid, pull.TargetBranch) if err != nil { return nil, fmt.Errorf("failed to resolve target branch: %w", err) } return s.changedWorkflowFiles(ctx, pull.RepoDid, base, pull.SourceRepo, latest.Head) }(r.Context()) if err != nil { l.Error("failed to inspect latest round's patch for workflow changes", "err", err) } workflowsChanged = len(changedWorkflows) > 0 } } // only the last round's buttons and banners use merge/resubmit checks var mergeCheckParams pages.MergeCheckParams var resubmitResult = pages.Unknown if isLastVersion { mergeCheckParams = s.composeMergeCheck(r.Context(), f, pull.TargetBranch, pull.SourceRepo, pull.LatestVersion().Head) if user != nil && syntax.DID(user.Did) == pull.OwnerDid { resubmitResult = s.resubmitCheck(r.Context(), pull) } } s.pages.PullActionsFragment(w, pages.PullActionsParams{ BaseParams: pages.BaseParamsFromContext(r.Context()), RepoInfo: s.repoResolver.GetRepoInfo(r, user), Pull: pull, RoundNumber: versionNumber, MergeCheck: mergeCheckParams, ResubmitCheck: resubmitResult, BranchDeleteStatus: s.branchDeleteStatus(r, f, pull), WorkflowsChanged: workflowsChanged, ChangedWorkflowFiles: changedWorkflows, HasPipeline: hasPipeline, }) return } } func (s *Pulls) branchDeleteStatus(r *http.Request, repo *models.Repo, pull *models.Pull) *pages.BranchDeleteStatus { if pull.State != models.PullMerged || pull.SourceBranch == nil { return nil } user := s.oauth.GetMultiAccountUser(r) if user == nil { return nil } branch := *pull.SourceBranch if syntax.DID(repo.RepoDid) != pull.SourceRepo { var err error repo, err = db.GetRepoByDid(s.db, pull.SourceRepo.String()) if err != nil { return nil } } // user can only delete branch if they are a collaborator in the repo that the branch belongs to if !s.acl.HasRepoPermission(r.Context(), repo, user.Did, "repo:push") { return nil } xrpcc := s.knotMirrorXRPC resp, err := tangled.GitTempGetBranch(r.Context(), xrpcc, branch, repo.RepoDid) if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { s.logger.Error("failed to get branch", "xrpcerr", xrpcerr, "err", err) return nil } return &pages.BranchDeleteStatus{ Repo: repo, Branch: resp.Name, } } func (s *Pulls) resubmitCheck(ctx context.Context, pull *models.Pull) pages.ResubmitResult { if pull.State == models.PullMerged || pull.State == models.PullAbandoned || pull.SourceBranch == nil { return pages.Unknown } sourceSha, err := s.resolveRev(ctx, pull.SourceRepo, *pull.SourceBranch) if err != nil { s.logger.Error("failed to resolve source branch", "err", err, "pull_id", pull.PullId, "branch", *pull.SourceBranch) return pages.Unknown } if pull.LatestVersion().Head != sourceSha { return pages.ShouldResubmit } return pages.ShouldNotResubmit }