From 5a3117dc7208c3b850b7fd4e89fead3b03618db7 Mon Sep 17 00:00:00 2001 From: Hua Date: Tue, 29 Apr 2025 11:30:15 +0800 Subject: [PATCH] =?UTF-8?q?gitea=E8=8E=B7=E5=8F=96diff=E6=9B=B4=E6=94=B9?= =?UTF-8?q?=20dockerfile=E6=9B=B4=E6=94=B9=20=E6=97=A5=E5=BF=97=E8=BE=93?= =?UTF-8?q?=E5=87=BA=E8=87=B3=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 8 +- main.go | 7 ++ services/ai/client.go | 1 + services/platforms/client.go | 35 +++++++++ services/platforms/gitea.go | 140 +++++++++++++++++++---------------- services/platforms/gitee.go | 27 +++++++ services/platforms/gitlab.go | 20 ++++- utils/logger.go | 47 ++++++++++++ 8 files changed, 220 insertions(+), 65 deletions(-) create mode 100644 utils/logger.go diff --git a/Dockerfile b/Dockerfile index e9f68bc..be7251a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,14 +22,20 @@ WORKDIR /app # 从构建阶段复制二进制文件 COPY --from=builder /app/ai-code-review /app/ +# 复制静态文件目录 +COPY --from=builder /app/static /app/static + # 创建默认配置文件到临时位置 RUN echo 'port: 53321\nadmin_token: "token"\n\nauto_disable:\n enabled: true\n max_failures: 3\n reset_after: 30\n\nais: []\n\ngit: []' > /app/config.default.yaml +# 创建日志目录并设置权限 +RUN mkdir -p /app/logs && chmod 755 /app/logs + # 暴露端口 EXPOSE 53321 # 设置卷挂载点 -VOLUME ["/app/config.yaml"] +VOLUME ["/app/config.yaml", "/app/logs"] # 运行应用,如果配置文件不存在则使用默认配置 CMD ["sh", "-c", "if [ ! -f /app/config.yaml ]; then cp /app/config.default.yaml /app/config.yaml; fi && /app/ai-code-review"] \ No newline at end of file diff --git a/main.go b/main.go index 0a0395f..3b75329 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "code-review/handlers" "code-review/services" "code-review/services/ai" + "code-review/utils" "fmt" "log" @@ -12,6 +13,12 @@ import ( ) func main() { + // 初始化日志系统 + if err := utils.InitLogger(); err != nil { + log.Fatalf("初始化日志系统失败: %v", err) + } + defer utils.CloseLogger() + if err := config.LoadConfig("config.yaml"); err != nil { log.Fatalf("加载配置失败: %v", err) } diff --git a/services/ai/client.go b/services/ai/client.go index cb30c10..cf6b99b 100644 --- a/services/ai/client.go +++ b/services/ai/client.go @@ -45,6 +45,7 @@ func (c *Client) Chat(systemMsg, prompt string) (string, error) { var response string var err error + log.Printf("AImodel=%s, prompt=%s", c.model, prompt) if c.aiType == "ollama" { response, err = c.ollamaChat(systemMsg, prompt) } else { diff --git a/services/platforms/client.go b/services/platforms/client.go index 534530f..b086282 100644 --- a/services/platforms/client.go +++ b/services/platforms/client.go @@ -195,3 +195,38 @@ func (c *httpClient) postWithHeaders(path string, data interface{}, headers map[ log.Printf("POST 请求成功: url=%s", url) return nil } + +func (c *httpClient) getRaw(path string) (string, error) { + url := fmt.Sprintf("%s%s", c.url, path) + log.Printf("发送 GET 请求: url=%s", url) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + log.Printf("创建 GET 请求失败: url=%s, error=%v", url, err) + return "", fmt.Errorf("创建请求失败: %w", err) + } + + c.setAuthHeaders(req) + + resp, err := c.client.Do(req) + if err != nil { + log.Printf("发送 GET 请求失败: url=%s, error=%v", url, err) + return "", fmt.Errorf("发送请求失败: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + log.Printf("GET 请求返回错误状态码: url=%s, status=%d, response=%s", url, resp.StatusCode, string(body)) + return "", fmt.Errorf("请求失败,状态码: %d,响应: %s", resp.StatusCode, string(body)) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Printf("读取响应失败: url=%s, error=%v", url, err) + return "", fmt.Errorf("读取响应失败: %w", err) + } + + log.Printf("GET 请求成功: url=%s", url) + return string(body), nil +} diff --git a/services/platforms/gitea.go b/services/platforms/gitea.go index d39ec08..849375e 100644 --- a/services/platforms/gitea.go +++ b/services/platforms/gitea.go @@ -172,27 +172,7 @@ func NewGiteaEvent(apiBase string, auth *AuthConfig, event string) *GiteaEvent { // 定义 Gitea commit 响应的结构 type giteaCommitResponse struct { - Commit struct { - Message string `json:"message"` - 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"` - } `json:"commit"` - Files []struct { - Filename string `json:"filename"` - Status string `json:"status"` - Additions int `json:"additions"` - Deletions int `json:"deletions"` - Changes int `json:"changes"` - Content string `json:"content"` - } `json:"files"` + Diff string `json:"diff"` } func (e *GiteaEvent) ExtractChanges() (*types.CodeChanges, error) { @@ -202,14 +182,6 @@ func (e *GiteaEvent) ExtractChanges() (*types.CodeChanges, error) { return nil, nil } - // 检查是否跳过代码审查 - for _, commit := range e.Commits { - if strings.Contains(commit.Commit.Message, "[skip codereview]") { - log.Printf("跳过代码审查: commit=%s", commit.ID) - return nil, nil - } - } - if e.client == nil { e.client = newHTTPClient(e.apiBase, e.auth) log.Printf("初始化 HTTP 客户端: url=%s", e.apiBase) @@ -223,48 +195,69 @@ func (e *GiteaEvent) ExtractChanges() (*types.CodeChanges, error) { } for _, commit := range e.Commits { - // 直接获取 diff 内容 - apiPath := fmt.Sprintf("/api/v1/repos/%s/%s/git/commits/%s", e.Repository.Owner.Login, e.Repository.Name, e.After) - var diffContent giteaCommitResponse + // 检查提交信息是否包含跳过标记 + if strings.Contains(commit.Message, "[skip codereview]") { + log.Printf("提交包含跳过标记,跳过所有文件审查: commit=%s", commit.ID) + continue + } - if err := e.client.get(apiPath, &diffContent); err != nil { + // 检查是否是合并提交 + 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 } - // 处理每个文件的变更 - for _, file := range diffContent.Files { - var content strings.Builder - content.WriteString(fmt.Sprintf("### 变更说明\n")) - content.WriteString(fmt.Sprintf("提交信息: %s\n\n", diffContent.Commit.Message)) + // 解析 diff 内容 + diffLines := strings.Split(diffContent, "\n") + var currentFile *types.FileChange + var currentContent strings.Builder - status := "modified" - switch file.Status { - case "added": - status = "added" - content.WriteString(fmt.Sprintf("新增文件: %s\n\n", file.Filename)) - case "removed": - status = "deleted" - content.WriteString(fmt.Sprintf("删除文件: %s\n\n", file.Filename)) - case "renamed": - status = "renamed" - content.WriteString(fmt.Sprintf("重命名文件: %s\n\n", file.Filename)) - default: - content.WriteString(fmt.Sprintf("修改文件: %s\n\n", file.Filename)) + 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 file.Content != "" { - content.WriteString("### 变更内容\n") - content.WriteString("```diff\n") - content.WriteString(file.Content) - content.WriteString("\n```\n") - } - - changes.Files = append(changes.Files, types.FileChange{ - Path: file.Filename, - Content: content.String(), - Type: utils.ParseFileType(status), - }) + // 保存最后一个文件的内容 + if currentFile != nil { + currentFile.Content = currentContent.String() + "```\n" + changes.Files = append(changes.Files, *currentFile) } } @@ -295,3 +288,24 @@ func (e *GiteaEvent) PostComments(result *types.ReviewResult) error { 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 +} diff --git a/services/platforms/gitee.go b/services/platforms/gitee.go index 92cf935..bb0644b 100644 --- a/services/platforms/gitee.go +++ b/services/platforms/gitee.go @@ -4,6 +4,8 @@ import ( "code-review/services/types" "code-review/utils" "fmt" + "log" + "strings" ) // GiteeEvent Gitee 平台的 webhook 事件 @@ -70,6 +72,31 @@ func (e *GiteeEvent) ExtractChanges() (*types.CodeChanges, error) { }, } + // 过滤合并提交 + validCommits := make([]struct { + ID string `json:"id"` + Message string `json:"message"` + }, 0) + + for _, commit := range e.PullRequest.Commits { + if strings.HasPrefix(commit.Message, "Merge remote-tracking branch") || + strings.HasPrefix(commit.Message, "Merge branch") { + log.Printf("跳过合并提交: commit=%s", commit.ID) + continue + } + validCommits = append(validCommits, commit) + } + + if len(validCommits) == 0 { + log.Printf("没有有效的提交记录(所有提交都是合并提交),跳过代码审查") + return nil, nil + } + + // 更新最后的提交ID + if len(validCommits) > 0 { + changes.CommitID = validCommits[len(validCommits)-1].ID + } + for _, change := range e.PullRequest.Changes { fileChange := types.FileChange{ Path: change.Path, diff --git a/services/platforms/gitlab.go b/services/platforms/gitlab.go index a2ebf3a..9abd34a 100644 --- a/services/platforms/gitlab.go +++ b/services/platforms/gitlab.go @@ -62,11 +62,29 @@ func (e *GitlabEvent) ExtractChanges() (*types.CodeChanges, error) { } // 检查是否跳过代码审查 + validCommits := make([]struct { + ID string `json:"id"` + Message string `json:"message"` + Title string `json:"title"` + }, 0) + for _, commit := range e.Commits { if strings.Contains(commit.Message, "[skip codereview]") { log.Printf("跳过代码审查: commit=%s", commit.ID) return nil, nil } + // 过滤合并提交 + if strings.HasPrefix(commit.Message, "Merge remote-tracking branch") || + strings.HasPrefix(commit.Message, "Merge branch") { + log.Printf("跳过合并提交: commit=%s", commit.ID) + continue + } + validCommits = append(validCommits, commit) + } + + if len(validCommits) == 0 { + log.Printf("没有有效的提交记录(所有提交都是合并提交),跳过代码审查") + return nil, nil } if e.client == nil { @@ -81,7 +99,7 @@ func (e *GitlabEvent) ExtractChanges() (*types.CodeChanges, error) { Files: make([]types.FileChange, 0), } - for _, commit := range e.Commits { + for _, commit := range validCommits { // 移除 URL 中的 token apiPath := fmt.Sprintf("/api/v4/projects/%d/repository/commits/%s/diff", e.Project.ID, commit.ID) diff --git a/utils/logger.go b/utils/logger.go new file mode 100644 index 0000000..378b750 --- /dev/null +++ b/utils/logger.go @@ -0,0 +1,47 @@ +package utils + +import ( + "fmt" + "io" + "log" + "os" + "path/filepath" + "time" +) + +var ( + // 日志文件句柄 + logFile *os.File + // 日志目录 + logDir = "logs" +) + +// InitLogger 初始化日志系统 +func InitLogger() error { + // 创建日志目录 + if err := os.MkdirAll(logDir, 0755); err != nil { + return fmt.Errorf("创建日志目录失败: %w", err) + } + + // 生成日志文件名 + logFileName := filepath.Join(logDir, time.Now().Format("2006-01-02")+".log") + + // 打开日志文件 + file, err := os.OpenFile(logFileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + return fmt.Errorf("打开日志文件失败: %w", err) + } + + // 设置日志输出 + log.SetOutput(io.MultiWriter(os.Stdout, file)) + logFile = file + + return nil +} + +// CloseLogger 关闭日志文件 +func CloseLogger() { + if logFile != nil { + logFile.Close() + } +}