Extract review logic into bot_actions module

Move `exec_review`, `download_git_diff`, and review formatting
to a new `bot_actions::review` module. Update the review flow to
post inline review comments via the Gitea API and simplify the
comment markdown to a summary. Add diff formatting that
preprocesses added lines with line numbers for the LLM prompt.
This commit is contained in:
2026-06-06 17:27:35 +00:00
parent ced1fca563
commit aa0dbdcc7a
6 changed files with 265 additions and 117 deletions
+8 -107
View File
@@ -1,14 +1,9 @@
use futures_util::stream::TryStreamExt;
use serde::Deserialize;
use std::time::Duration;
use tokio::io::AsyncReadExt;
use tokio_util::io::StreamReader;
use crate::{
consts::{BOT_PROCESS_MSG, MAX_DIFF_SIZE, REVIEW_PROMPT},
env::EnvConfig,
errors::AppError,
gitea::{GiteaAPI, ReviewPayload, WebhookType},
gitea::{GiteaAPI, WebhookType},
open_router::OpenRouterClient,
};
@@ -104,7 +99,13 @@ impl Bot {
pub async fn exec(&self, webhook: WebhookType) {
let exec_result = match webhook {
WebhookType::Review(review_payload) => self.exec_review(review_payload),
WebhookType::Review(review_payload) => crate::bot_actions::review::exec_review(
&self.gitea_api,
&self.open_router_client,
&self.http_client,
&self.config.open_router_model,
review_payload,
),
}
.await;
@@ -113,104 +114,4 @@ impl Bot {
Err(err) => println!("{}", err),
}
}
pub async fn exec_review(&self, review_payload: ReviewPayload) -> Result<(), AppError> {
let new_comment = self
.gitea_api
.comment(
&BOT_PROCESS_MSG.replace("{model}", &self.config.open_router_model),
&review_payload.repository.full_name,
review_payload.pull_request.number,
)
.await?;
let bot_result: Result<ReviewResult, anyhow::Error> = async {
let git_diff = self
.download_git_diff(&review_payload.pull_request.diff_url)
.await?;
let bot_request = REVIEW_PROMPT
.replace("{subject}", &review_payload.pull_request.title)
.replace("{comment}", &review_payload.comment.body)
.replace("{diff}", &git_diff);
let chat_result = self.open_router_client.chat(&bot_request).await?;
let mut review_result = serde_json::from_str::<ReviewResult>(&chat_result.message)?;
review_result.cost = chat_result.cost;
Ok(review_result)
}
.await;
let edit_msg = match bot_result {
Ok(bot_result) => self.review_result_to_markdown(&bot_result),
Err(e) => format!("Error while reviewing: {}", e),
};
self.gitea_api
.edit_comment(
&edit_msg,
&review_payload.repository.full_name,
new_comment.id,
)
.await?;
Ok(())
}
fn review_result_to_markdown(&self, review_result: &ReviewResult) -> String {
if review_result.reviews.is_empty() {
return String::from("No issues found. ✅");
}
let mut md = String::from("## Review Feedback\n\n");
for (i, item) in review_result.reviews.iter().enumerate() {
if i > 0 {
md.push_str("\n---\n\n");
}
md.push_str(&format!("### `{}`\n\n", item.filename));
if let Some(line) = item.line {
md.push_str(&format!("> **Line {}**\n\n", line));
}
if !item.code.is_empty() {
let lang = lang_from_filename(&item.filename);
md.push_str(&format!("```{}\n{}\n```\n\n", lang, item.code));
}
md.push_str(&item.message);
md.push('\n');
}
if !review_result.comment.is_empty() {
md.push_str("\n---\n\n");
md.push_str("### Summary\n\n");
md.push_str(&review_result.comment);
md.push('\n');
}
if let Some(cost) = review_result.cost {
md.push_str("\n---\n\n");
md.push_str(&format!("### Cost: ${}", cost));
md.push('\n');
}
md
}
async fn download_git_diff(&self, url: &str) -> anyhow::Result<String> {
let response = self.http_client.get(url).send().await?;
let stream = response.bytes_stream().map_err(std::io::Error::other);
let mut buf = Vec::with_capacity(MAX_DIFF_SIZE);
StreamReader::new(stream)
.take((MAX_DIFF_SIZE + 1) as u64)
.read_to_end(&mut buf)
.await?;
if buf.len() > MAX_DIFF_SIZE {
anyhow::bail!("Git diff exceeds the maximum allowed size of 1 Mo");
}
Ok(String::from_utf8_lossy(&buf).into_owned())
}
}