147 lines
6.2 KiB
Rust
147 lines
6.2 KiB
Rust
//! Git diff and blame tools.
|
|
|
|
use super::ctx::GitToolCtx;
|
|
use agent::ToolRegistry;
|
|
|
|
async fn git_diff_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
|
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
|
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
|
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
|
let base = p.get("base").and_then(|v| v.as_str()).map(|s| s.to_string());
|
|
let head = p.get("head").and_then(|v| v.as_str()).map(|s| s.to_string());
|
|
let paths = p.get("paths").and_then(|v| v.as_array()).map(|a| {
|
|
a.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect::<Vec<_>>()
|
|
});
|
|
|
|
let domain = ctx.open_repo(project_name, repo_name).await?;
|
|
|
|
let resolve = |rev: &str| -> Result<git::commit::types::CommitOid, String> {
|
|
if rev.len() >= 40 {
|
|
Ok(git::commit::types::CommitOid::new(rev))
|
|
} else {
|
|
domain.commit_get_prefix(rev).map_err(|e| e.to_string()).map(|m| m.oid)
|
|
}
|
|
};
|
|
|
|
let base_oid = match &base {
|
|
Some(b) => Some(resolve(b)?),
|
|
None => None,
|
|
};
|
|
let head_oid = match &head {
|
|
Some(h) => Some(resolve(h)?),
|
|
None => None,
|
|
};
|
|
|
|
let opts = paths.map(|ps| {
|
|
let mut o = git::diff::types::DiffOptions::new();
|
|
for p in ps { o = o.pathspec(&p); }
|
|
Some(o)
|
|
}).flatten();
|
|
|
|
let result = match (&base_oid, &head_oid) {
|
|
(None, None) => {
|
|
let head_meta = domain.commit_get_prefix("HEAD").map_err(|e| e.to_string())?;
|
|
domain.diff_commit_to_workdir(&head_meta.oid, opts).map_err(|e| e.to_string())?
|
|
}
|
|
(Some(base), None) => {
|
|
domain.diff_commit_to_workdir(base, opts).map_err(|e| e.to_string())?
|
|
}
|
|
(Some(base), Some(head_oid_val)) => {
|
|
domain.diff_tree_to_tree(Some(base), Some(head_oid_val), opts).map_err(|e| e.to_string())?
|
|
}
|
|
(None, Some(_)) => {
|
|
return Err("base revision required when head is specified".into());
|
|
}
|
|
};
|
|
|
|
let files: Vec<_> = result.deltas.iter().map(|d| {
|
|
serde_json::json!({ "path": d.new_file.path, "status": format!("{:?}", d.status), "is_binary": d.new_file.is_binary })
|
|
}).collect();
|
|
|
|
Ok(serde_json::json!({
|
|
"stats": { "files_changed": result.stats.files_changed, "insertions": result.stats.insertions, "deletions": result.stats.deletions },
|
|
"files": files
|
|
}))
|
|
}
|
|
|
|
async fn git_diff_stats_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
|
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
|
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
|
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
|
let base = p.get("base").and_then(|v| v.as_str()).ok_or("missing base")?;
|
|
let head = p.get("head").and_then(|v| v.as_str()).ok_or("missing head")?;
|
|
|
|
let domain = ctx.open_repo(project_name, repo_name).await?;
|
|
|
|
let stats = if base.len() >= 40 || head.len() >= 40 {
|
|
domain.diff_stats(&git::commit::types::CommitOid::new(base), &git::commit::types::CommitOid::new(head))
|
|
.map_err(|e| e.to_string())?
|
|
} else {
|
|
let b = domain.commit_get_prefix(base).map_err(|e| e.to_string())?.oid;
|
|
let h = domain.commit_get_prefix(head).map_err(|e| e.to_string())?.oid;
|
|
domain.diff_stats(&b, &h).map_err(|e| e.to_string())?
|
|
};
|
|
|
|
Ok(serde_json::json!({
|
|
"files_changed": stats.files_changed,
|
|
"insertions": stats.insertions,
|
|
"deletions": stats.deletions
|
|
}))
|
|
}
|
|
|
|
async fn git_blame_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
|
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
|
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
|
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
|
let path = p.get("path").and_then(|v| v.as_str()).ok_or("missing path")?;
|
|
let rev = p.get("rev").and_then(|v| v.as_str()).map(|s| s.to_string()).unwrap_or_else(|| "HEAD".to_string());
|
|
let from_line = p.get("from_line").and_then(|v| v.as_u64().map(|n| n as u32));
|
|
let to_line = p.get("to_line").and_then(|v| v.as_u64().map(|n| n as u32));
|
|
|
|
let domain = ctx.open_repo(project_name, repo_name).await?;
|
|
let oid = if rev.len() >= 40 {
|
|
git::commit::types::CommitOid::new(&rev)
|
|
} else {
|
|
domain.commit_get_prefix(&rev).map_err(|e| e.to_string())?.oid
|
|
};
|
|
|
|
use git::blame::ops::BlameOptions;
|
|
let mut bopts = BlameOptions::new();
|
|
if let Some(fl) = from_line { bopts = bopts.min_line(fl as usize); }
|
|
if let Some(tl) = to_line { bopts = bopts.max_line(tl as usize); }
|
|
|
|
let hunks = domain.blame_file(&oid, path, Some(bopts)).map_err(|e| e.to_string())?;
|
|
|
|
let result: Vec<_> = hunks.iter().map(|h| {
|
|
let oid = h.commit_oid.to_string();
|
|
serde_json::json!({
|
|
"commit_oid": oid.clone(),
|
|
"short_oid": oid.get(..7).unwrap_or(&oid).to_string(),
|
|
"final_start_line": h.final_start_line,
|
|
"final_lines": h.final_lines,
|
|
"orig_start_line": h.orig_start_line,
|
|
"orig_path": h.orig_path,
|
|
"boundary": h.boundary
|
|
})
|
|
}).collect();
|
|
|
|
Ok(serde_json::to_value(result).map_err(|e| e.to_string())?)
|
|
}
|
|
|
|
macro_rules! register_fn {
|
|
($registry:expr, $name:expr, $exec:expr) => {
|
|
let handler_fn = move |ctx: agent::ToolContext, args: serde_json::Value| async move {
|
|
let gctx = super::ctx::GitToolCtx::new(ctx);
|
|
$exec(gctx, args)
|
|
.await
|
|
.map_err(agent::ToolError::ExecutionError)
|
|
};
|
|
$registry.register_fn($name, handler_fn);
|
|
};
|
|
}
|
|
|
|
pub fn register_git_tools(registry: &mut ToolRegistry) {
|
|
register_fn!(registry, "git_diff", git_diff_exec);
|
|
register_fn!(registry, "git_diff_stats", git_diff_stats_exec);
|
|
register_fn!(registry, "git_blame", git_blame_exec);
|
|
} |