//! Git blob tools — raw object-level operations on blob OIDs. use super::ctx::GitToolCtx; use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema}; use base64::Engine; use std::collections::HashMap; async fn git_blob_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 oid = p.get("oid").and_then(|v| v.as_str()).ok_or("missing oid")?; let domain = ctx.open_repo(project_name, repo_name).await?; let commit_oid = resolve_oid(&domain, oid)?; let info = domain.blob_get(&commit_oid).map_err(|e| e.to_string())?; Ok(serde_json::json!({ "oid": info.oid.to_string(), "size": info.size, "is_binary": info.is_binary, })) } async fn git_blob_exists_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 oid = p.get("oid").and_then(|v| v.as_str()).ok_or("missing oid")?; let domain = ctx.open_repo(project_name, repo_name).await?; let commit_oid = resolve_oid(&domain, oid)?; let exists = domain.blob_exists(&commit_oid); Ok(serde_json::json!({ "oid": commit_oid.to_string(), "exists": exists })) } async fn git_blob_content_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 oid = p.get("oid").and_then(|v| v.as_str()).ok_or("missing oid")?; let max_size = p.get("max_size").and_then(|v| v.as_u64()).unwrap_or(1_048_576) as usize; // 1MB default let domain = ctx.open_repo(project_name, repo_name).await?; let commit_oid = resolve_oid(&domain, oid)?; let blob = domain.blob_content(&commit_oid).map_err(|e| e.to_string())?; if blob.size > max_size { return Err(format!( "blob too large ({} bytes), max {} bytes. Use a smaller max_size or retrieve the raw OID.", blob.size, max_size )); } let (content, is_binary) = if blob.is_binary { (base64::engine::general_purpose::STANDARD.encode(&blob.content), true) } else { (String::from_utf8_lossy(&blob.content).to_string(), false) }; Ok(serde_json::json!({ "oid": blob.oid.to_string(), "size": blob.size, "is_binary": is_binary, "content": content, })) } async fn git_blob_create_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 content = p.get("content").and_then(|v| v.as_str()).ok_or("missing content")?; let encoding = p.get("encoding").and_then(|v| v.as_str()).unwrap_or("utf-8"); let data = match encoding { "base64" => base64::engine::general_purpose::STANDARD .decode(content) .map_err(|e| format!("invalid base64: {}", e))?, "utf-8" => content.as_bytes().to_vec(), other => return Err(format!("unsupported encoding '{}'. Use 'utf-8' or 'base64'.", other)), }; let domain = ctx.open_repo(project_name, repo_name).await?; let oid = domain.blob_create(&data).map_err(|e| e.to_string())?; let info = domain.blob_get(&oid).map_err(|e| e.to_string())?; Ok(serde_json::json!({ "oid": info.oid.to_string(), "size": info.size, "is_binary": info.is_binary, })) } fn resolve_oid( domain: &git::GitDomain, rev: &str, ) -> Result { if rev.len() == 40 && rev.chars().all(|c| c.is_ascii_hexdigit()) { Ok(git::commit::types::CommitOid::new(rev)) } else { domain.commit_get_prefix(rev).map_err(|e| e.to_string()).map(|m| m.oid) } } pub fn register_git_tools(registry: &mut ToolRegistry) { // git_blob_info let p = HashMap::from([ ("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None, }), ("repo_name".into(), ToolParam { name: "repo_name".into(), param_type: "string".into(), description: Some("Repository name".into()), required: true, properties: None, items: None, }), ("oid".into(), ToolParam { name: "oid".into(), param_type: "string".into(), description: Some("Blob OID (full 40-char hex or short prefix)".into()), required: true, properties: None, items: None, }), ]); let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "oid".into()]), }; registry.register( ToolDefinition::new("git_blob_info") .description("Get metadata about a git blob by its OID. Returns size and whether the blob is binary.") .parameters(schema), ToolHandler::new(|ctx, args| { let gctx = GitToolCtx::new(ctx); Box::pin(async move { git_blob_info_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) }) }), ); // git_blob_exists let p = HashMap::from([ ("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None, }), ("repo_name".into(), ToolParam { name: "repo_name".into(), param_type: "string".into(), description: Some("Repository name".into()), required: true, properties: None, items: None, }), ("oid".into(), ToolParam { name: "oid".into(), param_type: "string".into(), description: Some("Blob OID to check".into()), required: true, properties: None, items: None, }), ]); let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "oid".into()]), }; registry.register( ToolDefinition::new("git_blob_exists") .description("Check whether a git blob exists in the repository by its OID.") .parameters(schema), ToolHandler::new(|ctx, args| { let gctx = GitToolCtx::new(ctx); Box::pin(async move { git_blob_exists_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) }) }), ); // git_blob_content let p = HashMap::from([ ("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None, }), ("repo_name".into(), ToolParam { name: "repo_name".into(), param_type: "string".into(), description: Some("Repository name".into()), required: true, properties: None, items: None, }), ("oid".into(), ToolParam { name: "oid".into(), param_type: "string".into(), description: Some("Blob OID to retrieve content for".into()), required: true, properties: None, items: None, }), ("max_size".into(), ToolParam { name: "max_size".into(), param_type: "integer".into(), description: Some("Maximum blob size in bytes (default: 1MB)".into()), required: false, properties: None, items: None, }), ]); let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "oid".into()]), }; registry.register( ToolDefinition::new("git_blob_content") .description("Retrieve the raw content of a git blob by its OID. Binary content is base64-encoded. Limits to 1MB by default.") .parameters(schema), ToolHandler::new(|ctx, args| { let gctx = GitToolCtx::new(ctx); Box::pin(async move { git_blob_content_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) }) }), ); // git_blob_create let p = HashMap::from([ ("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None, }), ("repo_name".into(), ToolParam { name: "repo_name".into(), param_type: "string".into(), description: Some("Repository name".into()), required: true, properties: None, items: None, }), ("content".into(), ToolParam { name: "content".into(), param_type: "string".into(), description: Some("Blob content (utf-8 string or base64-encoded bytes)".into()), required: true, properties: None, items: None, }), ("encoding".into(), ToolParam { name: "encoding".into(), param_type: "string".into(), description: Some("Encoding of content: 'utf-8' (default) or 'base64'".into()), required: false, properties: None, items: None, }), ]); let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "content".into()]), }; registry.register( ToolDefinition::new("git_blob_create") .description("Create a new git blob in the repository. Writes the raw content to the object database and returns the new blob OID. Supports both utf-8 text and base64-encoded binary content.") .parameters(schema), ToolHandler::new(|ctx, args| { let gctx = GitToolCtx::new(ctx); Box::pin(async move { git_blob_create_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) }) }), ); }