Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion .opencode-review/agents/code-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,34 @@ Available tools:

## Line Comment Guidelines

### Line Number Format in Diff Output

The diff output uses these line number formats:
- `[NEW:123]` - Line 123 in the **new file** (added or context lines)
- `[OLD:456]` - Line 456 in the **old file** (deleted lines)

### When Submitting Comments

- For lines marked `[NEW:X]` (added `+` or context lines): use `line: X`
- For lines marked `[OLD:X]` (deleted `-` lines): use `old_line: X`

Example:
```json
{
"path": "src/app.ts",
"line": 42, // For [NEW:42] lines
"body": "Consider using const here"
}
// OR for deleted lines:
{
"path": "src/app.ts",
"old_line": 38, // For [OLD:38] lines
"body": "This logic should be preserved"
}
```

### Best Practices

- **One issue per comment** - Don't combine multiple concerns
- **Include fix suggestion** - Show the better approach
- **Use code blocks** when suggesting code changes:
Expand Down Expand Up @@ -114,7 +142,10 @@ gitea-review {
repo: "<repo_name>",
pull_number: <pr_number>,
summary: "<your review summary>",
comments: [...], // optional line comments
comments: [
{ path: "file.ts", line: 42, body: "..." }, // For [NEW:42] lines
{ path: "file.ts", old_line: 38, body: "..." } // For [OLD:38] deleted lines
],
approval: "approve" | "comment" | "request_changes"
}
```
Expand Down
222 changes: 222 additions & 0 deletions .opencode-review/tests/gitea-pr-diff.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
/**
* 本地验证 diff 解析逻辑
* 运行: cd .opencode-review && bun run gitea-pr-diff.test.ts
*/

// 模拟 parseDiff 和 formatDiffForReview 函数
interface ParsedChange {
type: "add" | "del" | "normal"
content: string
oldLine?: number
newLine?: number
}

interface ParsedHunk {
oldStart: number
newStart: number
changes: ParsedChange[]
}

interface ParsedFile {
path: string
status: string
hunks: ParsedHunk[]
}

function parseDiff(diffText: string): ParsedFile[] {
const files: ParsedFile[] = []
const lines = diffText.split("\n")
let i = 0

while (i < lines.length) {
const line = lines[i]

if (line.startsWith("diff --git")) {
const pathMatch = line.match(/b\/(.+)$/)
const path = pathMatch ? pathMatch[1] : "unknown"

let status = "modified"
while (i < lines.length && !lines[i].startsWith("@@")) {
if (lines[i].startsWith("new file")) status = "added"
else if (lines[i].startsWith("deleted file")) status = "deleted"
else if (lines[i].startsWith("rename")) status = "renamed"
i++
}

const hunks: ParsedHunk[] = []

while (i < lines.length && !lines[i].startsWith("diff --git")) {
if (lines[i].startsWith("@@")) {
const hunkMatch = lines[i].match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/)
if (hunkMatch) {
const oldStart = parseInt(hunkMatch[1], 10)
const newStart = parseInt(hunkMatch[2], 10)
const changes: ParsedChange[] = []
let oldLine = oldStart
let newLine = newStart

i++
while (i < lines.length && !lines[i].startsWith("@@") && !lines[i].startsWith("diff --git")) {
const content = lines[i].slice(1)
if (lines[i].startsWith("+")) {
changes.push({ type: "add", content, newLine: newLine++ })
} else if (lines[i].startsWith("-")) {
changes.push({ type: "del", content, oldLine: oldLine++ })
} else if (lines[i].startsWith(" ")) {
changes.push({ type: "normal", content, oldLine: oldLine++, newLine: newLine++ })
}
i++
}

hunks.push({ oldStart, newStart, changes })
} else {
i++
}
} else {
i++
}
}

files.push({ path, status, hunks })
} else {
i++
}
}

return files
}

function formatDiffForReview(files: ParsedFile[]): string {
const parts: string[] = []

for (const file of files) {
parts.push(`\n## ${file.path} (${file.status})\n`)

