diff --git a/.opencode-review/agents/code-review.md b/.opencode-review/agents/code-review.md index 51a3fec..c03c6a1 100644 --- a/.opencode-review/agents/code-review.md +++ b/.opencode-review/agents/code-review.md @@ -1,5 +1,5 @@ --- -description: AI code reviewer for Gitea/Forgejo PRs +description: AI code reviewer for Gitea/Forgejo PRs with multi-language support # Model is configured via MODEL env var or opencode.json # Examples: anthropic/claude-sonnet-4-5, deepseek/deepseek-chat, openai/gpt-4o color: "#44BA81" @@ -7,69 +7,82 @@ tools: "*": false "gitea-review": true "gitea-pr-diff": true - "gitea-comment": true - "read": true + "gitea-pr-files": true --- -You are an expert code reviewer. Your job is to review pull requests and provide constructive feedback. +You are an expert code reviewer specializing in identifying bugs, security issues, and code quality improvements. -## Workflow +## Language / 语言 -1. **First**, use the `gitea-pr-diff` tool to fetch the PR diff -2. **Analyze** the code changes carefully -3. **Use `gitea-review` tool** to submit your review +**IMPORTANT**: Check the `REVIEW_LANGUAGE` environment variable and respond accordingly: +- If `REVIEW_LANGUAGE=zh-CN` or `REVIEW_LANGUAGE=zh`: Respond entirely in **简体中文** +- If `REVIEW_LANGUAGE=en`: Respond entirely in **English** +- If `REVIEW_LANGUAGE=auto` or not set: Detect from code comments/context and use that language -## Review Summary Format +## Core Principles -When writing the `summary` field, use this clean Markdown format: +1. **ONLY review code from the diff** - Do NOT request or read full files +2. **Focus on changed code** - Context lines are for reference only +3. **Be constructive** - Provide actionable suggestions, not just criticism +4. **Be concise** - Quality over quantity in feedback -``` -## Review Summary +## Workflow -**Changes Overview:** -- Brief description of what this PR does +1. **Optionally** use `gitea-pr-files` to see changed files list (for filtering) +2. **Use `gitea-pr-diff`** to fetch the actual code changes + - Use `file_patterns` param to filter specific files (e.g., `["*.ts", "*.go"]`) +3. **Analyze** only the changed lines (marked with `+` in diff) +4. **Submit review** using `gitea-review` tool -**What's Good:** ✅ -- Positive aspect 1 -- Positive aspect 2 +## Review Focus Areas -**Issues Found:** ⚠️ -- Issue 1 with explanation -- Issue 2 with explanation +| Priority | Category | What to Look For | +|----------|----------|------------------| +| 🔴 Critical | **Security** | SQL injection, XSS, hardcoded secrets, auth bypass | +| 🔴 Critical | **Bugs** | Logic errors, null/undefined access, race conditions | +| 🟡 Important | **Performance** | N+1 queries, memory leaks, inefficient algorithms | +| 🟢 Suggestion | **Quality** | Naming, error handling, code duplication | -**Suggestions:** 💡 -1. Suggestion 1 -2. Suggestion 2 -``` +## Review Summary Format + +```markdown +## 📋 Review Summary -Do NOT use `\n` escape sequences - just use actual line breaks in your text. +**Overview**: [One sentence describing what this PR does] -## Review Guidelines +### ✅ Strengths +- [Positive point 1] +- [Positive point 2] -Focus on: -- 🐛 **Bugs**: Logic errors, off-by-one errors, null pointer issues -- 🔒 **Security**: SQL injection, XSS, hardcoded secrets, auth issues -- ⚡ **Performance**: N+1 queries, unnecessary loops, memory leaks -- 📖 **Readability**: Unclear naming, missing comments, complex logic -- ✅ **Best Practices**: Error handling, type safety, testing +### ⚠️ Issues Found +- **[Category]**: [Issue description] → [Suggested fix] + +### 💡 Suggestions +- [Optional improvement 1] +``` -## Line Comment Format +## Line Comment Guidelines -Keep line comments concise and actionable: -- One issue per comment -- Include a suggestion to fix -- Use code blocks for examples if helpful +- **One issue per comment** - Don't combine multiple concerns +- **Include fix suggestion** - Show the better approach +- **Use code blocks** when suggesting code changes: + ``` + Consider using: + `const value = data ?? defaultValue;` + ``` -## Approval Decision +## Approval Decision Matrix -- `approve`: Code looks good, no blocking issues -- `request_changes`: Critical issues that must be fixed before merge -- `comment`: General feedback, neither approving nor blocking +| Situation | Decision | +|-----------|----------| +| No issues or only minor style suggestions | `approve` | +| Has bugs, security issues, or logic errors | `request_changes` | +| Only has questions or optional improvements | `comment` | -## Important Rules +## Rules -1. Only comment on lines that appear in the diff -2. Use the exact line numbers from `gitea-pr-diff` output -3. Always use `gitea-review` tool to submit (not `gitea-comment`) -4. Keep the summary under 500 words -5. Use real line breaks, not escape sequences +1. **Diff-only review**: Never ask to read complete files +2. **Accurate line numbers**: Use exact `[LINE_NUM]` from diff output +3. **Single tool for submission**: Always use `gitea-review`, not `gitea-comment` +4. **Respect filters**: If `file_patterns` is set, only review matching files +5. **No escape sequences**: Use real line breaks in summary text diff --git a/.opencode-review/tools/gitea-pr-diff.ts b/.opencode-review/tools/gitea-pr-diff.ts index 9975f7d..210604c 100644 --- a/.opencode-review/tools/gitea-pr-diff.ts +++ b/.opencode-review/tools/gitea-pr-diff.ts @@ -146,6 +146,56 @@ function formatDiffForReview(files: ParsedFile[]): string { return parts.join("\n") } +function matchPattern(filename: string, pattern: string): boolean { + // Convert glob pattern to regex + // Supports: *, **, ?, [abc], [!abc] + const regexPattern = pattern + .replace(/\./g, "\\.") + .replace(/\*\*/g, "{{GLOBSTAR}}") + .replace(/\*/g, "[^/]*") + .replace(/{{GLOBSTAR}}/g, ".*") + .replace(/\?/g, ".") + + const regex = new RegExp(`^${regexPattern}$`) + return regex.test(filename) +} + +function filterFilesByPatterns(files: ParsedFile[], patterns: string[]): ParsedFile[] { + if (!patterns || patterns.length === 0) { + return files + } + + return files.filter(file => + patterns.some(pattern => matchPattern(file.path, pattern)) + ) +} + +function formatAsRawDiff(files: ParsedFile[]): string { + // Reconstruct a simplified raw diff from parsed files + const parts: string[] = [] + + for (const file of files) { + parts.push(`diff --git a/${file.path} b/${file.path}`) + + for (const hunk of file.hunks) { + parts.push(`@@ -${hunk.oldStart},0 +${hunk.newStart},0 @@`) + + for (const change of hunk.changes) { + if (change.type === "add") { + parts.push(`+${change.content}`) + } else if (change.type === "del") { + parts.push(`-${change.content}`) + } else { + parts.push(` ${change.content}`) + } + } + } + parts.push("") + } + + return parts.join("\n") +} + export default tool({ description: DESCRIPTION, args: { @@ -157,24 +207,42 @@ export default tool({ .optional() .default("parsed") .describe("Output format: 'raw' for unified diff, 'parsed' for line-numbered format"), + file_patterns: tool.schema + .array(tool.schema.string()) + .optional() + .describe("Optional glob patterns to filter files (e.g., ['*.ts', 'src/**/*.go']). Only matching files will be included in the diff."), }, async execute(args) { - const { owner, repo, pull_number, format = "parsed" } = args + const { owner, repo, pull_number, format = "parsed", file_patterns } = args const response = await giteaFetch(`/repos/${owner}/${repo}/pulls/${pull_number}.diff`, { headers: { Accept: "text/plain" }, }) const diffText = await response.text() + let parsed = parseDiff(diffText) + + // Apply file filtering if patterns provided + if (file_patterns && file_patterns.length > 0) { + parsed = filterFilesByPatterns(parsed, file_patterns) + } + if (format === "raw") { + // For raw format with filtering, we reconstruct from parsed + if (file_patterns && file_patterns.length > 0) { + return formatAsRawDiff(parsed) + } return diffText } - const parsed = parseDiff(diffText) const formatted = formatDiffForReview(parsed) const summary = parsed.map((f) => `- ${f.path} (${f.status})`).join("\n") + + const filterNote = file_patterns && file_patterns.length > 0 + ? `\n**Filter:** ${file_patterns.join(", ")}\n` + : "" - return `# PR #${pull_number} Diff\n\n**Files Changed:**\n${summary}\n\n---\n${formatted}` + return `# PR #${pull_number} Diff\n${filterNote}\n**Files Changed:**\n${summary}\n\n---\n${formatted}` }, }) diff --git a/.opencode-review/tools/gitea-pr-diff.txt b/.opencode-review/tools/gitea-pr-diff.txt index 1cf4961..39b680e 100644 --- a/.opencode-review/tools/gitea-pr-diff.txt +++ b/.opencode-review/tools/gitea-pr-diff.txt @@ -1,16 +1,23 @@ Fetch the diff (code changes) for a Gitea/Forgejo pull request. -This tool retrieves all file changes in a PR with line numbers. +This tool retrieves file changes in a PR with line numbers for review. + +Features: +- Supports file filtering with glob patterns (e.g., "*.ts", "src/**/*.go") +- Two output formats: parsed (with line numbers) or raw (unified diff) Output formats: - "parsed" (default): Shows each line with [LINE_NUM] prefix for easy reference when creating review comments - "raw": Returns the raw unified diff format -Use this tool before submitting a code review to understand what changes were made. - The parsed format shows: - [42] +new line - Added line at line 42 - [DEL] -old line - Deleted line - [42] context - Unchanged context line +File filtering examples: +- ["*.ts", "*.tsx"] - Only TypeScript files +- ["src/**"] - Only files in src directory +- ["!*.test.ts"] - Exclude test files (not supported, use positive patterns) + Use the line numbers in [brackets] when creating review comments with gitea-review tool. diff --git a/.opencode-review/tools/gitea-pr-files.ts b/.opencode-review/tools/gitea-pr-files.ts new file mode 100644 index 0000000..a242fdf --- /dev/null +++ b/.opencode-review/tools/gitea-pr-files.ts @@ -0,0 +1,149 @@ +/// +import { tool } from "@opencode-ai/plugin" + +/** + * Gitea/Forgejo PR Files Tool + * + * This MCP tool fetches the list of changed files in a pull request. + * Useful for filtering which files to review. + * + * Environment Variables: + * GITEA_TOKEN - API token for Gitea/Forgejo + * GITEA_SERVER_URL - Base URL (e.g., https://gitea.example.com) + */ + +async function giteaFetch(endpoint: string, options: RequestInit = {}) { + const baseUrl = process.env.GITEA_SERVER_URL || process.env.GITHUB_SERVER_URL + const token = process.env.GITEA_TOKEN || process.env.GITHUB_TOKEN + + if (!baseUrl) throw new Error("GITEA_SERVER_URL environment variable is required") + if (!token) throw new Error("GITEA_TOKEN environment variable is required") + + const url = `${baseUrl}/api/v1${endpoint}` + const response = await fetch(url, { + ...options, + headers: { + Authorization: `token ${token}`, + Accept: "application/json", + ...options.headers, + }, + }) + + if (!response.ok) { + const text = await response.text() + throw new Error(`Gitea API error: ${response.status} ${response.statusText} - ${text}`) + } + + return response.json() +} + +interface ChangedFile { + filename: string + status: string + additions: number + deletions: number + changes: number +} + +function matchPattern(filename: string, pattern: string): boolean { + // Convert glob pattern to regex + // Supports: *, **, ?, [abc], [!abc] + const regexPattern = pattern + .replace(/\./g, "\\.") + .replace(/\*\*/g, "{{GLOBSTAR}}") + .replace(/\*/g, "[^/]*") + .replace(/{{GLOBSTAR}}/g, ".*") + .replace(/\?/g, ".") + + const regex = new RegExp(`^${regexPattern}$`) + return regex.test(filename) +} + +function filterFiles(files: ChangedFile[], patterns: string[]): ChangedFile[] { + if (!patterns || patterns.length === 0) { + return files + } + + return files.filter(file => + patterns.some(pattern => matchPattern(file.filename, pattern)) + ) +} + +const DESCRIPTION = `List all files changed in a Gitea/Forgejo pull request. + +This tool returns a summary of all changed files with their status (added/modified/deleted) and line counts. + +Use this tool to: +- Get an overview of what files were changed in a PR +- Filter files by pattern before fetching the full diff +- Understand the scope of changes + +The output includes: +- File path +- Status (added, modified, deleted, renamed) +- Lines added/deleted + +Use the file_patterns parameter to filter results by glob patterns (e.g., "*.ts", "src/**/*.go").` + +export default tool({ + description: DESCRIPTION, + args: { + owner: tool.schema.string().describe("Repository owner"), + repo: tool.schema.string().describe("Repository name"), + pull_number: tool.schema.number().describe("Pull request number"), + file_patterns: tool.schema + .array(tool.schema.string()) + .optional() + .describe("Optional glob patterns to filter files (e.g., ['*.ts', 'src/**/*.go'])"), + }, + async execute(args) { + const { owner, repo, pull_number, file_patterns } = args + + const files: ChangedFile[] = await giteaFetch(`/repos/${owner}/${repo}/pulls/${pull_number}/files`) + + const filteredFiles = filterFiles(files, file_patterns || []) + + // Build summary + const stats = { + total: filteredFiles.length, + added: filteredFiles.filter(f => f.status === "added").length, + modified: filteredFiles.filter(f => f.status === "modified" || f.status === "changed").length, + deleted: filteredFiles.filter(f => f.status === "deleted" || f.status === "removed").length, + renamed: filteredFiles.filter(f => f.status === "renamed").length, + } + + const totalAdditions = filteredFiles.reduce((sum, f) => sum + (f.additions || 0), 0) + const totalDeletions = filteredFiles.reduce((sum, f) => sum + (f.deletions || 0), 0) + + // Format output + const lines: string[] = [ + `# PR #${pull_number} Changed Files`, + "", + `**Total:** ${stats.total} file(s) | +${totalAdditions} -${totalDeletions}`, + ] + + if (file_patterns && file_patterns.length > 0) { + lines.push(`**Filter:** ${file_patterns.join(", ")}`) + lines.push(`**Matched:** ${filteredFiles.length} of ${files.length} files`) + } + + lines.push("") + lines.push("| Status | File | Changes |") + lines.push("|--------|------|---------|") + + for (const file of filteredFiles) { + const statusIcon = { + added: "🆕", + modified: "📝", + changed: "📝", + deleted: "🗑️", + removed: "🗑️", + renamed: "📋", + }[file.status] || "📄" + + lines.push(`| ${statusIcon} ${file.status} | \`${file.filename}\` | +${file.additions || 0} -${file.deletions || 0} |`) + } + + return lines.join("\n") + }, +}) diff --git a/README.md b/README.md index 8e2d82d..096232f 100644 --- a/README.md +++ b/README.md @@ -75,12 +75,41 @@ env: # MODEL: openai/gpt-4o # Requires OPENAI_API_KEY ``` -### 3. Docker-specific Options +### 3. Review Configuration + +These options work with both Docker and Source installations: ```yaml env: + # Response language REVIEW_LANGUAGE: auto # auto | en | zh-CN + + # Review depth and focus REVIEW_STYLE: balanced # concise | balanced | thorough | security + + # File filtering (glob patterns, comma-separated) + FILE_PATTERNS: "" # e.g., "*.ts,*.go,src/**" (empty = all files) +``` + +#### Language Options + +| Value | Description | +|-------|-------------| +| `auto` | Auto-detect from code comments (default) | +| `en` | Review in English | +| `zh-CN` | 使用简体中文审查 | + +#### File Filtering Examples + +```yaml +# Only review TypeScript files +FILE_PATTERNS: "*.ts,*.tsx" + +# Only review source files (exclude tests) +FILE_PATTERNS: "src/**/*.go" + +# Multiple patterns +FILE_PATTERNS: "*.py,*.js,!*.test.js" ``` ## 🚀 Usage diff --git a/README_zh.md b/README_zh.md index 7958311..5c9c14d 100644 --- a/README_zh.md +++ b/README_zh.md @@ -75,12 +75,41 @@ env: # MODEL: openai/gpt-4o # 需要 OPENAI_API_KEY ``` -### 3. Docker 专属选项 +### 3. 审查配置 + +以下选项适用于 Docker 和源码两种安装方式: ```yaml env: + # 响应语言 REVIEW_LANGUAGE: auto # auto | en | zh-CN + + # 审查深度和关注点 REVIEW_STYLE: balanced # concise | balanced | thorough | security + + # 文件筛选(glob 模式,逗号分隔) + FILE_PATTERNS: "" # 例如:「*.ts,*.go,src/**」(空 = 全部文件) +``` + +#### 语言选项 + +| 值 | 说明 | +|----|------| +| `auto` | 根据代码注释自动检测(默认) | +| `en` | 使用英文审查 | +| `zh-CN` | 使用简体中文审查 | + +#### 文件筛选示例 + +```yaml +# 只审查 TypeScript 文件 +FILE_PATTERNS: "*.ts,*.tsx" + +# 只审查源码文件(排除测试) +FILE_PATTERNS: "src/**/*.go" + +# 多种模式 +FILE_PATTERNS: "*.py,*.js,!*.test.js" ``` ## 🚀 使用方法 diff --git a/entrypoint.sh b/entrypoint.sh index 6f91830..3ee7c3f 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -74,7 +74,7 @@ build_prompt() { local repo_owner="${REPO_OWNER:-}" local repo_name="${REPO_NAME:-}" - local prompt="Please review" + local prompt="Review" if [ -n "$pr_num" ]; then prompt="$prompt PR #$pr_num" @@ -84,26 +84,28 @@ build_prompt() { prompt="$prompt in $repo_owner/$repo_name" fi + prompt="$prompt." + # Add style instructions case "${REVIEW_STYLE:-balanced}" in concise) - prompt="$prompt. Be concise, focus only on critical issues." + prompt="$prompt Focus only on critical issues, be concise." ;; thorough) - prompt="$prompt. Provide thorough analysis including suggestions, best practices, and potential improvements." + prompt="$prompt Provide thorough analysis including best practices and improvements." ;; security) - prompt="$prompt. Focus on security vulnerabilities and potential risks." + prompt="$prompt Focus on security vulnerabilities and potential risks." ;; *) - prompt="$prompt. Provide balanced feedback on code quality, bugs, and improvements." + prompt="$prompt Provide balanced feedback on bugs, security, and code quality." ;; esac # Add language preference case "${REVIEW_LANGUAGE:-auto}" in zh-CN|zh) - prompt="$prompt Reply in Chinese (简体中文)." + prompt="$prompt 请使用简体中文回复。" ;; en) prompt="$prompt Reply in English." @@ -111,6 +113,11 @@ build_prompt() { # auto: let the model decide based on code content esac + # Add file filter instructions + if [ -n "$FILE_PATTERNS" ]; then + prompt="$prompt Only review files matching: $FILE_PATTERNS." + fi + echo "$prompt" } @@ -121,6 +128,9 @@ print_config() { echo " Style: ${REVIEW_STYLE:-balanced}" echo " Language: ${REVIEW_LANGUAGE:-auto}" echo " Config: $OPENCODE_CONFIG_DIR" + if [ -n "$FILE_PATTERNS" ]; then + echo " Filter: $FILE_PATTERNS" + fi if [ -n "$PR_NUMBER" ]; then echo " PR: #$PR_NUMBER" fi @@ -176,6 +186,7 @@ main() { echo " MODEL AI model (default: deepseek/deepseek-chat)" echo " REVIEW_LANGUAGE auto|en|zh-CN (default: auto)" echo " REVIEW_STYLE concise|balanced|thorough|security (default: balanced)" + echo " FILE_PATTERNS Glob patterns to filter files (e.g., '*.ts,*.go')" echo " PR_NUMBER PR number to review" echo " REPO_OWNER Repository owner" echo " REPO_NAME Repository name" diff --git a/templates/workflow-docker.yaml b/templates/workflow-docker.yaml index 0c4f297..7a9b21c 100644 --- a/templates/workflow-docker.yaml +++ b/templates/workflow-docker.yaml @@ -47,9 +47,10 @@ jobs: # Examples: deepseek/deepseek-chat, anthropic/claude-sonnet-4-5, openai/gpt-4o MODEL: deepseek/deepseek-chat - # Review behavior - # REVIEW_LANGUAGE: auto # auto | en | zh-CN - # REVIEW_STYLE: balanced # concise | balanced | thorough | security + # Review configuration + REVIEW_LANGUAGE: auto # auto | en | zh-CN + # REVIEW_STYLE: balanced # concise | balanced | thorough | security + # FILE_PATTERNS: "" # Glob patterns to filter files (e.g., "*.ts,*.go,src/**") # LLM API Keys (configure the one matching your MODEL) DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} diff --git a/templates/workflow-source.yaml b/templates/workflow-source.yaml index 0ad5ed3..8749985 100644 --- a/templates/workflow-source.yaml +++ b/templates/workflow-source.yaml @@ -59,6 +59,11 @@ jobs: GITEA_TOKEN: ${{ secrets.OPENCODE_GIT_TOKEN }} GITEA_SERVER_URL: ${{ gitea.server_url }} + # Review configuration + REVIEW_LANGUAGE: auto # auto | en | zh-CN + # REVIEW_STYLE: balanced # concise | balanced | thorough | security + # FILE_PATTERNS: "" # Glob patterns to filter files (e.g., "*.ts,*.go,src/**") + # LLM API Keys (configure the one matching your MODEL) DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} @@ -72,7 +77,20 @@ jobs: # Logging OPENCODE_LOG_LEVEL: info run: | + # Build language instruction + LANG_INSTRUCTION="" + case "${REVIEW_LANGUAGE:-auto}" in + zh-CN|zh) LANG_INSTRUCTION="请使用中文回复。" ;; + en) LANG_INSTRUCTION="Reply in English." ;; + esac + + # Build file filter instruction + FILE_INSTRUCTION="" + if [ -n "$FILE_PATTERNS" ]; then + FILE_INSTRUCTION="Only review files matching: $FILE_PATTERNS." + fi + opencode run --agent code-review \ - "Please review PR #${PR_NUMBER} in ${REPO_OWNER}/${REPO_NAME}. \ - Analyze the code changes, identify issues, suggest improvements, \ - and submit a review with inline comments." + "Review PR #${PR_NUMBER} in ${REPO_OWNER}/${REPO_NAME}. \ + Analyze code changes, identify issues, and submit review with inline comments. \ + ${LANG_INSTRUCTION} ${FILE_INSTRUCTION}"