244 lines
7.8 KiB
Rust
244 lines
7.8 KiB
Rust
use ai::error::{AiError, AiResult};
|
|
use ai::tool::tools::FunctionCall;
|
|
use async_trait::async_trait;
|
|
use git::rpc::proto as p;
|
|
use git::rpc::proto::diff_service_client::DiffServiceClient;
|
|
use serde_json::{Value, json};
|
|
|
|
use super::helpers::{arg_str, git_ctx, require_repo_member, rpc_err};
|
|
use crate::agent::run::AppAgentContext;
|
|
|
|
pub struct GitDiffStatsTool;
|
|
|
|
impl GitDiffStatsTool {
|
|
pub fn new() -> Self {
|
|
Self
|
|
}
|
|
}
|
|
|
|
impl Default for GitDiffStatsTool {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl FunctionCall for GitDiffStatsTool {
|
|
type Context = AppAgentContext;
|
|
|
|
fn name(&self) -> &'static str {
|
|
"git_diff_stats"
|
|
}
|
|
|
|
fn description(&self) -> &'static str {
|
|
"Get diff statistics between two commits: files changed, insertions, deletions."
|
|
}
|
|
|
|
fn schema(&self) -> Value {
|
|
json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"workspace": { "type": "string", "description": "Workspace name" },
|
|
"repo": { "type": "string", "description": "Repository name" },
|
|
"old_oid": { "type": "string", "description": "Base commit OID" },
|
|
"new_oid": { "type": "string", "description": "Target commit OID" }
|
|
},
|
|
"required": ["workspace", "repo", "old_oid", "new_oid"]
|
|
})
|
|
}
|
|
|
|
async fn call(
|
|
&self,
|
|
ctx: &mut AppAgentContext,
|
|
args: Value,
|
|
) -> AiResult<Value> {
|
|
let git = git_ctx(ctx)?;
|
|
let workspace = arg_str(&args, "workspace")?;
|
|
let repo_name = arg_str(&args, "repo")?;
|
|
let old_oid = arg_str(&args, "old_oid")?;
|
|
let new_oid = arg_str(&args, "new_oid")?;
|
|
|
|
let repo =
|
|
require_repo_member(git, ctx.user_id, workspace, repo_name).await?;
|
|
|
|
let mut client = DiffServiceClient::new(git.channel.clone());
|
|
let resp = client
|
|
.diff_stats(p::DiffStatsRequest {
|
|
repo_id: repo.id.to_string(),
|
|
old_oid: Some(p::ObjectId {
|
|
value: old_oid.to_string(),
|
|
}),
|
|
new_oid: Some(p::ObjectId {
|
|
value: new_oid.to_string(),
|
|
}),
|
|
options: None,
|
|
})
|
|
.await
|
|
.map_err(rpc_err)?
|
|
.into_inner();
|
|
|
|
let result = resp
|
|
.result
|
|
.ok_or_else(|| AiError::Response("no diff result".to_string()))?;
|
|
let stats = result
|
|
.stats
|
|
.ok_or_else(|| AiError::Response("no stats".to_string()))?;
|
|
|
|
let files: Vec<Value> = result.deltas.iter().map(|d| {
|
|
let status = match p::DiffDeltaStatus::try_from(d.status) {
|
|
Ok(p::DiffDeltaStatus::Added) => "added",
|
|
Ok(p::DiffDeltaStatus::Deleted) => "deleted",
|
|
Ok(p::DiffDeltaStatus::Modified) => "modified",
|
|
Ok(p::DiffDeltaStatus::Renamed) => "renamed",
|
|
_ => "unknown",
|
|
};
|
|
let old_path = d.old_file.as_ref().and_then(|f| f.path.as_deref()).unwrap_or("");
|
|
let new_path = d.new_file.as_ref().and_then(|f| f.path.as_deref()).unwrap_or("");
|
|
json!({ "status": status, "old_path": old_path, "new_path": new_path, "hunks": d.hunks.len() })
|
|
}).collect();
|
|
|
|
Ok(json!({
|
|
"files_changed": stats.files_changed,
|
|
"insertions": stats.insertions,
|
|
"deletions": stats.deletions,
|
|
"files": files,
|
|
}))
|
|
}
|
|
}
|
|
|
|
pub struct GitDiffPatchTool;
|
|
|
|
impl GitDiffPatchTool {
|
|
pub fn new() -> Self {
|
|
Self
|
|
}
|
|
}
|
|
|
|
impl Default for GitDiffPatchTool {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl FunctionCall for GitDiffPatchTool {
|
|
type Context = AppAgentContext;
|
|
|
|
fn name(&self) -> &'static str {
|
|
"git_diff_patch"
|
|
}
|
|
|
|
fn description(&self) -> &'static str {
|
|
"Get the full diff (unified format) between two commits, including line-level changes."
|
|
}
|
|
|
|
fn schema(&self) -> Value {
|
|
json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"workspace": { "type": "string", "description": "Workspace name" },
|
|
"repo": { "type": "string", "description": "Repository name" },
|
|
"old_oid": { "type": "string", "description": "Base commit OID" },
|
|
"new_oid": { "type": "string", "description": "Target commit OID" },
|
|
"context_lines": { "type": "integer", "description": "Lines of context around changes (default 3)" }
|
|
},
|
|
"required": ["workspace", "repo", "old_oid", "new_oid"]
|
|
})
|
|
}
|
|
|
|
async fn call(
|
|
&self,
|
|
ctx: &mut AppAgentContext,
|
|
args: Value,
|
|
) -> AiResult<Value> {
|
|
let git = git_ctx(ctx)?;
|
|
let workspace = arg_str(&args, "workspace")?;
|
|
let repo_name = arg_str(&args, "repo")?;
|
|
let old_oid = arg_str(&args, "old_oid")?;
|
|
let new_oid = arg_str(&args, "new_oid")?;
|
|
let ctx_lines = args
|
|
.get("context_lines")
|
|
.and_then(|v| v.as_u64())
|
|
.unwrap_or(3) as u32;
|
|
|
|
let repo =
|
|
require_repo_member(git, ctx.user_id, workspace, repo_name).await?;
|
|
|
|
let mut client = DiffServiceClient::new(git.channel.clone());
|
|
let resp = client
|
|
.diff_patch(p::DiffPatchRequest {
|
|
repo_id: repo.id.to_string(),
|
|
old_oid: Some(p::ObjectId {
|
|
value: old_oid.to_string(),
|
|
}),
|
|
new_oid: Some(p::ObjectId {
|
|
value: new_oid.to_string(),
|
|
}),
|
|
options: Some(p::DiffOptions {
|
|
context_lines: ctx_lines,
|
|
..Default::default()
|
|
}),
|
|
})
|
|
.await
|
|
.map_err(rpc_err)?
|
|
.into_inner();
|
|
|
|
let result = resp
|
|
.result
|
|
.ok_or_else(|| AiError::Response("no diff result".to_string()))?;
|
|
let stats = result
|
|
.stats
|
|
.ok_or_else(|| AiError::Response("no stats".to_string()))?;
|
|
|
|
let mut patch_text = String::new();
|
|
for delta in &result.deltas {
|
|
let status = match p::DiffDeltaStatus::try_from(delta.status) {
|
|
Ok(p::DiffDeltaStatus::Added) => "added",
|
|
Ok(p::DiffDeltaStatus::Deleted) => "deleted",
|
|
Ok(p::DiffDeltaStatus::Modified) => "modified",
|
|
Ok(p::DiffDeltaStatus::Renamed) => "renamed",
|
|
_ => "unknown",
|
|
};
|
|
let old = delta
|
|
.old_file
|
|
.as_ref()
|
|
.and_then(|f| f.path.as_deref())
|
|
.unwrap_or("unknown");
|
|
let new = delta
|
|
.new_file
|
|
.as_ref()
|
|
.and_then(|f| f.path.as_deref())
|
|
.unwrap_or("unknown");
|
|
patch_text.push_str(&format!(
|
|
"--- {}\n+++ {}\n@@ status: {status} @@\n",
|
|
old, new
|
|
));
|
|
|
|
for hunk in &delta.hunks {
|
|
patch_text.push_str(&hunk.header);
|
|
patch_text.push('\n');
|
|
for line in &delta.lines {
|
|
patch_text.push_str(&format!(
|
|
"{}{}\n",
|
|
line.origin, line.content
|
|
));
|
|
}
|
|
patch_text.push('\n');
|
|
}
|
|
}
|
|
|
|
let truncated = patch_text.len() > 32_000;
|
|
if truncated {
|
|
patch_text = format!("{}...(truncated)", &patch_text[..32_000]);
|
|
}
|
|
|
|
Ok(json!({
|
|
"files_changed": stats.files_changed,
|
|
"insertions": stats.insertions,
|
|
"deletions": stats.deletions,
|
|
"patch": patch_text,
|
|
"truncated": truncated,
|
|
}))
|
|
}
|
|
}
|