package handlers import ( "encoding/json" "net/http" "github.com/go-chi/chi/v5" "xorm.io/xorm" "github.com/forgeo/forgebucket/internal/api/middleware" "github.com/forgeo/forgebucket/internal/models" ) type PRSettingsHandler struct{ db *xorm.Engine } func NewPRSettingsHandler(db *xorm.Engine) *PRSettingsHandler { return &PRSettingsHandler{db: db} } func (h *PRSettingsHandler) resolveRepo(w http.ResponseWriter, r *http.Request) (*models.Repository, bool) { ownerName := chi.URLParam(r, "owner") repoName := chi.URLParam(r, "repo") var owner models.User if found, _ := h.db.Where("username = ?", ownerName).Get(&owner); !found { jsonError(w, "repository not found", http.StatusNotFound) return nil, false } var repo models.Repository if found, _ := h.db.Where("owner_id = ? AND name = ?", owner.ID, repoName).Get(&repo); !found { jsonError(w, "repository not found", http.StatusNotFound) return nil, false } return &repo, true } func (h *PRSettingsHandler) canManage(repo *models.Repository, callerID int64) bool { if callerID == repo.OwnerID { return true } var m models.RepoMember found, _ := h.db.Where("repo_id = ? AND user_id = ? AND permission = 'admin'", repo.ID, callerID).Get(&m) return found } // ── Default reviewers ───────────────────────────────────────────────────────── type defaultReviewerInfo struct { UserID int64 `json:"userId"` Username string `json:"username"` AvatarURL string `json:"avatarUrl"` } func (h *PRSettingsHandler) ListDefaultReviewers(w http.ResponseWriter, r *http.Request) { repo, ok := h.resolveRepo(w, r) if !ok { return } var rows []models.RepoDefaultReviewer h.db.Where("repo_id = ?", repo.ID).Find(&rows) result := make([]defaultReviewerInfo, 0, len(rows)) for _, dr := range rows { var u models.User if found, _ := h.db.ID(dr.UserID).Get(&u); found { result = append(result, defaultReviewerInfo{ UserID: u.ID, Username: u.Username, AvatarURL: u.AvatarURL, }) } } jsonOK(w, result) } func (h *PRSettingsHandler) AddDefaultReviewer(w http.ResponseWriter, r *http.Request) { repo, ok := h.resolveRepo(w, r) if !ok { return } callerID, _ := middleware.UserIDFromContext(r.Context()) if !h.canManage(repo, callerID) { jsonError(w, "forbidden", http.StatusForbidden) return } var body struct { Username string `json:"username"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Username == "" { jsonError(w, "username is required", http.StatusBadRequest) return } var u models.User if found, _ := h.db.Where("username = ?", body.Username).Get(&u); !found { jsonError(w, "user not found", http.StatusNotFound) return } // Idempotent: don't duplicate var existing models.RepoDefaultReviewer if found, _ := h.db.Where("repo_id = ? AND user_id = ?", repo.ID, u.ID).Get(&existing); found { jsonOK(w, defaultReviewerInfo{UserID: u.ID, Username: u.Username, AvatarURL: u.AvatarURL}) return } dr := &models.RepoDefaultReviewer{RepoID: repo.ID, UserID: u.ID} if _, err := h.db.Insert(dr); err != nil { jsonError(w, "could not add reviewer", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(defaultReviewerInfo{UserID: u.ID, Username: u.Username, AvatarURL: u.AvatarURL}) } func (h *PRSettingsHandler) RemoveDefaultReviewer(w http.ResponseWriter, r *http.Request) { repo, ok := h.resolveRepo(w, r) if !ok { return } callerID, _ := middleware.UserIDFromContext(r.Context()) if !h.canManage(repo, callerID) { jsonError(w, "forbidden", http.StatusForbidden) return } username := chi.URLParam(r, "username") var u models.User if found, _ := h.db.Where("username = ?", username).Get(&u); !found { w.WriteHeader(http.StatusNoContent) return } h.db.Where("repo_id = ? AND user_id = ?", repo.ID, u.ID).Delete(&models.RepoDefaultReviewer{}) w.WriteHeader(http.StatusNoContent) } // ── Default description ─────────────────────────────────────────────────────── func (h *PRSettingsHandler) GetDefaultDescription(w http.ResponseWriter, r *http.Request) { repo, ok := h.resolveRepo(w, r) if !ok { return } var dd models.RepoDefaultDescription h.db.Where("repo_id = ?", repo.ID).Get(&dd) jsonOK(w, map[string]string{"template": dd.Template}) } func (h *PRSettingsHandler) UpdateDefaultDescription(w http.ResponseWriter, r *http.Request) { repo, ok := h.resolveRepo(w, r) if !ok { return } callerID, _ := middleware.UserIDFromContext(r.Context()) if !h.canManage(repo, callerID) { jsonError(w, "forbidden", http.StatusForbidden) return } var body struct { Template string `json:"template"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { jsonError(w, "invalid body", http.StatusBadRequest) return } var dd models.RepoDefaultDescription found, _ := h.db.Where("repo_id = ?", repo.ID).Get(&dd) dd.RepoID = repo.ID dd.Template = body.Template if found { h.db.ID(dd.ID).Cols("template").Update(&dd) } else { h.db.Insert(&dd) } jsonOK(w, map[string]string{"template": dd.Template}) } // ── Excluded files ──────────────────────────────────────────────────────────── func (h *PRSettingsHandler) GetExcludedFiles(w http.ResponseWriter, r *http.Request) { repo, ok := h.resolveRepo(w, r) if !ok { return } var ef models.RepoExcludedFiles h.db.Where("repo_id = ?", repo.ID).Get(&ef) jsonOK(w, map[string]string{"patterns": ef.Patterns}) } func (h *PRSettingsHandler) UpdateExcludedFiles(w http.ResponseWriter, r *http.Request) { repo, ok := h.resolveRepo(w, r) if !ok { return } callerID, _ := middleware.UserIDFromContext(r.Context()) if !h.canManage(repo, callerID) { jsonError(w, "forbidden", http.StatusForbidden) return } var body struct { Patterns string `json:"patterns"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { jsonError(w, "invalid body", http.StatusBadRequest) return } var ef models.RepoExcludedFiles found, _ := h.db.Where("repo_id = ?", repo.ID).Get(&ef) ef.RepoID = repo.ID ef.Patterns = body.Patterns if found { h.db.ID(ef.ID).Cols("patterns").Update(&ef) } else { h.db.Insert(&ef) } jsonOK(w, map[string]string{"patterns": ef.Patterns}) } // ── Helpers used by PR create ───────────────────────────────────────────────── // AutoAssignReviewers inserts PrReviewer rows for all default reviewers of a repo. func AutoAssignReviewers(db *xorm.Engine, repoID, prID int64) { var defaults []models.RepoDefaultReviewer db.Where("repo_id = ?", repoID).Find(&defaults) for _, dr := range defaults { db.Insert(&models.PrReviewer{PRID: prID, UserID: dr.UserID}) } } // GetDefaultDescriptionTemplate returns the stored PR body template for a repo. func GetDefaultDescriptionTemplate(db *xorm.Engine, repoID int64) string { var dd models.RepoDefaultDescription db.Where("repo_id = ?", repoID).Get(&dd) return dd.Template } // GetPrReviewers returns reviewer details for a PR. type ReviewerInfo struct { UserID int64 `json:"userId"` Username string `json:"username"` AvatarURL string `json:"avatarUrl"` } func GetPrReviewers(db *xorm.Engine, prID int64) []ReviewerInfo { var rows []models.PrReviewer db.Where("pr_id = ?", prID).Find(&rows) result := make([]ReviewerInfo, 0, len(rows)) for _, rv := range rows { var u models.User if found, _ := db.ID(rv.UserID).Get(&u); found { result = append(result, ReviewerInfo{UserID: u.ID, Username: u.Username, AvatarURL: u.AvatarURL}) } } return result }