Files
2026-05-11 23:56:45 +02:00

268 lines
6.6 KiB
Go

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
}
// Auto-assign default reviewers.
AutoAssignReviewers(h.db, repoID, pr.ID)
// Fire webhook.
go FireWebhooks(h.db, repoID, "pull_request", map[string]interface{}{
"action": "opened",
"pullRequest": map[string]interface{}{"id": pr.ID, "title": pr.Title},
})
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(prWithReviewers(h.db, pr))
}
type prResponse struct {
models.PullRequest
Reviewers []ReviewerInfo `json:"reviewers"`
}
func prWithReviewers(db *xorm.Engine, pr *models.PullRequest) prResponse {
return prResponse{
PullRequest: *pr,
Reviewers: GetPrReviewers(db, pr.ID),
}
}
func (h *PRHandler) Get(w http.ResponseWriter, r *http.Request) {
pr, ok := h.lookupPR(w, r)
if !ok {
return
}
jsonOK(w, prWithReviewers(h.db, 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
}
go FireWebhooks(h.db, pr.RepoID, "pull_request", map[string]interface{}{
"action": "closed",
"pullRequest": map[string]interface{}{"id": pr.ID, "title": pr.Title},
})
jsonOK(w, prWithReviewers(h.db, pr))
}
func (h *PRHandler) Reopen(w http.ResponseWriter, r *http.Request) {
pr, ok := h.lookupPR(w, r)
if !ok {
return
}
if pr.Status != models.PRStatusClosed {
jsonError(w, "pull request is not closed", http.StatusConflict)
return
}
pr.Status = models.PRStatusOpen
if _, err := h.db.ID(pr.ID).Cols("status").Update(pr); err != nil {
jsonError(w, "could not reopen pull request", http.StatusInternalServerError)
return
}
go FireWebhooks(h.db, pr.RepoID, "pull_request", map[string]interface{}{
"action": "reopened",
"pullRequest": map[string]interface{}{"id": pr.ID, "title": pr.Title},
})
jsonOK(w, prWithReviewers(h.db, pr))
}
func (h *PRHandler) Update(w http.ResponseWriter, r *http.Request) {
pr, ok := h.lookupPR(w, r)
if !ok {
return
}
var body struct {
Title *string `json:"title"`
Body *string `json:"body"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
jsonError(w, "invalid request body", http.StatusBadRequest)
return
}
var cols []string
if body.Title != nil {
if *body.Title == "" {
jsonError(w, "title cannot be empty", http.StatusBadRequest)
return
}
pr.Title = *body.Title
cols = append(cols, "title")
}
if body.Body != nil {
pr.Body = *body.Body
cols = append(cols, "body")
}
if len(cols) > 0 {
if _, err := h.db.ID(pr.ID).Cols(cols...).Update(pr); err != nil {
jsonError(w, "could not update pull request", http.StatusInternalServerError)
return
}
}
jsonOK(w, prWithReviewers(h.db, pr))
}
func (h *PRHandler) repoIDFromURL(w http.ResponseWriter, r *http.Request) (int64, bool) {
return resolveRepoID(h.db, w, r)
}
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
}