use anyhow::Result;
use aptu_core::ai::types::PrReviewComment;
use aptu_core::{
PrDetails, PrReviewResponse, render_pr_review_comment_body, render_pr_review_markdown,
};
use tracing::{debug, info, instrument};
use super::types::PrLabelResult;
use crate::provider::CliTokenProvider;
pub struct AnalyzeResult {
pub pr_details: PrDetails,
pub review: PrReviewResponse,
}
#[instrument(skip_all, fields(reference = %reference))]
pub async fn fetch(reference: &str, repo_context: Option<&str>) -> Result<PrDetails> {
let provider = CliTokenProvider;
let pr_details = aptu_core::fetch_pr_for_review(&provider, reference, repo_context).await?;
debug!(pr_number = pr_details.number, "PR fetched successfully");
Ok(pr_details)
}
#[instrument(skip_all, fields(pr_number = pr_details.number))]
pub async fn analyze(
pr_details: &PrDetails,
ai_config: &aptu_core::AiConfig,
repo_path: Option<String>,
deep: bool,
) -> Result<(PrReviewResponse, aptu_core::history::AiStats)> {
let provider = CliTokenProvider;
let (review, ai_stats) =
aptu_core::analyze_pr(&provider, pr_details, ai_config, repo_path, deep).await?;
debug!("PR analyzed successfully");
Ok((review, ai_stats))
}
pub(crate) fn format_comment_header(comment: &PrReviewComment) -> String {
let line_part = comment.line.map_or_else(String::new, |l| format!(":{l}"));
let severity = comment.severity.as_str().to_uppercase();
format!("{}{} [{}]", comment.file, line_part, severity)
}
#[instrument(skip_all, fields(pr_number = analyze_result.pr_details.number))]
pub async fn post(
analyze_result: &AnalyzeResult,
reference: &str,
repo_context: Option<&str>,
event: aptu_core::ReviewEvent,
dry_run: bool,
skip_confirm: bool,
verbose: bool,
) -> Result<()> {
let provider = CliTokenProvider;
let review_body = render_pr_review_markdown(
&analyze_result.review,
analyze_result.pr_details.files.len(),
);
if dry_run {
debug!("Dry-run mode: skipping post");
eprintln!(
"Dry-run: Would post {} review to PR #{}",
event, analyze_result.pr_details.number
);
eprintln!("Review body:\n{review_body}");
if verbose && !analyze_result.review.comments.is_empty() {
eprintln!(
"\nInline comments ({}):",
analyze_result.review.comments.len()
);
for (i, comment) in analyze_result.review.comments.iter().enumerate() {
eprintln!(" [{}] {}", i + 1, format_comment_header(comment));
let body = render_pr_review_comment_body(comment);
let indented = body
.lines()
.map(|l| format!(" {l}"))
.collect::<Vec<_>>()
.join("\n");
eprintln!("{indented}\n");
}
}
} else {
if !skip_confirm {
eprintln!(
"About to post {} review to PR #{}",
event, analyze_result.pr_details.number
);
eprintln!("Continue? (y/n) ");
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
if !input.trim().eq_ignore_ascii_case("y") {
debug!("User cancelled review posting");
return Ok(());
}
}
let review_id = aptu_core::post_pr_review(
&provider,
reference,
repo_context,
&review_body,
event,
&analyze_result.review.comments,
&analyze_result.pr_details.head_sha,
)
.await?;
info!(review_id = review_id, "Review posted successfully");
eprintln!("Review posted successfully (ID: {review_id})");
}
Ok(())
}
#[instrument(skip_all)]
pub async fn run_pr_create(
repo: Option<String>,
inferred_repo: Option<String>,
default_repo: Option<String>,
title: String,
body: Option<String>,
branch: Option<String>,
base: String,
) -> anyhow::Result<aptu_core::PrCreateResult> {
use aptu_core::github::parse_owner_repo;
let resolved_repo = repo
.as_deref()
.or(inferred_repo.as_deref())
.or(default_repo.as_deref())
.ok_or_else(|| {
anyhow::anyhow!(
"No repository specified. Use --repo or run inside a git repo with a GitHub remote."
)
})?;
let (owner, repo_name) = parse_owner_repo(resolved_repo)?;
let head = if let Some(b) = branch {
b
} else {
let output = std::process::Command::new("git")
.args(["rev-parse", "--abbrev-ref", "HEAD"])
.output()
.map_err(|e| anyhow::anyhow!("Failed to run git: {e}"))?;
if !output.status.success() {
anyhow::bail!("Failed to determine current git branch. Use --branch to specify.");
}
String::from_utf8_lossy(&output.stdout).trim().to_string()
};
let provider = CliTokenProvider;
aptu_core::create_pr(
&provider,
&owner,
&repo_name,
&title,
&base,
&head,
body.as_deref(),
)
.await
.map_err(Into::into)
}
#[instrument(skip_all, fields(reference = %reference))]
pub async fn run_label(
reference: &str,
repo_context: Option<&str>,
dry_run: bool,
ai_config: &aptu_core::AiConfig,
) -> Result<PrLabelResult> {
let provider = crate::provider::CliTokenProvider;
let (pr_number, pr_title, pr_url, labels) =
aptu_core::label_pr(&provider, reference, repo_context, dry_run, ai_config).await?;
Ok(PrLabelResult {
pr_number,
pr_title,
pr_url,
labels,
dry_run,
})
}
#[cfg(test)]
mod tests {
use super::*;
use aptu_core::ai::types::{CommentSeverity, PrReviewComment};
#[test]
fn test_format_comment_header_with_line() {
let comment = PrReviewComment {
file: "src/main.rs".to_string(),
line: Some(42),
comment: "Test comment".to_string(),
severity: CommentSeverity::Warning,
suggested_code: None,
};
let header = format_comment_header(&comment);
assert_eq!(header, "src/main.rs:42 [WARNING]");
}
#[test]
fn test_format_comment_header_no_line() {
let comment = PrReviewComment {
file: "src/lib.rs".to_string(),
line: None,
comment: "Test comment".to_string(),
severity: CommentSeverity::Info,
suggested_code: None,
};
let header = format_comment_header(&comment);
assert_eq!(header, "src/lib.rs [INFO]");
}
}