package platforms import ( "code-review/services/types" "code-review/utils" "fmt" "log" "strings" ) type GiteaEvent struct { apiBase string auth *AuthConfig Event string client *httpClient Secret string `json:"secret"` Ref string `json:"ref"` Before string `json:"before"` After string `json:"after"` CompareURL string `json:"compare_url"` Commits []struct { ID string `json:"sha"` Message string `json:"commit.message"` URL string `json:"url"` HTMLURL string `json:"html_url"` Created string `json:"created"` Author struct { Active bool `json:"active"` AvatarURL string `json:"avatar_url"` Created string `json:"created"` Description string `json:"description"` Email string `json:"email"` FollowersCount int `json:"followers_count"` FollowingCount int `json:"following_count"` FullName string `json:"full_name"` HTMLURL string `json:"html_url"` ID int `json:"id"` IsAdmin bool `json:"is_admin"` Language string `json:"language"` LastLogin string `json:"last_login"` Location string `json:"location"` Login string `json:"login"` LoginName string `json:"login_name"` ProhibitLogin bool `json:"prohibit_login"` Restricted bool `json:"restricted"` SourceID int `json:"source_id"` Visibility string `json:"visibility"` Website string `json:"website"` } `json:"author"` Committer struct { Active bool `json:"active"` AvatarURL string `json:"avatar_url"` Created string `json:"created"` Description string `json:"description"` Email string `json:"email"` FollowersCount int `json:"followers_count"` FollowingCount int `json:"following_count"` FullName string `json:"full_name"` HTMLURL string `json:"html_url"` ID int `json:"id"` IsAdmin bool `json:"is_admin"` Language string `json:"language"` LastLogin string `json:"last_login"` Location string `json:"location"` Login string `json:"login"` LoginName string `json:"login_name"` ProhibitLogin bool `json:"prohibit_login"` Restricted bool `json:"restricted"` SourceID int `json:"source_id"` Visibility string `json:"visibility"` Website string `json:"website"` } `json:"committer"` Commit struct { Author struct { Date string `json:"date"` Email string `json:"email"` Name string `json:"name"` } `json:"author"` Committer struct { Date string `json:"date"` Email string `json:"email"` Name string `json:"name"` } `json:"committer"` Message string `json:"message"` Tree struct { Created string `json:"created"` SHA string `json:"sha"` URL string `json:"url"` } `json:"tree"` URL string `json:"url"` Verification struct { Payload string `json:"payload"` Reason string `json:"reason"` Signature string `json:"signature"` Signer struct { Email string `json:"email"` Name string `json:"name"` Username string `json:"username"` } `json:"signer"` Verified bool `json:"verified"` } `json:"verification"` } `json:"commit"` Files []struct { Filename string `json:"filename"` Status string `json:"status"` } `json:"files"` Parents []struct { Created string `json:"created"` SHA string `json:"sha"` URL string `json:"url"` } `json:"parents"` Stats struct { Additions int `json:"additions"` Deletions int `json:"deletions"` Total int `json:"total"` } `json:"stats"` } `json:"commits"` Repository struct { ID int `json:"id"` Name string `json:"name"` FullName string `json:"full_name"` Description string `json:"description"` Private bool `json:"private"` Fork bool `json:"fork"` HTMLURL string `json:"html_url"` SSHURL string `json:"ssh_url"` CloneURL string `json:"clone_url"` Website string `json:"website"` StarsCount int `json:"stars_count"` ForksCount int `json:"forks_count"` WatchersCount int `json:"watchers_count"` OpenIssuesCount int `json:"open_issues_count"` DefaultBranch string `json:"default_branch"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` Owner struct { ID int `json:"id"` Login string `json:"login"` FullName string `json:"full_name"` Email string `json:"email"` AvatarURL string `json:"avatar_url"` Username string `json:"username"` } `json:"owner"` } `json:"repository"` Pusher struct { ID int `json:"id"` Login string `json:"login"` FullName string `json:"full_name"` Email string `json:"email"` AvatarURL string `json:"avatar_url"` Username string `json:"username"` } `json:"pusher"` Sender struct { ID int `json:"id"` Login string `json:"login"` FullName string `json:"full_name"` Email string `json:"email"` AvatarURL string `json:"avatar_url"` Username string `json:"username"` } `json:"sender"` } // NewGiteaEvent 创建 Gitea 事件 func NewGiteaEvent(apiBase string, auth *AuthConfig, event string) *GiteaEvent { return &GiteaEvent{ apiBase: apiBase, auth: auth, Event: event, } } // 定义 Gitea commit 响应的结构 type giteaCommitResponse struct { Diff string `json:"diff"` } func (e *GiteaEvent) ExtractChanges() (*types.CodeChanges, error) { // 检查是否有提交记录 if len(e.Commits) == 0 { log.Printf("没有提交记录,跳过代码审查") return nil, nil } if e.client == nil { e.client = newHTTPClient(e.apiBase, e.auth) log.Printf("初始化 HTTP 客户端: url=%s", e.apiBase) } changes := &types.CodeChanges{ Repository: e.Repository.FullName, Branch: e.Ref, CommitID: e.After, Files: make([]types.FileChange, 0), } for _, commit := range e.Commits { // 检查提交信息是否包含跳过标记 if strings.Contains(commit.Message, "[skip codereview]") { log.Printf("提交包含跳过标记,跳过所有文件审查: commit=%s", commit.ID) continue } // 检查是否是合并提交 if strings.HasPrefix(commit.Message, "Merge remote-tracking branch") || strings.HasPrefix(commit.Message, "Merge branch") { log.Printf("跳过合并提交的文件审查: commit=%s", commit.ID) continue } // 获取 diff 内容 apiPath := fmt.Sprintf("/api/v1/repos/%s/%s/git/commits/%s.diff", e.Repository.Owner.Login, e.Repository.Name, e.After) diffContent, err := e.client.getRaw(apiPath) if err != nil { log.Printf("获取提交详情失败: commit=%s, error=%v", commit.ID, err) continue } // 解析 diff 内容 diffLines := strings.Split(diffContent, "\n") var currentFile *types.FileChange var currentContent strings.Builder for _, line := range diffLines { if strings.HasPrefix(line, "diff --git") { // 保存前一个文件的内容 if currentFile != nil { currentFile.Content = currentContent.String() + "```\n" changes.Files = append(changes.Files, *currentFile) } // 解析文件名 parts := strings.Split(line, " ") if len(parts) >= 3 { filePath := strings.TrimPrefix(parts[2], "b/") if shouldSkipFile(filePath) { log.Printf("跳过文件审查: file=%s", filePath) currentFile = nil continue } currentFile = &types.FileChange{ Path: filePath, Type: utils.ParseFileType("modified"), } currentContent.Reset() currentContent.WriteString(fmt.Sprintf("### 变更说明\n")) currentContent.WriteString(fmt.Sprintf("提交信息: %s\n\n", commit.Message)) currentContent.WriteString("### 变更内容\n") currentContent.WriteString("```diff\n") } } else if currentFile != nil { currentContent.WriteString(line + "\n") } } // 保存最后一个文件的内容 if currentFile != nil { currentFile.Content = currentContent.String() + "```\n" changes.Files = append(changes.Files, *currentFile) } } return changes, nil } func (e *GiteaEvent) PostComments(result *types.ReviewResult) error { if e.client == nil { e.client = newHTTPClient(e.apiBase, e.auth) } // 创建 issue path := fmt.Sprintf("/api/v1/repos/%s/%s/issues", e.Repository.Owner.Login, e.Repository.Name) issueData := map[string]interface{}{ "title": fmt.Sprintf("AI 代码审查 - %s", e.After[:7]), "body": utils.FormatReviewResult(result), } if err := e.client.post(path, issueData); err != nil { log.Printf("创建 issue 失败: path=%s, error=%v", path, err) return fmt.Errorf("创建 issue 失败: %w", err) } log.Printf("成功创建 issue: path=%s, commitID=%s", path, e.After[:7]) return nil } func (e *GiteaEvent) GetPlatform() string { return "gitea" } // 判断是否应该跳过文件审查 func shouldSkipFile(filename string) bool { // 跳过特定文件类型 skipExtensions := []string{".md", ".txt", ".json", ".yaml", ".yml", ".lock"} for _, ext := range skipExtensions { if strings.HasSuffix(filename, ext) { return true } } // 跳过特定目录 skipDirs := []string{"node_modules/", "dist/", "build/", "vendor/"} for _, dir := range skipDirs { if strings.Contains(filename, dir) { return true } } return false }