439 lines
14 KiB
Rust
439 lines
14 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::commit_service_client::CommitServiceClient;
|
|
use serde_json::{json, Value};
|
|
|
|
use super::helpers::{arg_str, arg_opt_str, arg_u64, git_ctx, require_repo_member, rpc_err};
|
|
use crate::agent::run::AppAgentContext;
|
|
|
|
pub struct GitCommitHistoryTool;
|
|
|
|
impl GitCommitHistoryTool {
|
|
pub fn new() -> Self {
|
|
Self
|
|
}
|
|
}
|
|
|
|
impl Default for GitCommitHistoryTool {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl FunctionCall for GitCommitHistoryTool {
|
|
type Context = AppAgentContext;
|
|
|
|
fn name(&self) -> &'static str {
|
|
"git_commit_history"
|
|
}
|
|
|
|
fn description(&self) -> &'static str {
|
|
"List recent commits on a branch. Returns commit OID, message, author, and timestamp."
|
|
}
|
|
|
|
fn schema(&self) -> Value {
|
|
json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"workspace": {
|
|
"type": "string",
|
|
"description": "Workspace name (e.g. 'my-org')"
|
|
},
|
|
"repo": {
|
|
"type": "string",
|
|
"description": "Repository name"
|
|
},
|
|
"branch": {
|
|
"type": "string",
|
|
"description": "Branch name (optional, defaults to default branch)"
|
|
},
|
|
"limit": {
|
|
"type": "integer",
|
|
"description": "Max commits to return (default 20, max 100)"
|
|
},
|
|
"skip": {
|
|
"type": "integer",
|
|
"description": "Number of commits to skip (for pagination)"
|
|
}
|
|
},
|
|
"required": ["workspace", "repo"]
|
|
})
|
|
}
|
|
|
|
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 branch = arg_opt_str(&args, "branch").map(String::from);
|
|
let limit = arg_u64(&args, "limit", 20).min(100);
|
|
let skip = arg_u64(&args, "skip", 0);
|
|
|
|
let repo = require_repo_member(git, ctx.user_id, workspace, repo_name).await?;
|
|
|
|
let mut client = CommitServiceClient::new(git.channel.clone());
|
|
let resp = client
|
|
.commit_history(p::CommitHistoryRequest {
|
|
repo_id: repo.id.to_string(),
|
|
limit,
|
|
skip,
|
|
sort: p::CommitWalkSort::Time as i32,
|
|
branch,
|
|
})
|
|
.await
|
|
.map_err(rpc_err)?
|
|
.into_inner();
|
|
|
|
let commits: Vec<Value> = resp
|
|
.commits
|
|
.iter()
|
|
.map(|c| {
|
|
json!({
|
|
"oid": c.oid.as_ref().map(|o| &o.value).unwrap_or(&String::new()),
|
|
"summary": c.summary,
|
|
"message": c.message,
|
|
"author_name": c.author.as_ref().map(|a| &a.name).unwrap_or(&String::new()),
|
|
"author_email": c.author.as_ref().map(|a| &a.email).unwrap_or(&String::new()),
|
|
"time": c.author.as_ref().map(|a| a.time_secs).unwrap_or(0),
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
Ok(json!({ "commits": commits, "count": commits.len() }))
|
|
}
|
|
}
|
|
|
|
pub struct GitCommitInfoTool;
|
|
|
|
impl GitCommitInfoTool {
|
|
pub fn new() -> Self {
|
|
Self
|
|
}
|
|
}
|
|
|
|
impl Default for GitCommitInfoTool {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl FunctionCall for GitCommitInfoTool {
|
|
type Context = AppAgentContext;
|
|
|
|
fn name(&self) -> &'static str {
|
|
"git_commit_info"
|
|
}
|
|
|
|
fn description(&self) -> &'static str {
|
|
"Get detailed information about a specific commit by its OID."
|
|
}
|
|
|
|
fn schema(&self) -> Value {
|
|
json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"workspace": { "type": "string", "description": "Workspace name" },
|
|
"repo": { "type": "string", "description": "Repository name" },
|
|
"oid": { "type": "string", "description": "Commit OID (SHA)" }
|
|
},
|
|
"required": ["workspace", "repo", "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 oid = arg_str(&args, "oid")?;
|
|
|
|
let repo = require_repo_member(git, ctx.user_id, workspace, repo_name).await?;
|
|
|
|
let mut client = CommitServiceClient::new(git.channel.clone());
|
|
let resp = client
|
|
.commit_info(p::CommitInfoRequest {
|
|
repo_id: repo.id.to_string(),
|
|
oid: Some(p::ObjectId { value: oid.to_string() }),
|
|
})
|
|
.await
|
|
.map_err(rpc_err)?
|
|
.into_inner();
|
|
|
|
let c = resp
|
|
.commit
|
|
.ok_or_else(|| AiError::Response("commit not found".to_string()))?;
|
|
let parent_ids: Vec<String> = c.parent_ids.iter().map(|o| o.value.clone()).collect();
|
|
|
|
Ok(json!({
|
|
"oid": c.oid.as_ref().map(|o| &o.value),
|
|
"summary": c.summary,
|
|
"message": c.message,
|
|
"author_name": c.author.as_ref().map(|a| &a.name),
|
|
"author_email": c.author.as_ref().map(|a| &a.email),
|
|
"author_time": c.author.as_ref().map(|a| a.time_secs),
|
|
"committer_name": c.committer.as_ref().map(|a| &a.name),
|
|
"tree_id": c.tree_id.as_ref().map(|o| &o.value),
|
|
"parent_ids": parent_ids,
|
|
}))
|
|
}
|
|
}
|
|
|
|
|
|
pub struct GitCommitExistsTool;
|
|
|
|
impl GitCommitExistsTool {
|
|
pub fn new() -> Self { Self }
|
|
}
|
|
|
|
impl Default for GitCommitExistsTool {
|
|
fn default() -> Self { Self::new() }
|
|
}
|
|
|
|
#[async_trait]
|
|
impl FunctionCall for GitCommitExistsTool {
|
|
type Context = AppAgentContext;
|
|
|
|
fn name(&self) -> &'static str { "git_commit_exists" }
|
|
|
|
fn description(&self) -> &'static str {
|
|
"Check whether a specific commit OID exists in the repository."
|
|
}
|
|
|
|
fn schema(&self) -> Value {
|
|
json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"workspace": { "type": "string", "description": "Workspace name" },
|
|
"repo": { "type": "string", "description": "Repository name" },
|
|
"oid": { "type": "string", "description": "Commit OID (SHA)" }
|
|
},
|
|
"required": ["workspace", "repo", "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 oid = arg_str(&args, "oid")?;
|
|
|
|
let repo = require_repo_member(git, ctx.user_id, workspace, repo_name).await?;
|
|
|
|
let mut client = CommitServiceClient::new(git.channel.clone());
|
|
let resp = client
|
|
.commit_exists(p::CommitExistsRequest {
|
|
repo_id: repo.id.to_string(),
|
|
oid: Some(p::ObjectId { value: oid.to_string() }),
|
|
})
|
|
.await
|
|
.map_err(rpc_err)?
|
|
.into_inner();
|
|
|
|
Ok(json!({ "exists": resp.exists }))
|
|
}
|
|
}
|
|
|
|
|
|
|
|
pub struct GitCherryPickTool;
|
|
|
|
impl GitCherryPickTool {
|
|
pub fn new() -> Self { Self }
|
|
}
|
|
|
|
impl Default for GitCherryPickTool {
|
|
fn default() -> Self { Self::new() }
|
|
}
|
|
|
|
#[async_trait]
|
|
impl FunctionCall for GitCherryPickTool {
|
|
type Context = AppAgentContext;
|
|
|
|
fn name(&self) -> &'static str { "git_cherry_pick" }
|
|
|
|
fn description(&self) -> &'static str {
|
|
"Cherry-pick a commit onto the current branch. Requires write access."
|
|
}
|
|
|
|
fn schema(&self) -> Value {
|
|
json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"workspace": { "type": "string", "description": "Workspace name" },
|
|
"repo": { "type": "string", "description": "Repository name" },
|
|
"oid": { "type": "string", "description": "Commit OID to cherry-pick" },
|
|
"message": { "type": "string", "description": "Override commit message (optional)" },
|
|
"update_ref": { "type": "string", "description": "Branch ref to update (optional)" }
|
|
},
|
|
"required": ["workspace", "repo", "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 oid = arg_str(&args, "oid")?;
|
|
let message = args.get("message").and_then(|v| v.as_str()).map(String::from);
|
|
let update_ref = arg_opt_str(&args, "update_ref").map(String::from);
|
|
|
|
let repo = require_repo_member(git, ctx.user_id, workspace, repo_name).await?;
|
|
|
|
let mut client = CommitServiceClient::new(git.channel.clone());
|
|
let resp = client
|
|
.cherry_pick(p::CherryPickRequest {
|
|
repo_id: repo.id.to_string(),
|
|
params: Some(p::CommitCherryPickParams {
|
|
cherrypick_oid: Some(p::ObjectId { value: oid.to_string() }),
|
|
message,
|
|
update_ref,
|
|
..Default::default()
|
|
}),
|
|
})
|
|
.await
|
|
.map_err(rpc_err)?
|
|
.into_inner();
|
|
|
|
Ok(json!({
|
|
"success": true,
|
|
"new_oid": resp.oid.as_ref().map(|o| &o.value),
|
|
}))
|
|
}
|
|
}
|
|
|
|
|
|
pub struct GitCommitCreateTool;
|
|
|
|
impl GitCommitCreateTool {
|
|
pub fn new() -> Self {
|
|
Self
|
|
}
|
|
}
|
|
|
|
impl Default for GitCommitCreateTool {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl FunctionCall for GitCommitCreateTool {
|
|
type Context = AppAgentContext;
|
|
|
|
fn name(&self) -> &'static str {
|
|
"git_commit_create"
|
|
}
|
|
|
|
fn description(&self) -> &'static str {
|
|
"Create a commit with new or updated files in a workspace repository. Author is set to the requesting user, committer is redpanda <redpanda@gitdata.ai>. Provide workspace name, repo name, branch, commit message, and a list of files (path and content for each)."
|
|
}
|
|
|
|
fn schema(&self) -> Value {
|
|
json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"workspace": {
|
|
"type": "string",
|
|
"description": "Workspace name (e.g. 'my-org')"
|
|
},
|
|
"repo": {
|
|
"type": "string",
|
|
"description": "Repository name"
|
|
},
|
|
"branch": {
|
|
"type": "string",
|
|
"description": "Branch name to commit to (e.g. 'main'). If the branch does not exist, it will be created."
|
|
},
|
|
"message": {
|
|
"type": "string",
|
|
"description": "Commit message"
|
|
},
|
|
"files": {
|
|
"type": "array",
|
|
"description": "List of file changes",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"path": {
|
|
"type": "string",
|
|
"description": "File path relative to repo root (e.g. 'src/main.rs')"
|
|
},
|
|
"content": {
|
|
"type": "string",
|
|
"description": "File content as a string"
|
|
}
|
|
},
|
|
"required": ["path", "content"]
|
|
}
|
|
}
|
|
},
|
|
"required": ["workspace", "repo", "branch", "message", "files"]
|
|
})
|
|
}
|
|
|
|
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 branch = arg_str(&args, "branch")?;
|
|
let message = arg_str(&args, "message")?;
|
|
|
|
let files_val = args
|
|
.get("files")
|
|
.and_then(|v| v.as_array())
|
|
.ok_or_else(|| AiError::Config("'files' must be an array of {path, content} objects".to_string()))?;
|
|
|
|
let mut file_changes: Vec<super::helpers::FileChange> = Vec::new();
|
|
for f in files_val {
|
|
let path = f
|
|
.get("path")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or_else(|| AiError::Config("each file must have a 'path' field".to_string()))?;
|
|
let content = f
|
|
.get("content")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or_else(|| AiError::Config("each file must have a 'content' field".to_string()))?;
|
|
file_changes.push(super::helpers::FileChange {
|
|
path: path.to_string(),
|
|
content: content.as_bytes().to_vec(),
|
|
});
|
|
}
|
|
|
|
if file_changes.is_empty() {
|
|
return Err(AiError::Config("'files' array must not be empty".to_string()));
|
|
}
|
|
|
|
let repo = require_repo_member(git, ctx.user_id, workspace, repo_name).await?;
|
|
|
|
let mut client = CommitServiceClient::new(git.channel.clone());
|
|
let resp = client
|
|
.create_commit(tonic::Request::new(p::CreateCommitRequest {
|
|
repo_id: repo.id.to_string(),
|
|
branch: branch.to_string(),
|
|
message: message.to_string(),
|
|
author_name: ctx.user_id.to_string(),
|
|
author_email: format!("{}@gitdata.ai", ctx.user_id),
|
|
committer_name: "redpanda".to_string(),
|
|
committer_email: "redpanda@gitdata.ai".to_string(),
|
|
files: file_changes
|
|
.into_iter()
|
|
.map(|fc| p::FileChange {
|
|
path: fc.path,
|
|
content: fc.content,
|
|
})
|
|
.collect(),
|
|
}))
|
|
.await
|
|
.map_err(rpc_err)?
|
|
.into_inner();
|
|
|
|
Ok(json!({
|
|
"success": true,
|
|
"oid": resp.oid.as_ref().map(|o| &o.value),
|
|
"files_committed": files_val.len(),
|
|
}))
|
|
}
|
|
}
|