package handlers import ( "encoding/json" "net/http" "strconv" "github.com/go-chi/chi/v5" "xorm.io/xorm" "github.com/forgeo/forgebucket/internal/api/middleware" "github.com/forgeo/forgebucket/internal/models" ) type PRHandler struct { db *xorm.Engine } func NewPRHandler(db *xorm.Engine) *PRHandler { return &PRHandler{db: db} } func (h *PRHandler) List(w http.ResponseWriter, r *http.Request) { repoID, ok := h.repoIDFromURL(w, r) if !ok { return } status := r.URL.Query().Get("status") sess := h.db.Where("repo_id = ?", repoID) if status != "" { sess = sess.And("status = ?", status) } var prs []models.PullRequest if err := sess.Find(&prs); err != nil { jsonError(w, "could not list pull requests", http.StatusInternalServerError) return } if prs == nil { prs = []models.PullRequest{} } jsonOK(w, prs) } func (h *PRHandler) Create(w http.ResponseWriter, r *http.Request) { repoID, ok := h.repoIDFromURL(w, r) if !ok { return } authorID, _ := middleware.UserIDFromContext(r.Context()) var body struct { Title string `json:"title"` Body string `json:"body"` SourceBranch string `json:"sourceBranch"` TargetBranch string `json:"targetBranch"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { jsonError(w, "invalid request body", http.StatusBadRequest) return } if body.Title == "" || body.SourceBranch == "" { jsonError(w, "title and sourceBranch are required", http.StatusBadRequest) return } if body.TargetBranch == "" { body.TargetBranch = "main" } pr := &models.PullRequest{ RepoID: repoID, AuthorID: authorID, Title: body.Title, Body: body.Body, SourceBranch: body.SourceBranch, TargetBranch: body.TargetBranch, Status: models.PRStatusOpen, } if _, err := h.db.Insert(pr); err != nil { jsonError(w, "could not create pull request", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(pr) } func (h *PRHandler) Get(w http.ResponseWriter, r *http.Request) { pr, ok := h.lookupPR(w, r) if !ok { return } jsonOK(w, pr) } func (h *PRHandler) Merge(w http.ResponseWriter, r *http.Request) { pr, ok := h.lookupPR(w, r) if !ok { return } if pr.Status != models.PRStatusOpen { jsonError(w, "pull request is not open", http.StatusConflict) return } // Parse optional strategy from body; default to "merge". var body struct { Strategy string `json:"strategy"` } json.NewDecoder(r.Body).Decode(&body) if body.Strategy == "" { body.Strategy = "merge" } // Enforce merge strategy policy for this repo. allowed := GetAllowedStrategies(h.db, pr.RepoID) if !allowed[body.Strategy] { jsonError(w, "merge strategy '"+body.Strategy+"' is not allowed for this repository", http.StatusConflict) return } pr.Status = models.PRStatusMerged if _, err := h.db.ID(pr.ID).Cols("status").Update(pr); err != nil { jsonError(w, "could not merge pull request", http.StatusInternalServerError) return } // Fire pull_request webhook. go FireWebhooks(h.db, pr.RepoID, "pull_request", map[string]interface{}{ "action": "merged", "strategy": body.Strategy, "pullRequest": map[string]interface{}{"id": pr.ID, "title": pr.Title}, }) jsonOK(w, pr) } func (h *PRHandler) Close(w http.ResponseWriter, r *http.Request) { pr, ok := h.lookupPR(w, r) if !ok { return } if pr.Status != models.PRStatusOpen { jsonError(w, "pull request is not open", http.StatusConflict) return } pr.Status = models.PRStatusClosed if _, err := h.db.ID(pr.ID).Cols("status").Update(pr); err != nil { jsonError(w, "could not close pull request", http.StatusInternalServerError) return } jsonOK(w, pr) } func (h *PRHandler) repoIDFromURL(w http.ResponseWriter, r *http.Request) (int64, bool) { ownerName := chi.URLParam(r, "owner") repoName := chi.URLParam(r, "repo") var owner models.User found, err := h.db.Where("username = ?", ownerName).Get(&owner) if err != nil || !found { jsonError(w, "repository not found", http.StatusNotFound) return 0, false } var repo models.Repository found, err = h.db.Where("owner_id = ? AND name = ?", owner.ID, repoName).Get(&repo) if err != nil || !found { jsonError(w, "repository not found", http.StatusNotFound) return 0, false } return repo.ID, true } func (h *PRHandler) lookupPR(w http.ResponseWriter, r *http.Request) (*models.PullRequest, bool) { repoID, ok := h.repoIDFromURL(w, r) if !ok { return nil, false } prIDStr := chi.URLParam(r, "prID") prID, err := strconv.ParseInt(prIDStr, 10, 64) if err != nil { jsonError(w, "invalid pull request ID", http.StatusBadRequest) return nil, false } var pr models.PullRequest found, err := h.db.Where("id = ? AND repo_id = ?", prID, repoID).Get(&pr) if err != nil || !found { jsonError(w, "pull request not found", http.StatusNotFound) return nil, false } return &pr, true }