-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Add pr analysis #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| package cmd | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "github.com/fatih/color" | ||
| "github.com/gsoares85/code-guardian/internal/github_internal" | ||
| "github.com/gsoares85/code-guardian/internal/openai" | ||
| "github.com/spf13/cobra" | ||
| "path/filepath" | ||
| "strings" | ||
| "time" | ||
| ) | ||
|
|
||
| var repoReviewCmd = &cobra.Command{ | ||
| Use: "repo-review [owner] [repo] [flags]", | ||
| Short: "Analyse an entire github repository", | ||
| Long: `This command scans all source files recursively in a repository | ||
| to train the AI about the application and provide: | ||
| - A summary of what the application does | ||
| - Key use cases | ||
| - A high-level code quality review (only critical issues) | ||
| - A high-level security review (only critical issues) | ||
| - Key improvement areas`, | ||
| Args: cobra.ExactArgs(2), | ||
| Run: func(cmd *cobra.Command, args []string) { | ||
| owner, repo := args[0], args[1] | ||
| saveOutput, err := cmd.Flags().GetBool("output") | ||
| if err != nil { | ||
| color.Red("Error: %v", err) | ||
| return | ||
| } | ||
|
|
||
| color.Blue("\n🔍 Fetching all source code files recursively...\n") | ||
| files, err := github_internal.GetRepositoryFilesRecursive(owner, repo) | ||
| if err != nil { | ||
| color.Red("❌ ERROR: Fetching repository files: %s\n", err) | ||
| return | ||
| } | ||
|
|
||
| color.Green("📂 Repository contains %d files\n", len(files)) | ||
|
|
||
| sourceCode := fetchAllSourceCode(owner, repo, files) | ||
| if len(sourceCode) == 0 { | ||
| color.Red("❌ ERROR: No valid source code found for analysis") | ||
| return | ||
| } | ||
|
|
||
| color.Blue("\n🤖 Training AI with full source code...\n") | ||
| summary, useCases, codeReview, securityReview, improvements := analyzeRepositoryWithAI(sourceCode) | ||
|
|
||
| displayRepoAnalysis(repo, summary, useCases, codeReview, securityReview, improvements) | ||
|
|
||
| if saveOutput { | ||
| saveRepoAnalysis(repo, owner, summary, useCases, codeReview, securityReview, improvements) | ||
| } | ||
| }, | ||
| } | ||
|
|
||
| func fetchAllSourceCode(owner, repo string, files []string) string { | ||
| var allCode strings.Builder | ||
|
|
||
| for _, file := range files { | ||
| if strings.HasSuffix(file, ".md") || strings.Contains(file, "LICENSE") { | ||
| continue | ||
| } | ||
|
|
||
| color.Cyan("\n📄 Reading file: %s", file) | ||
|
|
||
| content, err := github_internal.GetFileContent(owner, repo, file) | ||
| if err != nil { | ||
| color.Red("❌ ERROR fetching file content: %s\n", err) | ||
| continue | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
|
|
||
| allCode.WriteString(fmt.Sprintf("\n// File: %s\n%s\n", file, content)) | ||
| } | ||
|
|
||
| return allCode.String() | ||
| } | ||
|
|
||
| func analyzeRepositoryWithAI(sourceCode string) (string, string, string, string, string) { | ||
| summaryPrompt := "Analyze this entire source codebase and provide a concise summary of what the application does." | ||
| useCasesPrompt := "Extract the most important use cases from the source code." | ||
| codeQualityPrompt := "Identify the most critical code quality issues found in the source code. Provide a brief list." | ||
| securityPrompt := "Identify the most critical security vulnerabilities in the source code. Provide a brief list." | ||
| improvementPrompt := "Suggest the most important areas to improve in the application." | ||
|
|
||
| summary, _ := openai.AnalyzeCodeWithAI(sourceCode, summaryPrompt) | ||
| useCases, _ := openai.AnalyzeCodeWithAI(sourceCode, useCasesPrompt) | ||
| codeReview, _ := openai.AnalyzeCodeWithAI(sourceCode, codeQualityPrompt) | ||
| securityReview, _ := openai.AnalyzeCodeWithAI(sourceCode, securityPrompt) | ||
| improvements, _ := openai.AnalyzeCodeWithAI(sourceCode, improvementPrompt) | ||
|
|
||
| return summary, useCases, codeReview, securityReview, improvements | ||
| } | ||
|
|
||
| func displayRepoAnalysis(repo, summary, useCases, codeReview, securityReview, improvements string) { | ||
| color.Magenta("\n📌 Repository Analysis Summary for %s\n", repo) | ||
| color.Cyan("\n📖 Application Summary:\n") | ||
| fmt.Println(summary) | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| color.Green("\n✅ Key Use Cases:\n") | ||
| fmt.Println(useCases) | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same |
||
|
|
||
| color.Red("\n🚨 Code Quality Issues (Critical Only):\n") | ||
| fmt.Println(codeReview) | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same |
||
|
|
||
| color.Yellow("\n🔒 Security Issues (Critical Only):\n") | ||
| fmt.Println(securityReview) | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same |
||
|
|
||
| color.Blue("\n📈 Key Areas for Improvement:\n") | ||
| fmt.Println(improvements) | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same |
||
| } | ||
|
|
||
| func saveRepoAnalysis(repo, owner, summary, useCases, codeReview, securityReview, improvements string) { | ||
| timestamp := time.Now().Format("20060102-150405") | ||
| outputFile := fmt.Sprintf("%s-%s_%s.md", timestamp, repo, owner) | ||
| outputPath := filepath.Join("reports", "repo", outputFile) | ||
|
|
||
| content := generateRepoMarkdown(repo, summary, useCases, codeReview, securityReview, improvements) | ||
|
|
||
| if err := saveAnalysisToFile(outputPath, content); err != nil { | ||
| color.Red("❌ ERROR saving analysis: %s\n", err) | ||
| return | ||
| } | ||
| color.Green("✅ Repository analysis saved to file: %s\n", outputPath) | ||
| } | ||
|
|
||
| func generateRepoMarkdown(repo, summary, useCases, codeReview, securityReview, improvements string) string { | ||
| return fmt.Sprintf(`# Repository Analysis Report | ||
|
|
||
| ## 📂 Repository: %s | ||
|
|
||
| ## 📖 Application Summary: | ||
| %s | ||
|
|
||
| ## ✅ Key Use Cases: | ||
| %s | ||
|
|
||
| ## 🚨 Code Quality Issues (Critical Only): | ||
| %s | ||
|
|
||
| ## 🔒 Security Issues (Critical Only): | ||
| %s | ||
|
|
||
| ## 📈 Key Areas for Improvement: | ||
| %s | ||
| `, repo, summary, useCases, codeReview, securityReview, improvements) | ||
| } | ||
|
|
||
| func init() { | ||
| rootCmd.AddCommand(repoReviewCmd) | ||
| repoReviewCmd.Flags().BoolP("output", "o", false, "Save analysis to a Markdown file in ./reports/repo/") | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -57,3 +57,52 @@ func GetPullRequestFiles(owner string, repo string, prNumber int) ([]string, err | |
| } | ||
| return fileNames, nil | ||
| } | ||
|
|
||
| func GetRepositoryFilesRecursive(owner, repo string) ([]string, error) { | ||
| client := NewGithubClient() | ||
| var files []string | ||
|
|
||
| err := fetchFilesRecursive(client, owner, repo, "", &files) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("error fetching repository files: %w", err) | ||
| } | ||
|
|
||
| return files, nil | ||
| } | ||
|
|
||
| func fetchFilesRecursive(client *github.Client, owner, repo, path string, files *[]string) error { | ||
| contents, dirContents, _, err := client.Repositories.GetContents(context.Background(), owner, repo, path, nil) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if contents != nil { | ||
| *files = append(*files, contents.GetPath()) | ||
| return nil | ||
| } | ||
|
|
||
| for _, item := range dirContents { | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| if item.GetType() == "file" { | ||
| *files = append(*files, item.GetPath()) | ||
| } else if item.GetType() == "dir" { | ||
| fetchFilesRecursive(client, owner, repo, item.GetPath(), files) | ||
| } | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func GetFileContent(owner, repo, path string) (string, error) { | ||
| client := NewGithubClient() | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| fileContent, _, _, err := client.Repositories.GetContents(context.Background(), owner, repo, path, nil) | ||
| if err != nil { | ||
| return "", fmt.Errorf("error fetching file content: %w", err) | ||
| } | ||
|
|
||
| content, err := fileContent.GetContent() | ||
| if err != nil { | ||
| return "", fmt.Errorf("error decoding file content: %w", err) | ||
| } | ||
|
|
||
| return content, nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ import ( | |
| "fmt" | ||
| "github.com/gsoares85/code-guardian/config" | ||
| "github.com/sashabaranov/go-openai" | ||
| "strings" | ||
| ) | ||
|
|
||
| func AnalyzePRWithAI(diff string) (string, error) { | ||
|
|
@@ -43,3 +44,55 @@ func AnalyzePRWithAI(diff string) (string, error) { | |
|
|
||
| return resp.Choices[0].Message.Content, nil | ||
| } | ||
|
|
||
| func AnalyzeCodeWithAI(code string, prompt string) (string, error) { | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| apiKey := config.GetEnv("OPENAI_API_KEY") | ||
| if apiKey == "" { | ||
| return "", fmt.Errorf("missing OpenAI API key") | ||
| } | ||
|
|
||
| client := openai.NewClient(apiKey) | ||
|
|
||
| codeChunks := SplitLargeCode(code, 3000) | ||
|
|
||
| var fullResponse strings.Builder | ||
|
|
||
| for _, chunk := range codeChunks { | ||
| requestPrompt := fmt.Sprintf("%s\n\n%s", prompt, chunk) | ||
|
|
||
| resp, err := client.CreateChatCompletion(context.Background(), openai.ChatCompletionRequest{ | ||
| Model: openai.GPT4, | ||
| Messages: []openai.ChatCompletionMessage{ | ||
| {Role: "system", Content: "You are a senior software engineer reviewing code."}, | ||
| {Role: "user", Content: requestPrompt}, | ||
| }, | ||
| }) | ||
|
|
||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| if len(resp.Choices) == 0 { | ||
| return "", fmt.Errorf("empty response from OpenAI") | ||
| } | ||
|
|
||
| fullResponse.WriteString(resp.Choices[0].Message.Content + "\n\n") | ||
| } | ||
|
|
||
| return fullResponse.String(), nil | ||
| } | ||
|
|
||
| func SplitLargeCode(code string, maxTokens int) []string { | ||
| words := strings.Fields(code) | ||
| var chunks []string | ||
|
|
||
| for i := 0; i < len(words); i += maxTokens { | ||
| end := i + maxTokens | ||
| if end > len(words) { | ||
| end = len(words) | ||
| } | ||
| chunks = append(chunks, strings.Join(words[i:end], " ")) | ||
| } | ||
|
|
||
| return chunks | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cmd/pr_review_test.gofile are not properly handling the errors from the Mock functions. For example, in theTestGenerateMarkdownReporttest case, it might be beneficial to asserterrisnilafter each Mock function call to ensure there are no errors.