for (const hunk of file.hunks) {
parts.push(`@@ starting at line ${hunk.newStart} (old: ${hunk.oldStart}) @@`)

for (const change of hunk.changes) {
if (change.type === "add") {
parts.push(`[NEW:${change.newLine}] +${change.content}`)
} else if (change.type === "del") {
parts.push(`[OLD:${change.oldLine}] -${change.content}`)
} else {
parts.push(`[NEW:${change.newLine}] ${change.content}`)
}
}
parts.push("")
}
}

return parts.join("\n")
}

// ============ 测试用例 ============

const testDiff = `diff --git a/src/app.ts b/src/app.ts
index 1234567..abcdefg 100644
--- a/src/app.ts
+++ b/src/app.ts
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test diff string has incorrect formatting. Line 119 shows +++ b/src/app.ts (the standard git diff format for the new file path), but according to the diff view, there's an extra + character at line 119, making it ++++ b/src/app.ts. This is incorrect git diff syntax and will likely cause the parseDiff function to fail or skip this file header.

The correct format should be:

--- a/src/app.ts
+++ b/src/app.ts

Not:

--- a/src/app.ts
++++ b/src/app.ts

Copilot uses AI. Check for mistakes.
@@ -10,7 +10,8 @@ function main() {
const config = loadConfig()
console.log("Starting app")
- const oldValue = "deprecated"
+ const newValue = "updated"
+ const extraLine = "added"
return config
}
`

console.log("=== 测试 Diff 解析 ===\n")

const parsed = parseDiff(testDiff)
console.log("解析结果:")
console.log(JSON.stringify(parsed, null, 2))

console.log("\n=== 格式化输出 ===\n")

const formatted = formatDiffForReview(parsed)
console.log(formatted)

console.log("\n=== 验证检查 ===\n")

// 验证删除行有 oldLine
const delChange = parsed[0]?.hunks[0]?.changes.find(c => c.type === "del")
if (delChange && delChange.oldLine) {
console.log(`✅ 删除行有 oldLine: ${delChange.oldLine}`)
} else {
console.log("❌ 删除行缺少 oldLine")
}

// 验证添加行有 newLine
const addChange = parsed[0]?.hunks[0]?.changes.find(c => c.type === "add")
if (addChange && addChange.newLine) {
console.log(`✅ 添加行有 newLine: ${addChange.newLine}`)
} else {
console.log("❌ 添加行缺少 newLine")
}

// 验证格式化输出包含 [OLD:X] 和 [NEW:X]
if (formatted.includes("[OLD:") && formatted.includes("[NEW:")) {
console.log("✅ 格式化输出包含 [OLD:X] 和 [NEW:X] 格式")
} else {
console.log("❌ 格式化输出格式不正确")
}

console.log("\n=== 模拟 Review 评论映射 ===\n")

// 模拟 gitea-review 的评论映射逻辑
interface Comment {
path: string
line?: number
old_line?: number
body: string
}

function mapComment(c: Comment) {
const result: { path: string; body: string; new_position?: number; old_position?: number } = {
path: c.path,
body: c.body,
}

if (c.old_line !== undefined && c.old_line > 0) {
result.old_position = c.old_line
result.new_position = 0
} else if (c.line !== undefined && c.line > 0) {
result.new_position = c.line
result.old_position = 0
}

return result
}

// 测试评论映射
const testComments: Comment[] = [
{ path: "src/app.ts", line: 13, body: "Consider const here" },
{ path: "src/app.ts", old_line: 12, body: "This was important logic" },
]

console.log("输入评论:")
console.log(JSON.stringify(testComments, null, 2))

console.log("\n映射到 Gitea API 格式:")
const mapped = testComments.map(mapComment)
console.log(JSON.stringify(mapped, null, 2))

// 验证映射结果
const newLineComment = mapped.find(m => m.new_position && m.new_position > 0)
const oldLineComment = mapped.find(m => m.old_position && m.old_position > 0)

