package handlers import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "io" "net/http" "strconv" "strings" "giteapm/internal/models" "github.com/gin-gonic/gin" ) type GiteaWebhookPayload struct { Action string `json:"action"` Number int `json:"number"` Repository struct { ID int `json:"id"` Name string `json:"name"` FullName string `json:"full_name"` } `json:"repository"` PullRequest *struct { ID int `json:"id"` Number int `json:"number"` Title string `json:"title"` State string `json:"state"` HTMLURL string `json:"html_url"` } `json:"pull_request,omitempty"` Commits []struct { ID string `json:"id"` Message string `json:"message"` URL string `json:"url"` } `json:"commits,omitempty"` Pusher struct { ID int `json:"id"` Username string `json:"username"` } `json:"pusher,omitempty"` } func (h *Handlers) HandleGiteaWebhook(c *gin.Context) { // 验证 webhook 签名 if !h.verifyWebhookSignature(c) { c.JSON(http.StatusUnauthorized, ErrorResponse(401, "Invalid webhook signature")) return } // 读取请求体 body, err := io.ReadAll(c.Request.Body) if err != nil { c.JSON(http.StatusBadRequest, ErrorResponse(400, "Failed to read request body")) return } // 解析 payload var payload GiteaWebhookPayload if err := json.Unmarshal(body, &payload); err != nil { c.JSON(http.StatusBadRequest, ErrorResponse(400, "Invalid JSON payload")) return } // 获取事件类型 eventType := c.GetHeader("X-Gitea-Event") // 处理不同类型的事件 switch eventType { case "push": h.handlePushEvent(payload) case "pull_request": h.handlePullRequestEvent(payload) case "issues": h.handleIssueEvent(payload) default: // 忽略其他事件类型 } c.JSON(http.StatusOK, SuccessResponse(gin.H{"message": "Webhook processed successfully"})) } func (h *Handlers) verifyWebhookSignature(c *gin.Context) bool { signature := c.GetHeader("X-Gitea-Signature") if signature == "" { return false } // 读取请求体 body, err := io.ReadAll(c.Request.Body) if err != nil { return false } // 重新设置请求体,以便后续读取 c.Request.Body = io.NopCloser(strings.NewReader(string(body))) // 计算 HMAC-SHA256 mac := hmac.New(sha256.New, []byte(h.Cfg.Gitea.WebhookSecret)) mac.Write(body) expectedSignature := hex.EncodeToString(mac.Sum(nil)) // 比较签名 return hmac.Equal([]byte(signature), []byte(expectedSignature)) } func (h *Handlers) handlePushEvent(payload GiteaWebhookPayload) { // 处理推送事件 repoID := uint(payload.Repository.ID) for _, commit := range payload.Commits { // 查找关联的任务 _, err := models.GetGiteaRelationsByRepo(repoID, "commit") if err != nil { continue } // 检查提交消息中是否包含任务引用(如 #123) commitMessage := commit.Message taskNumbers := extractTaskNumbers(commitMessage) for _, taskNumber := range taskNumbers { // 创建或更新 Git 关联 relation := &models.GiteaRelation{ GiteaRepoID: repoID, GiteaRepoName: payload.Repository.FullName, RelationType: "commit", GiteaObjectID: commit.ID, GiteaObjectTitle: commitMessage, GiteaObjectURL: commit.URL, } // 根据任务号查找任务 if task := findTaskByNumber(taskNumber); task != nil { relation.TaskID = task.ID models.CreateGiteaRelation(relation) // 可以根据提交消息自动更新任务状态 if strings.Contains(strings.ToLower(commitMessage), "fix") || strings.Contains(strings.ToLower(commitMessage), "resolve") { task.Status = "review" models.UpdateTask(task) } } } } } func (h *Handlers) handlePullRequestEvent(payload GiteaWebhookPayload) { if payload.PullRequest == nil { return } pr := payload.PullRequest repoID := uint(payload.Repository.ID) // 检查 PR 标题中的任务引用 taskNumbers := extractTaskNumbers(pr.Title) for _, taskNumber := range taskNumbers { if task := findTaskByNumber(taskNumber); task != nil { relation := &models.GiteaRelation{ TaskID: task.ID, GiteaRepoID: repoID, GiteaRepoName: payload.Repository.FullName, RelationType: "pull_request", GiteaObjectID: strconv.Itoa(pr.ID), GiteaObjectNumber: func() *uint { n := uint(pr.Number); return &n }(), GiteaObjectTitle: pr.Title, GiteaObjectURL: pr.HTMLURL, } models.CreateGiteaRelation(relation) // 根据 PR 状态自动更新任务状态 switch payload.Action { case "opened": task.Status = "review" case "closed": if pr.State == "merged" { task.Status = "done" } } models.UpdateTask(task) } } } func (h *Handlers) handleIssueEvent(payload GiteaWebhookPayload) { // 处理 Issue 事件,可以将 Gitea Issue 同步为任务 // 这里可以根据需要实现 } // 从文本中提取任务编号(如 #123, fixes #456) func extractTaskNumbers(text string) []int { var numbers []int // 简单的正则匹配,实际可以使用更复杂的逻辑 words := strings.Fields(text) for _, word := range words { if strings.HasPrefix(word, "#") { if num, err := strconv.Atoi(strings.TrimPrefix(word, "#")); err == nil { numbers = append(numbers, num) } } } return numbers } // 根据任务编号查找任务(这里简化实现) func findTaskByNumber(taskNumber int) *models.Task { // 这里应该实现根据任务编号查找任务的逻辑 // 可以在数据库中添加 task_number 字段,或者使用 ID task, err := models.GetTaskByID(uint(taskNumber)) if err != nil { return nil } return task }