diff --git a/cmd/gittar/conf/i18n/i18n.yaml b/cmd/gittar/conf/i18n/i18n.yaml index b8af5b171f7..be99679eb79 100644 --- a/cmd/gittar/conf/i18n/i18n.yaml +++ b/cmd/gittar/conf/i18n/i18n.yaml @@ -1,4 +1,16 @@ zh: mr.note.comment.cannot.be.empty: 评论不能为空 + template.mr.ai.cr.tip.reach.max.limit: 这里仅展示前 %d 个文件的审查结果。对于其他未审查到的文件,请移至 "**变更**" 页里,手动点击 "**AI 审查**" 按钮,或者手动选择代码片段开启 "**AI 对话**"。 + mr.ai.cr.title: AI 代码审查 + file: 文件 + snippet: 代码片段 + mr.ai.cr.no.suggestion: 你的代码变更看起来不错,暂无审查建议。 + template.mr.ai.cr.file.content.max.limit: 文件内容超长,本次审查至第 %d 行。对于其他未审查到的内容,请在 "**变更**" 页里手动选择代码片段开启 "**AI 对话**"。 en: mr.note.comment.cannot.be.empty: Comment cannot be empty + template.mr.ai.cr.tip.reach.max.limit: Only the first %d files are displayed here. For other files that have not been reviewed, please manually click the "**AI Code Review**" button in the "**Changes**" tab, or manually select the code snippet to start the "**AI Conversation**". + mr.ai.cr.title: AI Code Review + file: File + snippet: Code Snippet + mr.ai.cr.no.suggestion: Your code changes look good, no review suggestions for now. + template.mr.ai.cr.file.content.max.limit: The file content is too long, only the first %d lines are reviewed. For other content that has not been reviewed, please manually select the code snippet in "Changes" tab to start the "AI Conversation". diff --git a/internal/tools/gittar/ai/cr/impl/cr_mr/mr.go b/internal/tools/gittar/ai/cr/impl/cr_mr/mr.go index 81fa9609784..f2b4e7a2541 100644 --- a/internal/tools/gittar/ai/cr/impl/cr_mr/mr.go +++ b/internal/tools/gittar/ai/cr/impl/cr_mr/mr.go @@ -15,16 +15,26 @@ package cr_mr import ( + "fmt" + "strings" "sync" + "github.com/sirupsen/logrus" + + "github.com/erda-project/erda-infra/providers/i18n" "github.com/erda-project/erda/apistructs" "github.com/erda-project/erda/internal/tools/gittar/ai/cr/impl/cr_mr_file" + "github.com/erda-project/erda/internal/tools/gittar/ai/cr/util/mdutil" "github.com/erda-project/erda/internal/tools/gittar/ai/cr/util/mrutil" "github.com/erda-project/erda/internal/tools/gittar/models" "github.com/erda-project/erda/internal/tools/gittar/pkg/gitmodule" "github.com/erda-project/erda/pkg/limit_sync_group" ) +const ( + MaxDiffFileNum = 5 +) + type mrReviewer struct { req models.AICodeReviewNoteRequest repo *gitmodule.Repository @@ -38,18 +48,24 @@ func init() { }) } -func (r *mrReviewer) CodeReview() string { +func (r *mrReviewer) CodeReview(i18n i18n.Translator, lang i18n.LanguageCodes) string { diff := mrutil.GetDiffFromMR(r.repo, r.mr) // mr has many changed files, we will review only the first ten files one by one. Then, combine the file-level suggestions. var changedFiles []models.FileCodeReviewer + var reachMaxDiffFileNumLimit bool for _, diffFile := range diff.Files { - if len(changedFiles) >= 10 { + if len(changedFiles) >= MaxDiffFileNum { + reachMaxDiffFileNumLimit = true break } if len(diffFile.Sections) > 0 { - fr := cr_mr_file.NewFileReviewer(diffFile, r.user, r.mr) + fr, err := cr_mr_file.NewFileReviewer(diffFile.Name, r.repo, r.mr, r.user) + if err != nil { + logrus.Warnf("failed to create file reviewer for file %s, err: %v", diffFile.Name, err) + continue + } changedFiles = append(changedFiles, fr) } } @@ -57,7 +73,7 @@ func (r *mrReviewer) CodeReview() string { // parallel do file-level cr var fileOrder []string fileSuggestions := make(map[string]string) - wg := limit_sync_group.NewSemaphore(5) // parallel is 5 + wg := limit_sync_group.NewSemaphore(MaxDiffFileNum) // parallel is 5 var mu sync.Mutex wg.Add(len(changedFiles)) @@ -66,19 +82,33 @@ func (r *mrReviewer) CodeReview() string { go func(file models.FileCodeReviewer) { defer wg.Done() + fileSuggestion := file.CodeReview(i18n, lang) + if strings.TrimSpace(fileSuggestion) == "" { + fileSuggestion = i18n.Text(lang, models.I18nKeyMrAICrNoSuggestion) + } mu.Lock() - fileSuggestions[file.GetFileName()] = file.CodeReview() + fileSuggestions[file.GetFileName()] = fileSuggestion mu.Unlock() }(file) } wg.Wait() // combine result - var mrReviewResult string - mrReviewResult = "# AI Code Review\n" + var mrReviewResults []string + mrReviewResults = append(mrReviewResults, fmt.Sprintf("# %s", i18n.Text(lang, models.I18nKeyMrAICrTitle))) + if reachMaxDiffFileNumLimit { + tip := fmt.Sprintf(i18n.Text(lang, models.I18nKeyTemplateMrAICrTipForEachMaxLimit), MaxDiffFileNum) + tip = mdutil.MakeRef(mdutil.MakeItalic(tip)) + mrReviewResults = append(mrReviewResults, tip) + } + mrReviewResults = append(mrReviewResults, "") for _, fileName := range fileOrder { - mrReviewResult += "------\n## File: `" + fileName + "`\n\n" + fileSuggestions[fileName] + "\n" + mrReviewResults = append(mrReviewResults, "------") + mrReviewResults = append(mrReviewResults, fmt.Sprintf("## %s: `%s`", i18n.Text(lang, models.I18nKeyFile), fileName)) + mrReviewResults = append(mrReviewResults, "") + mrReviewResults = append(mrReviewResults, fileSuggestions[fileName]) + mrReviewResults = append(mrReviewResults, "") } - return mrReviewResult + return strings.Join(mrReviewResults, "\n") } diff --git a/internal/tools/gittar/ai/cr/impl/cr_mr_code_snippet/prompt.yaml b/internal/tools/gittar/ai/cr/impl/cr_mr_code_snippet/prompt.yaml index c6ff0b11733..55776aac659 100644 --- a/internal/tools/gittar/ai/cr/impl/cr_mr_code_snippet/prompt.yaml +++ b/internal/tools/gittar/ai/cr/impl/cr_mr_code_snippet/prompt.yaml @@ -1,10 +1,10 @@ messages: + - role: system + content: Please reply in {{.UserLang}}. + - role: system content: | Please give a review suggestions for the selected code, use markdown title for each suggestion. Code examples can be provided when necessary. The first-level title is: AI Code Review. Code: {{.SelectedCode}} - - - role: system - content: Please reply in Chinese. diff --git a/internal/tools/gittar/ai/cr/impl/cr_mr_code_snippet/snippet.go b/internal/tools/gittar/ai/cr/impl/cr_mr_code_snippet/snippet.go index 63ec7dd217f..411c8abccf7 100644 --- a/internal/tools/gittar/ai/cr/impl/cr_mr_code_snippet/snippet.go +++ b/internal/tools/gittar/ai/cr/impl/cr_mr_code_snippet/snippet.go @@ -26,8 +26,10 @@ import ( "github.com/sashabaranov/go-openai" "gopkg.in/yaml.v3" + "github.com/erda-project/erda-infra/providers/i18n" "github.com/erda-project/erda/apistructs" "github.com/erda-project/erda/internal/tools/gittar/ai/cr/util/aiutil" + "github.com/erda-project/erda/internal/tools/gittar/ai/cr/util/i18nutil" "github.com/erda-project/erda/internal/tools/gittar/models" "github.com/erda-project/erda/internal/tools/gittar/pkg/gitmodule" ) @@ -41,6 +43,7 @@ type CodeSnippet struct { CodeLanguage string SelectedCode string Truncated bool // if there are too many changes, we have to truncate the content according to the model context + UserLang string user *models.User } @@ -82,15 +85,16 @@ func newSnippetCodeReviewer(codeLang, selectedCode string, truncated bool, user return cs } -func (cs CodeSnippet) CodeReview() string { - // invoke ai - req := cs.constructAIRequest() +func (cs CodeSnippet) CodeReview(i18n i18n.Translator, lang i18n.LanguageCodes) string { + // construct AI request + req := cs.constructAIRequest(i18n, lang) // invoke return aiutil.InvokeAI(req, cs.user) } -func (cs CodeSnippet) constructAIRequest() openai.ChatCompletionRequest { +func (cs CodeSnippet) constructAIRequest(i18n i18n.Translator, lang i18n.LanguageCodes) openai.ChatCompletionRequest { + cs.UserLang = i18nutil.GetUserLang(lang) msgs := deepcopy.Copy(promptStruct.Messages).([]openai.ChatCompletionMessage) // invoke ai diff --git a/internal/tools/gittar/ai/cr/impl/cr_mr_file/fc.yaml b/internal/tools/gittar/ai/cr/impl/cr_mr_file/fc.yaml deleted file mode 100644 index db3bb87a0f3..00000000000 --- a/internal/tools/gittar/ai/cr/impl/cr_mr_file/fc.yaml +++ /dev/null @@ -1,27 +0,0 @@ -$schema: http://json-schema.org/draft-07/schema# -type: object -description: create code review note -required: - - fileReviewResult -properties: - fileReviewResult: - type: array - description: review result for each file - items: - type: object - required: - - snippetIndex - - riskLevel - - details - properties: - snippetIndex: - type: integer - description: snippet index - riskLevel: - type: string - description: | - risk level (using language understandable to ordinary people, such as: advisory, severe, fatal, etc.) - details: - type: string - description: | - details, one or array. (Summarize this code; for each issue, if possible, please provide example code for fixing) diff --git a/internal/tools/gittar/ai/cr/impl/cr_mr_file/file.go b/internal/tools/gittar/ai/cr/impl/cr_mr_file/file.go index 33455886ebc..c27b2dd90ca 100644 --- a/internal/tools/gittar/ai/cr/impl/cr_mr_file/file.go +++ b/internal/tools/gittar/ai/cr/impl/cr_mr_file/file.go @@ -17,7 +17,6 @@ package cr_mr_file import ( "bytes" _ "embed" - "encoding/json" "fmt" "path/filepath" "strings" @@ -25,46 +24,50 @@ import ( "github.com/mohae/deepcopy" "github.com/sashabaranov/go-openai" - "github.com/sirupsen/logrus" "sigs.k8s.io/yaml" + "github.com/erda-project/erda-infra/providers/i18n" "github.com/erda-project/erda/apistructs" - "github.com/erda-project/erda/internal/tools/gittar/ai/cr/impl/cr_mr_code_snippet" "github.com/erda-project/erda/internal/tools/gittar/ai/cr/util/aiutil" + "github.com/erda-project/erda/internal/tools/gittar/ai/cr/util/i18nutil" + "github.com/erda-project/erda/internal/tools/gittar/ai/cr/util/mdutil" "github.com/erda-project/erda/internal/tools/gittar/ai/cr/util/mrutil" "github.com/erda-project/erda/internal/tools/gittar/models" "github.com/erda-project/erda/internal/tools/gittar/pkg/gitmodule" - "github.com/erda-project/erda/pkg/strutil" ) type OneChangedFile struct { FileName string CodeLanguage string + FileContent string Truncated bool - CodeSnippets []cr_mr_code_snippet.CodeSnippet - mr *apistructs.MergeRequestInfo - diffFile *gitmodule.DiffFile - user *models.User + user *models.User } -func NewFileReviewer(diffFile *gitmodule.DiffFile, user *models.User, mr *apistructs.MergeRequestInfo) models.FileCodeReviewer { - fr := OneChangedFile{diffFile: diffFile, user: user, mr: mr} - fr.FileName = diffFile.Name - fr.CodeLanguage = strings.TrimPrefix(filepath.Ext(diffFile.Name), ".") - return &fr +func NewFileReviewer(filePath string, repo *gitmodule.Repository, mr *apistructs.MergeRequestInfo, user *models.User) (models.FileCodeReviewer, error) { + if filePath == "" { + return nil, fmt.Errorf("no file specified") + } + fileContent, truncated, err := mrutil.GetFileContent(repo, mr, filePath) + if err != nil { + return nil, err + } + + fr := OneChangedFile{ + FileName: filePath, + CodeLanguage: strings.TrimPrefix(filepath.Ext(filePath), "."), + FileContent: fileContent, + Truncated: truncated, + + user: user, + } + return &fr, nil } func init() { models.RegisterCodeReviewer(models.AICodeReviewTypeMRFile, func(req models.AICodeReviewNoteRequest, repo *gitmodule.Repository, mr *apistructs.MergeRequestInfo, user *models.User) (models.CodeReviewer, error) { - if req.NoteLocation.NewPath == "" { - return nil, fmt.Errorf("no file specified") - } - diffFile := mrutil.GetDiffFileFromMR(repo, mr, req.NoteLocation.NewPath) - if diffFile == nil { - return nil, fmt.Errorf("file not found") - } - return NewFileReviewer(diffFile, user, mr), nil + return NewFileReviewer(req.NoteLocation.NewPath, repo, mr, user) }) } @@ -73,43 +76,21 @@ func (r *OneChangedFile) GetFileName() string { } // CodeReview for file level, invoke once with all code snippets. -func (r *OneChangedFile) CodeReview() string { - if r.diffFile == nil { - return "" - } - r.parseCodeSnippets() - +func (r *OneChangedFile) CodeReview(i18n i18n.Translator, lang i18n.LanguageCodes) string { // invoke ai - result := aiutil.InvokeAI(r.constructAIRequest(), r.user) - if result == "" { - return "" + result := aiutil.InvokeAI(r.constructAIRequest(i18n, lang), r.user) + + // truncate + if r.Truncated { + // calculate how many lines of file content + lines := strings.Split(r.FileContent, "\n") + lineCount := len(lines) + truncatedTip := fmt.Sprintf(i18n.Text(lang, models.I18nKeyTemplateMrAICrFileContentMaxLimit), lineCount) + truncatedTip = mdutil.MakeRef(mdutil.MakeItalic(truncatedTip)) + result = truncatedTip + "\n\n" + result } - // handle response - var res FileReviewResult - if err := json.Unmarshal([]byte(result), &res); err != nil { - logrus.Warnf("failed to unmarshal ai result, err: %s", err) - } - // group result by snippet index - snippetIndexIssues := make(map[int][]FileReviewResultItem) - for _, item := range res.Result { - if item.SnippetIndex >= len(r.CodeSnippets) { - continue - } - snippetIndexIssues[item.SnippetIndex] = append(snippetIndexIssues[item.SnippetIndex], item) - } - // handle each snippet index - var s string - for snippetIndex := range r.CodeSnippets { - // add original code - s += fmt.Sprintf("**Snippet:**\n%s\n", r.CodeSnippets[snippetIndex].GetMarkdownCode()) - for _, issue := range snippetIndexIssues[snippetIndex] { - s += fmt.Sprintf("**RiskLevel:** %s\n", issue.RiskLevel) - s += fmt.Sprintf("\n%s\n\n", issue.Details) - } - s += "\n" - } - return s + return result } type ( @@ -126,10 +107,6 @@ type ( //go:embed prompt.yaml var promptYaml string -//go:embed fc.yaml -var functionDefinitionYaml string -var functionDefinition json.RawMessage - type PromptStruct = struct { Messages []openai.ChatCompletionMessage `yaml:"messages"` } @@ -140,50 +117,26 @@ func init() { if err := yaml.Unmarshal([]byte(promptYaml), &promptStruct); err != nil { panic(err) } - var err error - functionDefinition, err = strutil.YamlOrJsonToJson([]byte(functionDefinitionYaml)) - if err != nil { - panic(err) - } } -func (r *OneChangedFile) constructAIRequest() openai.ChatCompletionRequest { +func (r *OneChangedFile) constructAIRequest(i18n i18n.Translator, lang i18n.LanguageCodes) openai.ChatCompletionRequest { msgs := deepcopy.Copy(promptStruct.Messages).([]openai.ChatCompletionMessage) req := openai.ChatCompletionRequest{ Messages: msgs, Stream: false, - Functions: []openai.FunctionDefinition{ - { - Name: "create-cr-note", - Description: "create code review note", - Parameters: functionDefinition, - }, - }, - FunctionCall: openai.FunctionCall{ - Name: "create-cr-note", - }, } var tmplArgs struct { CodeLanguage string FileName string - FileContents string + FileContent string + UserLang string } tmplArgs.CodeLanguage = r.CodeLanguage tmplArgs.FileName = r.FileName - - type SnippetContent struct { - SnippetIndex int `json:"snippetIndex"` - PromptContent string `json:"promptContent"` - } - var changedFileContents []string - for i, cs := range r.CodeSnippets { - sc := SnippetContent{SnippetIndex: i, PromptContent: cs.SelectedCode} - b, _ := json.Marshal(sc) - changedFileContents = append(changedFileContents, string(b)) - } - tmplArgs.FileContents = strings.Join(changedFileContents, "\n\n") + tmplArgs.FileContent = r.FileContent + tmplArgs.UserLang = i18nutil.GetUserLang(lang) for i := range req.Messages { t, _ := template.New("").Parse(req.Messages[i].Content) @@ -197,18 +150,3 @@ func (r *OneChangedFile) constructAIRequest() openai.ChatCompletionRequest { return req } - -func (r *OneChangedFile) parseCodeSnippets() { - for _, section := range r.diffFile.Sections { - selectedCode, truncated := mrutil.ConvertDiffLinesToSnippet(section.Lines) - if selectedCode == "" { - continue - } - codeSnippet := cr_mr_code_snippet.CodeSnippet{ - CodeLanguage: strings.TrimPrefix(filepath.Ext(r.diffFile.Name), "."), - SelectedCode: selectedCode, - Truncated: truncated, - } - r.CodeSnippets = append(r.CodeSnippets, codeSnippet) - } -} diff --git a/internal/tools/gittar/ai/cr/impl/cr_mr_file/prompt.yaml b/internal/tools/gittar/ai/cr/impl/cr_mr_file/prompt.yaml index 80b981a8b11..1b494090f9a 100644 --- a/internal/tools/gittar/ai/cr/impl/cr_mr_file/prompt.yaml +++ b/internal/tools/gittar/ai/cr/impl/cr_mr_file/prompt.yaml @@ -1,28 +1,15 @@ messages: - role: system - content: | - You are an expert {{.CodeLanguage}} developer, your task is to review a set of pull requests. - You are given a filename and it's partial changed contents, but note that you might not have the full context of the code. - - Only review lines of code which have been changed (added or removed) in the pull request. The code looks similar to the output of a git diff command. Lines which have been removed are prefixed with a minus (-) and lines which have been added are prefixed with a plus (+). Other lines are added to provide context but should be ignored in the review. - - In your feedback, focus on highlighting potential bugs, improving readability if it is a problem, making code cleaner, and maximising the performance of the programming language. Flag any API keys or secrets present in the code in plain text immediately as highest risk. Rate the changes based on SOLID principles if applicable. - - Do not comment on breaking functions down into smaller, more manageable functions unless it is a huge problem. Also be aware that there will be libraries and techniques used which you are not familiar with, so do not comment on those unless you are confident that there is a problem. - - Ignore issue that: '\ No newline at end of file'. - - Use markdown formatting for the feedback details. Also do not include the filename or risk level in the feedback details. + content: Please reply in {{.UserLang}}. - Ensure the feedback details are brief, concise, accurate. If there are multiple similar issues, only comment on the most critical. - - Include brief example code snippets in the feedback details for your suggested changes when you're confident your suggestions are improvements. Use the same programming language as the file under review. - If there are multiple improvements you suggest in the feedback details, use an ordered list to indicate the priority of the changes. - - The filename is: {{.FileName}} + - role: system + content: | + You are an expert Software Engineer. + Below is a file, please help me do a brief code review on it (don't print file name and file content in your review). + Please summarize the code and identify potential problems (at most 5). Start with the most important findings. - Changed file contents to review are provided below as a list of JSON objects: - {{.FileContents}} + File `{{.FileName}}`: - - role: system - content: Please reply in Chinese. + ``` + {{.FileContent}} + ``` diff --git a/internal/tools/gittar/ai/cr/util/i18nutil/lang.go b/internal/tools/gittar/ai/cr/util/i18nutil/lang.go new file mode 100644 index 00000000000..1b4fdbbcc7e --- /dev/null +++ b/internal/tools/gittar/ai/cr/util/i18nutil/lang.go @@ -0,0 +1,47 @@ +// Copyright (c) 2021 Terminus, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package i18nutil + +import ( + "strings" + + "github.com/erda-project/erda-infra/providers/i18n" +) + +const ( + Chinese = "Chinese" + English = "English" + + CodeZh = "zh" + CodeEn = "en" +) + +func GetUserLang(langs i18n.LanguageCodes) string { + var code string + if len(langs) == 0 { + code = CodeZh + } else { + code = langs[0].RestrictedCode() + } + code = strings.ToLower(code) + switch code { + case CodeZh: + return Chinese + case CodeEn: + return English + default: + return Chinese + } +} diff --git a/internal/tools/gittar/ai/cr/util/i18nutil/lang_test.go b/internal/tools/gittar/ai/cr/util/i18nutil/lang_test.go new file mode 100644 index 00000000000..8e34551c869 --- /dev/null +++ b/internal/tools/gittar/ai/cr/util/i18nutil/lang_test.go @@ -0,0 +1,95 @@ +// Copyright (c) 2021 Terminus, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package i18nutil + +import ( + "testing" + + "github.com/erda-project/erda-infra/providers/i18n" +) + +func TestGetUserLang(t *testing.T) { + type args struct { + langs i18n.LanguageCodes + } + tests := []struct { + name string + args args + want string + }{ + { + name: "zh-CN", + args: args{ + langs: i18n.LanguageCodes{ + { + Code: "zh-CN", + }, + }, + }, + want: Chinese, + }, + { + name: "zh", + args: args{ + langs: i18n.LanguageCodes{ + { + Code: "zh", + }, + }, + }, + want: Chinese, + }, + { + name: "multi langs, use: zh", + args: args{ + langs: i18n.LanguageCodes{ + { + Code: "zh;q=0.9,en;q=0.8", + }, + }, + }, + want: Chinese, + }, + { + name: "multi langs, use: en", + args: args{ + langs: i18n.LanguageCodes{ + { + Code: "zh;q=0.7,en;q=0.8", + }, + }, + }, + want: Chinese, + }, + { + name: "unknown lang", + args: args{ + langs: i18n.LanguageCodes{ + { + Code: "jp;q=0.9,en;q=0.8", + }, + }, + }, + want: Chinese, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GetUserLang(tt.args.langs); got != tt.want { + t.Errorf("GetUserLang() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/tools/gittar/ai/cr/util/mdutil/md.go b/internal/tools/gittar/ai/cr/util/mdutil/md.go new file mode 100644 index 00000000000..aaf61f06ef2 --- /dev/null +++ b/internal/tools/gittar/ai/cr/util/mdutil/md.go @@ -0,0 +1,34 @@ +// Copyright (c) 2021 Terminus, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mdutil + +func WrapCodeBlock(code string, lang ...string) string { + if len(lang) == 0 { + return "```\n" + code + "\n```" + } + return "```" + lang[0] + "\n" + code + "\n```" +} + +func MakeItalic(text string) string { + return "_" + text + "_" +} + +func MakeBold(text string) string { + return "**" + text + "**" +} + +func MakeRef(text string) string { + return "> " + text +} diff --git a/internal/tools/gittar/ai/cr/util/mrutil/blob.go b/internal/tools/gittar/ai/cr/util/mrutil/blob.go new file mode 100644 index 00000000000..a62686f4f99 --- /dev/null +++ b/internal/tools/gittar/ai/cr/util/mrutil/blob.go @@ -0,0 +1,68 @@ +// Copyright (c) 2021 Terminus, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mrutil + +import ( + "fmt" + "io" + "net/http" + "strings" + + "github.com/erda-project/erda/apistructs" + "github.com/erda-project/erda/internal/tools/gittar/pkg/gitmodule" +) + +func GetFileContent(repo *gitmodule.Repository, mr *apistructs.MergeRequestInfo, filePath string) (fileContent string, truncated bool, err error) { + ref := mr.SourceBranch + "/" + filePath + if err = repo.ParseRefAndTreePath(ref); err != nil { + return + } + treeEntry, err := repo.GetParsedTreeEntry() + if err != nil { + return + } + if treeEntry.IsDir() { + err = fmt.Errorf("file path is a directory") + return + } + data, err := treeEntry.Blob().Data() + if err != nil { + return + } + buf := make([]byte, 1024) + n, _ := data.Read(buf) + buf = buf[:n] + contentType := http.DetectContentType(buf) + isTextFile := isTextType(contentType) + if !isTextFile { + err = fmt.Errorf("file is not text file") + return + } + d, err := io.ReadAll(data) + if err != nil { + return + } + buf = append(buf, d...) + if len(buf) > MAX_FILE_CHANGES_CHAR_SIZE { + buf = buf[:MAX_FILE_CHANGES_CHAR_SIZE] + truncated = true + } + fileContent = string(buf) + return +} + +func isTextType(contentType string) bool { + return strings.Contains(contentType, "text/") +} diff --git a/internal/tools/gittar/models/code_reviewe.go b/internal/tools/gittar/models/code_reviewe.go index 0c657148a33..0097ccbb8e3 100644 --- a/internal/tools/gittar/models/code_reviewe.go +++ b/internal/tools/gittar/models/code_reviewe.go @@ -17,12 +17,13 @@ package models import ( "fmt" + "github.com/erda-project/erda-infra/providers/i18n" "github.com/erda-project/erda/apistructs" "github.com/erda-project/erda/internal/tools/gittar/pkg/gitmodule" ) type CodeReviewer interface { - CodeReview() string + CodeReview(i18n i18n.Translator, lang i18n.LanguageCodes) string } type FileCodeReviewer interface { @@ -41,9 +42,7 @@ func RegisterCodeReviewer(t AICodeReviewType, f CodeReviewerCreateFunc) { type AICodeReviewNoteRequest struct { NoteLocation NoteRequest `json:"noteLocation"` - Type AICodeReviewType `json:"type,omitempty"` - - FileRelated *AICodeReviewRequestForFile `json:"fileRelated,omitempty"` + Type AICodeReviewType `json:"type,omitempty"` CodeSnippetRelated *AICodeReviewRequestForCodeSnippet `json:"codeSnippetRelated,omitempty"` } @@ -56,7 +55,6 @@ var ( ) type AICodeReviewRequestForMR struct{} -type AICodeReviewRequestForFile struct{} type AICodeReviewRequestForCodeSnippet struct { CodeLanguage string `json:"codeLanguage,omitempty"` // if empty, will parse by newFilePath SelectedCode string `json:"selectedCode,omitempty"` diff --git a/internal/tools/gittar/models/i18n.go b/internal/tools/gittar/models/i18n.go index 78c341edc2c..e1814d61714 100644 --- a/internal/tools/gittar/models/i18n.go +++ b/internal/tools/gittar/models/i18n.go @@ -15,5 +15,11 @@ package models const ( - I18nKeyMrNoteCommentCannotBeEmpty = "mr.note.comment.cannot.be.empty" + I18nKeyMrNoteCommentCannotBeEmpty = "mr.note.comment.cannot.be.empty" + I18nKeyTemplateMrAICrTipForEachMaxLimit = "template.mr.ai.cr.tip.reach.max.limit" + I18nKeyMrAICrTitle = "mr.ai.cr.title" + I18nKeyFile = "file" + I18nKeyCodeSnippet = "snippet" + I18nKeyMrAICrNoSuggestion = "mr.ai.cr.no.suggestion" + I18nKeyTemplateMrAICrFileContentMaxLimit = "template.mr.ai.cr.file.content.max.limit" ) diff --git a/internal/tools/gittar/models/note.go b/internal/tools/gittar/models/note.go index dfd4bb33b70..1af860080b7 100644 --- a/internal/tools/gittar/models/note.go +++ b/internal/tools/gittar/models/note.go @@ -161,7 +161,6 @@ func (svc *Service) handleAIRelatedNote(repo *gitmodule.Repository, user *User, switch req.AICodeReviewType { case AICodeReviewTypeMR: case AICodeReviewTypeMRFile: - crReq.FileRelated = &AICodeReviewRequestForFile{} case AICodeReviewTypeMRCodeSnippet: selectedCode, _ := mrutil.ConvertDiffLinesToSnippet(note.DataResult.DiffLines) crReq.CodeSnippetRelated = &AICodeReviewRequestForCodeSnippet{SelectedCode: selectedCode} @@ -170,7 +169,7 @@ func (svc *Service) handleAIRelatedNote(repo *gitmodule.Repository, user *User, if err != nil { return fmt.Errorf("failed to create code reviewer err: %v", err) } - suggestions := reviewer.CodeReview() + suggestions := reviewer.CodeReview(svc.i18nTran, svc.lang) note.Note = suggestions } diff --git a/pkg/limit_sync_group/group.go b/pkg/limit_sync_group/group.go index cd176d43111..c8f288ba8e3 100644 --- a/pkg/limit_sync_group/group.go +++ b/pkg/limit_sync_group/group.go @@ -31,9 +31,11 @@ func NewSemaphore(maxSize int) *limitSyncGroup { } func (s *limitSyncGroup) Add(delta int) { s.wg.Add(delta) - for i := 0; i < delta; i++ { - s.c <- struct{}{} - } + go func() { + for i := 0; i < delta; i++ { + s.c <- struct{}{} + } + }() } func (s *limitSyncGroup) Done() { <-s.c