//! Git commit-related tools. use super::ctx::GitToolCtx; use agent::ToolRegistry; use chrono::TimeZone; // --- Execution functions for each tool --- async fn git_log_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result { let p: serde_json::Map = 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 rev = p.get("rev").and_then(|v| v.as_str()).map(|s| s.to_string()); let limit = p.get("limit").and_then(|v| v.as_u64()).unwrap_or(20) as usize; let skip = p.get("skip").and_then(|v| v.as_u64()).unwrap_or(0) as usize; let domain = ctx.open_repo(project_name, repo_name).await?; let commits = domain.commit_log(rev.as_deref(), skip, limit) .map_err(|e| e.to_string())?; // Flatten to simple JSON let result: Vec<_> = commits.iter().map(|c| { use chrono::TimeZone; let ts = c.author.time_secs + (c.author.offset_minutes as i64 * 60); let time_str = chrono::Utc.timestamp_opt(ts, 0).single() .map(|dt| dt.to_rfc3339()).unwrap_or_else(|| format!("{}", c.author.time_secs)); let oid = c.oid.to_string(); let short_oid = oid.get(..7).unwrap_or(&oid).to_string(); serde_json::json!({ "oid": oid, "short_oid": short_oid, "message": c.message, "summary": c.summary, "author_name": c.author.name, "author_email": c.author.email, "author_time": time_str, "committer_name": c.committer.name, "committer_email": c.committer.email, "parent_oids": c.parent_ids.iter().map(|p| p.to_string()).collect::>(), "tree_oid": c.tree_id.to_string() }) }).collect(); Ok(serde_json::to_value(result).map_err(|e| e.to_string())?) } async fn git_show_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result { let p: serde_json::Map = 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 rev = p.get("rev").and_then(|v| v.as_str()).ok_or("missing rev")?; let domain = ctx.open_repo(project_name, repo_name).await?; let meta = if rev.len() >= 40 { domain.commit_get(&git::commit::types::CommitOid::new(rev)).map_err(|e| e.to_string())? } else { domain.commit_get_prefix(rev).map_err(|e| e.to_string())? }; let refs = domain.commit_refs(&meta.oid).map_err(|e| e.to_string())?; use chrono::TimeZone; let ts = meta.author.time_secs + (meta.author.offset_minutes as i64 * 60); let author_time = chrono::Utc.timestamp_opt(ts, 0).single() .map(|dt| dt.to_rfc3339()).unwrap_or_else(|| format!("{}", meta.author.time_secs)); let oid = meta.oid.to_string(); let short_oid = oid.get(..7).unwrap_or(&oid).to_string(); Ok(serde_json::json!({ "commit": { "oid": oid, "short_oid": short_oid, "message": meta.message, "summary": meta.summary, "author_name": meta.author.name, "author_email": meta.author.email, "author_time": author_time, "committer_name": meta.committer.name, "committer_email": meta.committer.email, "parent_oids": meta.parent_ids.iter().map(|p| p.to_string()).collect::>(), "tree_oid": meta.tree_id.to_string() }, "refs": refs.into_iter().map(|r| serde_json::json!({ "name": r.name, "is_tag": r.is_tag })).collect::>() })) } async fn git_search_commits_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result { let p: serde_json::Map = 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 query = p.get("query").and_then(|v| v.as_str()).ok_or("missing query")?; let limit = p.get("limit").and_then(|v| v.as_u64()).unwrap_or(20) as usize; let domain = ctx.open_repo(project_name, repo_name).await?; let commits = domain.commit_log(Some("HEAD"), 0, 100).map_err(|e| e.to_string())?; let q = query.to_lowercase(); let result: Vec<_> = commits.iter() .filter(|c| c.message.to_lowercase().contains(&q)) .take(limit) .map(|c| flatten_commit(c)) .collect(); Ok(serde_json::to_value(result).map_err(|e| e.to_string())?) } fn flatten_commit(c: &git::commit::types::CommitMeta) -> serde_json::Value { use chrono::TimeZone; let ts = c.author.time_secs + (c.author.offset_minutes as i64 * 60); let author_time = chrono::Utc.timestamp_opt(ts, 0).single() .map(|dt| dt.to_rfc3339()).unwrap_or_else(|| format!("{}", c.author.time_secs)); let oid = c.oid.to_string(); serde_json::json!({ "oid": oid.clone(), "short_oid": oid.get(..7).unwrap_or(&oid).to_string(), "message": c.message, "summary": c.summary, "author_name": c.author.name, "author_email": c.author.email, "author_time": author_time, "committer_name": c.committer.name, "committer_email": c.committer.email, "parent_oids": c.parent_ids.iter().map(|p| p.to_string()).collect::>(), "tree_oid": c.tree_id.to_string() }) } async fn git_commit_info_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result { let p: serde_json::Map = 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 rev = p.get("rev").and_then(|v| v.as_str()).ok_or("missing rev")?; let domain = ctx.open_repo(project_name, repo_name).await?; let meta = if rev.len() >= 40 { domain.commit_get(&git::commit::types::CommitOid::new(rev)).map_err(|e| e.to_string())? } else { domain.commit_get_prefix(rev).map_err(|e| e.to_string())? }; Ok(flatten_commit(&meta)) } async fn git_graph_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result { let p: serde_json::Map = 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 rev = p.get("rev").and_then(|v| v.as_str()).map(|s| s.to_string()); let limit = p.get("limit").and_then(|v| v.as_u64()).unwrap_or(20) as usize; let domain = ctx.open_repo(project_name, repo_name).await?; let commits = domain.commit_log(rev.as_deref(), 0, limit).map_err(|e| e.to_string())?; let mut col_map: std::collections::HashMap = std::collections::HashMap::new(); let lines: Vec<_> = commits.iter().map(|m| { let lane_index = *col_map.get(m.oid.as_str()).unwrap_or(&0); let oid = m.oid.to_string(); let refs = match domain.commit_refs(&m.oid) { Ok(refs) => refs.iter().map(|r| { if r.is_tag { format!("tag: {}", r.name.trim_start_matches("refs/tags/")) } else if r.is_remote { r.name.trim_start_matches("refs/remotes/").to_string() } else { r.name.trim_start_matches("refs/heads/").to_string() } }).collect::>().join(", "), Err(_) => String::new(), }; for (i, p) in m.parent_ids.iter().enumerate() { if i == 0 { col_map.insert(p.to_string(), lane_index); } else { col_map.remove(p.as_str()); } } let ts = m.author.time_secs + (m.author.offset_minutes as i64 * 60); let author_time = chrono::Utc.timestamp_opt(ts, 0).single() .map(|dt| dt.to_rfc3339()).unwrap_or_else(|| format!("{}", m.author.time_secs)); serde_json::json!({ "oid": oid.clone(), "short_oid": oid.get(..7).unwrap_or(&oid).to_string(), "refs": refs, "short_message": m.summary, "lane_index": lane_index, "author_name": m.author.name, "author_email": m.author.email, "author_time": author_time, "parent_oids": m.parent_ids.iter().map(|p| p.to_string()).collect::>() }) }).collect(); Ok(serde_json::to_value(lines).map_err(|e| e.to_string())?) } async fn git_reflog_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result { let p: serde_json::Map = 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 ref_name = p.get("ref_name").and_then(|v| v.as_str()).map(|s| s.to_string()); let limit = p.get("limit").and_then(|v| v.as_u64()).unwrap_or(50) as usize; let domain = ctx.open_repo(project_name, repo_name).await?; let entries = domain.reflog_entries(ref_name.as_deref()).map_err(|e| e.to_string())?; let result: Vec<_> = entries.iter() .take(limit) .map(|e| { let ts = e.time_secs; let time_str = chrono::Utc.timestamp_opt(ts, 0).single() .map(|dt| dt.to_rfc3339()).unwrap_or_else(|| format!("{}", ts)); serde_json::json!({ "oid_new": e.oid_new.to_string(), "oid_old": e.oid_old.to_string(), "committer_name": e.committer_name, "committer_email": e.committer_email, "time": time_str, "message": e.message, "ref_name": e.ref_name }) }) .collect(); Ok(serde_json::to_value(result).map_err(|e| e.to_string())?) } // --- Registration macro --- 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_log", git_log_exec); register_fn!(registry, "git_show", git_show_exec); register_fn!(registry, "git_search_commits", git_search_commits_exec); register_fn!(registry, "git_commit_info", git_commit_info_exec); register_fn!(registry, "git_graph", git_graph_exec); register_fn!(registry, "git_reflog", git_reflog_exec); }