333 lines
10 KiB
Go
333 lines
10 KiB
Go
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)
|
|
}
|
|
|
|
// 获取所有提交作者并去重
|
|
assignees := make(map[string]struct{})
|
|
for _, commit := range e.Commits {
|
|
if commit.Author.Login != "" {
|
|
assignees[commit.Author.Login] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// 如果没有提交作者,则使用推送者
|
|
if len(assignees) == 0 {
|
|
assignees[e.Pusher.Login] = struct{}{}
|
|
}
|
|
|
|
// 将 map 转换为 slice
|
|
assigneeList := make([]string, 0, len(assignees))
|
|
for assignee := range assignees {
|
|
assigneeList = append(assigneeList, assignee)
|
|
}
|
|
|
|
// 创建 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),
|
|
"assignee": assigneeList[0], // 使用第一个作者作为主要受理人
|
|
"assignees": assigneeList,
|
|
}
|
|
|
|
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, assignees=%v", path, e.After[:7], assigneeList)
|
|
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
|
|
}
|