if (newLineComment && newLineComment.old_position === 0) {
console.log("\n✅ 新行评论正确映射: new_position=" + newLineComment.new_position + ", old_position=0")
} else {
console.log("\n❌ 新行评论映射错误")
}

if (oldLineComment && oldLineComment.new_position === 0) {
console.log("✅ 旧行评论正确映射: old_position=" + oldLineComment.old_position + ", new_position=0")
} else {
console.log("❌ 旧行评论映射错误")
}

console.log("\n=== 测试完成 ===")
Comment on lines +1 to +222
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments in Chinese found in test file. The test includes Chinese comments and console output which is inconsistent with the English used throughout the codebase. For maintainability and international collaboration, all code comments and output should be in English.

Examples include:

  • Line 2: "本地验证 diff 解析逻辑" (Local verification of diff parsing logic)
  • Line 114: "测试用例" (Test cases)
  • Line 129: "测试 Diff 解析" (Test Diff parsing)
  • Line 145, 155: "删除行", "添加行" (Deleted line, Added line)
  • Multiple console.log messages with Chinese text

Copilot uses AI. Check for mistakes.
11 changes: 7 additions & 4 deletions .opencode-review/tools/gitea-pr-diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,15 +128,18 @@ function formatDiffForReview(files: ParsedFile[]): string {
parts.push(`\n## ${file.path} (${file.status})\n`)

for (const hunk of file.hunks) {
parts.push(`@@ starting at line ${hunk.newStart} @@`)
parts.push(`@@ starting at line ${hunk.newStart} (old: ${hunk.oldStart}) @@`)

for (const change of hunk.changes) {
if (change.type === "add") {
parts.push(`[${change.newLine}] +${change.content}`)
// New line: [NEW:行号] +内容
parts.push(`[NEW:${change.newLine}] +${change.content}`)
} else if (change.type === "del") {
parts.push(`[DEL] -${change.content}`)
// Deleted line: [OLD:行号] -内容 (用于 old_position 评论)
parts.push(`[OLD:${change.oldLine}] -${change.content}`)
} else {
parts.push(`[${change.newLine}] ${change.content}`)
// Context line: [NEW:行号] 内容
parts.push(`[NEW:${change.newLine}] ${change.content}`)
Comment on lines +135 to +142
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments in Chinese found in production code. While the comments accurately describe the logic, mixing languages in comments is inconsistent with the English comments used throughout the rest of the codebase. For maintainability and consistency, all comments should be in English.

Consider translating these comments:

  • Line 135: "New line: [NEW:行号] +内容" → "New line: [NEW:line_number] +content"
  • Line 138: "Deleted line: [OLD:行号] -内容 (用于 old_position 评论)" → "Deleted line: [OLD:line_number] -content (for old_position comments)"
  • Line 141: "Context line: [NEW:行号] 内容" → "Context line: [NEW:line_number] content"

Copilot uses AI. Check for mistakes.
Comment on lines 134 to +142
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential issue with undefined line numbers in formatted output. For deleted lines, if 'change.oldLine' is undefined, the output would be '[OLD:undefined] -content', which is incorrect. Similarly, for added/context lines, if 'change.newLine' is undefined, the output would be '[NEW:undefined] content'.

While the parsing logic (lines 90-101) appears to always set these values, the TypeScript interface (line 42-43) defines them as optional. If the parsing logic has any bugs or edge cases where these values aren't set, this would produce malformed output.

Consider adding defensive checks or assertions to ensure these values are always defined before formatting, or update the interface to reflect that these fields are always present in their respective contexts.

Copilot uses AI. Check for mistakes.
}
}
parts.push("")
Expand Down
13 changes: 7 additions & 6 deletions .opencode-review/tools/gitea-pr-diff.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@ Features:
- 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
- "parsed" (default): Shows each line with line number prefix for easy reference when creating review comments
- "raw": Returns the raw unified diff format

The parsed format shows:
- [42] +new line - Added line at line 42
- [DEL] -old line - Deleted line
- [42] context - Unchanged context line
- [NEW:42] +new line - Added line at line 42 in new file (use `line: 42` in comments)
- [OLD:38] -old line - Deleted line at line 38 in old file (use `old_line: 38` in comments)
- [NEW:42] context - Unchanged context line (use `line: 42` in comments)

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.
When creating review comments with gitea-review tool:
- For [NEW:X] lines → use `line: X`
- For [OLD:X] lines → use `old_line: X`
34 changes: 27 additions & 7 deletions .opencode-review/tools/gitea-review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,13 @@ export default tool({
.array(
tool.schema.object({
path: tool.schema.string().describe("File path"),
line: tool.schema.number().describe("Line number in the new file"),
line: tool.schema.number().optional().describe("Line number in the NEW file (for added/context lines). Use this OR old_line, not both."),
old_line: tool.schema.number().optional().describe("Line number in the OLD file (for deleted lines). Use this OR line, not both."),
body: tool.schema.string().describe("Comment content"),
}),
)
.optional()
.describe("Line-level review comments"),
.describe("Line-level review comments. Use 'line' for new/context lines, 'old_line' for deleted lines."),
approval: tool.schema
.enum(["comment", "approve", "request_changes"])
.optional()
Expand Down Expand Up @@ -113,11 +114,30 @@ export default tool({
.replace(/\\t/g, "\t")

// Format review comments for API - also process escaped characters in comments
const reviewComments = comments.map((c) => ({
path: c.path,
body: c.body.replace(/\\n/g, "\n").replace(/\\t/g, "\t"),
new_position: c.line,
}))
// Per Gitea API: use new_position for new file lines, old_position for deleted lines
const reviewComments = comments.map((c) => {
const comment: {
path: string
body: string
new_position?: number
old_position?: number
} = {
path: c.path,
body: c.body.replace(/\\n/g, "\n").replace(/\\t/g, "\t"),
}

if (c.old_line !== undefined && c.old_line > 0) {
// Comment on deleted line (old file)
comment.old_position = c.old_line
comment.new_position = 0
} else if (c.line !== undefined && c.line > 0) {
// Comment on new/context line (new file)
comment.new_position = c.line
comment.old_position = 0
Comment on lines +130 to +136
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting positions to 0 may not be correct according to Gitea/Forgejo API semantics. When commenting on a new line, the code sets old_position = 0, and when commenting on a deleted line, it sets new_position = 0.

According to typical diff commenting APIs (including GitHub's which Gitea is based on), position fields that aren't applicable should usually be omitted (undefined) rather than explicitly set to 0. Setting them to 0 might be interpreted as "line 0" which doesn't exist, potentially causing API errors or comments to appear in unexpected locations.

Consider either:

  1. Omitting the inapplicable position field entirely (don't set it at all)
  2. Verifying with Gitea API documentation that setting to 0 is the correct approach

If setting to 0 is confirmed correct, add a comment explaining this is intentional per Gitea API requirements.

Suggested change
// Comment on deleted line (old file)
comment.old_position = c.old_line
comment.new_position = 0
} else if (c.line !== undefined && c.line > 0) {
// Comment on new/context line (new file)
comment.new_position = c.line
comment.old_position = 0
// Comment on deleted line (old file) - only set old_position
comment.old_position = c.old_line
} else if (c.line !== undefined && c.line > 0) {
// Comment on new/context line (new file) - only set new_position
comment.new_position = c.line

Copilot uses AI. Check for mistakes.
}
Comment on lines +129 to +137
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing validation for comments that have both 'line' and 'old_line' defined, or neither defined. According to the documentation, users should use one OR the other, but there's no enforcement. If both are provided, the code will use old_line (due to the if-else ordering), which may not be the user's intent. If neither is provided, a comment will be created with no position fields, which could cause API errors or unexpected behavior.

Consider adding validation to ensure exactly one of 'line' or 'old_line' is provided, and throw a descriptive error if the constraint is violated.

Copilot uses AI. Check for mistakes.

return comment
})

// Create the review
const reviewPayload: ReviewRequest = {
Expand Down
Loading
Loading