fix: render per-comment file line instead of thread-level location

This commit is contained in:
2026-04-08 23:23:19 +08:00
parent a61be1b4c4
commit d00e6bcb2c
7 changed files with 35 additions and 8 deletions
+4 -2
View File
@@ -71,14 +71,15 @@ total: <files_changed> files, +<additions>, -<deletions>
## Review 1 (<review-state>) ## Review 1 (<review-state>)
> <reviewer> > <reviewer>
### Comment 1.1 ### Comment <pr-index>.<comment-seq>
<file>:<line> <file>:<line>
<user>: <user>:
```md ```md
<original comment body> <original comment body>
``` ```
### Reply 1.1.1 ### Reply <pr-index>.<comment-seq>.<reply-seq>
<file>:<line>
<user>: <user>:
```md ```md
<original reply body> <original reply body>
@@ -89,3 +90,4 @@ total: <files_changed> files, +<additions>, -<deletions>
- Output goes to `stdout` by default; use `--out` to write a file. - Output goes to `stdout` by default; use `--out` to write a file.
- The renderer preserves markdown-heavy comment bodies safely by adjusting code fences when needed. - The renderer preserves markdown-heavy comment bodies safely by adjusting code fences when needed.
- Both `Comment` and `Reply` include their directly associated file path and line number (when provided by the API).
+2
View File
@@ -79,6 +79,7 @@ total: <files_changed> files, +<additions>, -<deletions>
``` ```
### Reply <pr-index>.<comment-seq>.<reply-seq> ### Reply <pr-index>.<comment-seq>.<reply-seq>
<file>:<line>
<user>: <user>:
```md ```md
<原始回复正文> <原始回复正文>
@@ -89,3 +90,4 @@ total: <files_changed> files, +<additions>, -<deletions>
- 默认输出到 `stdout`;仅在指定 `--out` 时写入文件。 - 默认输出到 `stdout`;仅在指定 `--out` 时写入文件。
- 当评论正文本身包含 Markdown / 代码块时,渲染器会自动处理 fence,避免破坏整体结构。 - 当评论正文本身包含 Markdown / 代码块时,渲染器会自动处理 fence,避免破坏整体结构。
- `Comment` 和 `Reply` 都会显示各自直接关联的文件路径与行号(若 API 提供)。
+2
View File
@@ -69,5 +69,7 @@ pub struct CommentItem {
pub id: i64, pub id: i64,
pub user: String, pub user: String,
pub created_at: String, pub created_at: String,
pub path: Option<String>,
pub line: Option<i64>,
pub body: String, pub body: String,
} }
+2
View File
@@ -201,6 +201,8 @@ fn to_comment_item(comment: ReviewCommentDto) -> CommentItem {
id: comment.id, id: comment.id,
user: comment.user.login, user: comment.user.login,
created_at: comment.created_at, created_at: comment.created_at,
path: comment.path,
line: comment.line,
body: comment.body, body: comment.body,
} }
} }
+15 -6
View File
@@ -22,16 +22,22 @@ fn render_body(body: &str) -> String {
format!("{fence}md\n{body}\n{fence}\n") format!("{fence}md\n{body}\n{fence}\n")
} }
fn render_comment_location(path: Option<&str>, line: Option<i64>) -> Option<String> {
path.map(|file_path| match line {
Some(value) => format!("{file_path}:{value}"),
None => file_path.to_string(),
})
}
fn render_thread(pr_index: i64, comment_seq: usize, thread: &CommentThread) -> String { fn render_thread(pr_index: i64, comment_seq: usize, thread: &CommentThread) -> String {
let mut out = String::new(); let mut out = String::new();
let comment_number = format!("{pr_index}.{comment_seq}"); let comment_number = format!("{pr_index}.{comment_seq}");
out.push_str(&format!("### Comment {comment_number}\n")); out.push_str(&format!("### Comment {comment_number}\n"));
if let Some(file_path) = &thread.file_path { let root_location =
if let Some(line) = thread.line { render_comment_location(thread.root_comment.path.as_deref(), thread.root_comment.line)
out.push_str(&format!("{file_path}:{line}\n")); .or_else(|| render_comment_location(thread.file_path.as_deref(), thread.line));
} else { if let Some(location) = root_location {
out.push_str(&format!("{file_path}\n")); out.push_str(&format!("{location}\n"));
}
} }
out.push_str(&format!("{}:\n", thread.root_comment.user)); out.push_str(&format!("{}:\n", thread.root_comment.user));
out.push_str(&render_body(&thread.root_comment.body)); out.push_str(&render_body(&thread.root_comment.body));
@@ -39,6 +45,9 @@ fn render_thread(pr_index: i64, comment_seq: usize, thread: &CommentThread) -> S
for (reply_index, reply) in thread.replies.iter().enumerate() { for (reply_index, reply) in thread.replies.iter().enumerate() {
let reply_number = format!("{comment_number}.{}", reply_index + 1); let reply_number = format!("{comment_number}.{}", reply_index + 1);
out.push_str(&format!("\n### Reply {reply_number}\n")); out.push_str(&format!("\n### Reply {reply_number}\n"));
if let Some(location) = render_comment_location(reply.path.as_deref(), reply.line) {
out.push_str(&format!("{location}\n"));
}
out.push_str(&format!("{}:\n", reply.user)); out.push_str(&format!("{}:\n", reply.user));
out.push_str(&render_body(&reply.body)); out.push_str(&render_body(&reply.body));
} }
+2
View File
@@ -37,6 +37,8 @@ fn model_json_roundtrip() {
id: 100, id: 100,
user: "reviewer".into(), user: "reviewer".into(),
created_at: "2026-04-08T12:00:00Z".into(), created_at: "2026-04-08T12:00:00Z".into(),
path: Some("src/main.rs".into()),
line: Some(10),
body: "hello".into(), body: "hello".into(),
}, },
replies: vec![], replies: vec![],
+8
View File
@@ -50,12 +50,16 @@ fn render_markdown_includes_expected_sections_and_preserves_comment_markdown() {
id: 11, id: 11,
user: "bob".into(), user: "bob".into(),
created_at: "2026-04-08T11:01:00Z".into(), created_at: "2026-04-08T11:01:00Z".into(),
path: Some("src/main.rs".into()),
line: Some(9),
body: "```rs\nlet x = 1;\n```".into(), body: "```rs\nlet x = 1;\n```".into(),
}, },
replies: vec![CommentItem { replies: vec![CommentItem {
id: 12, id: 12,
user: "alice".into(), user: "alice".into(),
created_at: "2026-04-08T11:02:00Z".into(), created_at: "2026-04-08T11:02:00Z".into(),
path: Some("src/main.rs".into()),
line: Some(12),
body: "looks good".into(), body: "looks good".into(),
}], }],
}], }],
@@ -69,7 +73,9 @@ fn render_markdown_includes_expected_sections_and_preserves_comment_markdown() {
assert!(md.contains("## Diff Stat")); assert!(md.contains("## Diff Stat"));
assert!(md.contains("## Review 1 (COMMENT)")); assert!(md.contains("## Review 1 (COMMENT)"));
assert!(md.contains("### Comment 7.1")); assert!(md.contains("### Comment 7.1"));
assert!(md.contains("src/main.rs:9"));
assert!(md.contains("### Reply 7.1.1")); assert!(md.contains("### Reply 7.1.1"));
assert!(md.contains("src/main.rs:12"));
assert!(md.contains("````md")); assert!(md.contains("````md"));
assert!(md.contains("```rs\nlet x = 1;\n```")); assert!(md.contains("```rs\nlet x = 1;\n```"));
assert!(md.contains("looks good")); assert!(md.contains("looks good"));
@@ -107,6 +113,8 @@ fn render_markdown_uses_minimal_fence_for_plain_text() {
id: 1, id: 1,
user: "bob".into(), user: "bob".into(),
created_at: "2026-04-08T11:00:00Z".into(), created_at: "2026-04-08T11:00:00Z".into(),
path: None,
line: None,
body: body.into(), body: body.into(),
}, },
replies: vec![], replies: vec![],