215 lines
6.0 KiB
Rust
215 lines
6.0 KiB
Rust
use futures_util::stream::TryStreamExt;
|
|
use tokio::io::AsyncReadExt;
|
|
use tokio_util::io::StreamReader;
|
|
use tracing::instrument;
|
|
|
|
use crate::{
|
|
bot::ReviewResult,
|
|
consts::{BOT_PROCESS_MSG, MAX_DIFF_SIZE, REVIEW_PROMPT},
|
|
gitea::{GiteaAPI, ReviewPayload},
|
|
open_router::OpenRouterClient,
|
|
};
|
|
|
|
#[instrument(skip(gitea_api, open_router_client, http_client, review_payload))]
|
|
pub async fn exec_review(
|
|
gitea_api: &GiteaAPI,
|
|
open_router_client: &OpenRouterClient,
|
|
http_client: &reqwest::Client,
|
|
model: &str,
|
|
review_payload: ReviewPayload,
|
|
) -> anyhow::Result<()> {
|
|
tracing::info!(
|
|
repo = %review_payload.repository.full_name,
|
|
pr = review_payload.pull_request.number,
|
|
action = %review_payload.action,
|
|
"Starting review"
|
|
);
|
|
|
|
let new_comment = gitea_api
|
|
.comment(
|
|
&BOT_PROCESS_MSG.replace("{model}", model),
|
|
&review_payload.repository.full_name,
|
|
review_payload.pull_request.number,
|
|
)
|
|
.await?;
|
|
|
|
let bot_result: Result<ReviewResult, anyhow::Error> = async {
|
|
let git_diff =
|
|
download_git_diff(http_client, &review_payload.pull_request.diff_url).await?;
|
|
|
|
let diff_for_llm = format_diff_for_review(&git_diff);
|
|
|
|
let bot_request = REVIEW_PROMPT
|
|
.replace("{subject}", &review_payload.pull_request.title)
|
|
.replace("{comment}", &review_payload.comment.body)
|
|
.replace("{diff}", &diff_for_llm);
|
|
|
|
let chat_result = 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;
|
|
|
|
let final_review_markdown = review_result_to_markdown(&review_result);
|
|
|
|
gitea_api
|
|
.post_pull_request_review(
|
|
&review_result,
|
|
&final_review_markdown,
|
|
&review_payload.repository.full_name,
|
|
review_payload.pull_request.number,
|
|
)
|
|
.await?;
|
|
|
|
Ok(review_result)
|
|
}
|
|
.await;
|
|
|
|
match bot_result {
|
|
Ok(_) => {
|
|
gitea_api
|
|
.delete_comment(&review_payload.repository.full_name, new_comment.id)
|
|
.await
|
|
}
|
|
Err(e) => {
|
|
gitea_api
|
|
.edit_comment(
|
|
&format!("Error while reviewing: {}", e),
|
|
&review_payload.repository.full_name,
|
|
new_comment.id,
|
|
)
|
|
.await
|
|
}
|
|
}
|
|
}
|
|
|
|
fn review_result_to_markdown(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");
|
|
|
|
md.push_str(&format!(
|
|
"### {} issues found.\n\n",
|
|
review_result.reviews.len()
|
|
));
|
|
|
|
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(http_client: &reqwest::Client, url: &str) -> anyhow::Result<String> {
|
|
let response = 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())
|
|
}
|
|
|
|
fn format_diff_for_review(git_diff: &str) -> String {
|
|
let mut output = String::new();
|
|
let mut current_file: Option<&str> = None;
|
|
let mut new_line: u64 = 0;
|
|
|
|
for line in git_diff.lines() {
|
|
if let Some(rest) = line.strip_prefix("diff --git a/") {
|
|
if let Some(end) = rest.find(' ') {
|
|
current_file = Some(&rest[..end]);
|
|
}
|
|
new_line = 0;
|
|
continue;
|
|
}
|
|
|
|
if line.starts_with("---") || line.starts_with("+++") {
|
|
continue;
|
|
}
|
|
|
|
if line.starts_with("@@") && line.contains('+') {
|
|
if let Some(start) = parse_hunk_new_start(line) {
|
|
new_line = start;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
let Some(filename) = current_file else {
|
|
continue;
|
|
};
|
|
|
|
if line.starts_with(' ') {
|
|
new_line += 1;
|
|
continue;
|
|
}
|
|
|
|
if let Some(code) = line.strip_prefix('+') {
|
|
use std::fmt::Write;
|
|
let _ = writeln!(output, "{filename}:{new_line}:{code}");
|
|
new_line += 1;
|
|
}
|
|
}
|
|
|
|
output
|
|
}
|
|
|
|
fn parse_hunk_new_start(hunk_header: &str) -> Option<u64> {
|
|
let plus_part = hunk_header.split('+').nth(1)?;
|
|
let num_str = plus_part.split(|c: char| !c.is_ascii_digit()).next()?;
|
|
num_str.parse::<u64>().ok()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[test]
|
|
fn test_format_diff_for_review() {
|
|
let diff = concat!(
|
|
"diff --git a/src/foo.rs b/src/foo.rs\n",
|
|
"--- a/src/foo.rs\n",
|
|
"+++ b/src/foo.rs\n",
|
|
"@@ -1,3 +1,6 @@\n",
|
|
" fn main() {\n",
|
|
"+ let x = 1;\n",
|
|
" println!(\"hello\");\n",
|
|
"+ let y = 2;\n",
|
|
"+ let z = 3;\n",
|
|
" }\n",
|
|
"diff --git a/src/bar.rs b/src/bar.rs\n",
|
|
"--- a/src/bar.rs\n",
|
|
"+++ b/src/bar.rs\n",
|
|
"@@ -10,4 +10,6 @@\n",
|
|
" old context\n",
|
|
"+ let a = 10;\n",
|
|
" more context\n",
|
|
"+ let b = 20;\n",
|
|
);
|
|
|
|
let result = format_diff_for_review(diff);
|
|
let expected = concat!(
|
|
"src/foo.rs:2: let x = 1;\n",
|
|
"src/foo.rs:4: let y = 2;\n",
|
|
"src/foo.rs:5: let z = 3;\n",
|
|
"src/bar.rs:11: let a = 10;\n",
|
|
"src/bar.rs:13: let b = 20;\n",
|
|
);
|
|
|
|
assert_eq!(result, expected);
|
|
}
|