gitdataai/lib/service/agent/git_tools/branch.rs

366 lines
10 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::branch_service_client::BranchServiceClient;
use serde_json::{Value, json};
use super::helpers::{arg_str, git_ctx, require_repo_member, rpc_err};
use crate::agent::run::AppAgentContext;
pub struct GitBranchListTool;
impl GitBranchListTool {
pub fn new() -> Self {
Self
}
}
impl Default for GitBranchListTool {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl FunctionCall for GitBranchListTool {
type Context = AppAgentContext;
fn name(&self) -> &'static str {
"git_branch_list"
}
fn description(&self) -> &'static str {
"List all branches in a repository with their HEAD commit OID."
}
fn schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"workspace": { "type": "string", "description": "Workspace name" },
"repo": { "type": "string", "description": "Repository name" }
},
"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 repo =
require_repo_member(git, ctx.user_id, workspace, repo_name).await?;
let mut client = BranchServiceClient::new(git.channel.clone());
let resp = client
.branch_list(p::BranchListRequest {
repo_id: repo.id.to_string(),
})
.await
.map_err(rpc_err)?
.into_inner();
let branches: Vec<Value> = resp.branches.iter().map(|b| json!({
"name": b.name,
"oid": b.oid.as_ref().map(|o| &o.value).unwrap_or(&String::new()),
"is_head": b.is_head,
"is_current": b.is_current,
})).collect();
Ok(json!({ "branches": branches, "count": branches.len() }))
}
}
pub struct GitBranchInfoTool;
impl GitBranchInfoTool {
pub fn new() -> Self {
Self
}
}
impl Default for GitBranchInfoTool {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl FunctionCall for GitBranchInfoTool {
type Context = AppAgentContext;
fn name(&self) -> &'static str {
"git_branch_info"
}
fn description(&self) -> &'static str {
"Get detailed information about a single branch, including its HEAD OID and upstream."
}
fn schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"workspace": { "type": "string", "description": "Workspace name" },
"repo": { "type": "string", "description": "Repository name" },
"branch": { "type": "string", "description": "Branch name" }
},
"required": ["workspace", "repo", "branch"]
})
}
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 repo =
require_repo_member(git, ctx.user_id, workspace, repo_name).await?;
let mut client = BranchServiceClient::new(git.channel.clone());
let resp = client
.branch_info(p::BranchInfoRequest {
repo_id: repo.id.to_string(),
branch: branch.to_string(),
})
.await
.map_err(rpc_err)?
.into_inner();
let b = resp.branch.ok_or_else(|| {
AiError::Config(format!("branch '{branch}' not found"))
})?;
Ok(json!({
"name": b.name,
"oid": b.oid.as_ref().map(|o| &o.value),
"is_head": b.is_head,
"is_current": b.is_current,
"upstream": b.upstream,
}))
}
}
pub struct GitBranchAheadBehindTool;
impl GitBranchAheadBehindTool {
pub fn new() -> Self {
Self
}
}
impl Default for GitBranchAheadBehindTool {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl FunctionCall for GitBranchAheadBehindTool {
type Context = AppAgentContext;
fn name(&self) -> &'static str {
"git_branch_ahead_behind"
}
fn description(&self) -> &'static str {
"Compare a local branch with its remote tracking branch. Returns commits ahead and behind."
}
fn schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"workspace": { "type": "string", "description": "Workspace name" },
"repo": { "type": "string", "description": "Repository name" },
"local_branch": { "type": "string", "description": "Local branch name" },
"remote_branch": { "type": "string", "description": "Remote tracking branch name" }
},
"required": ["workspace", "repo", "local_branch", "remote_branch"]
})
}
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 local_branch = arg_str(&args, "local_branch")?;
let remote_branch = arg_str(&args, "remote_branch")?;
let repo =
require_repo_member(git, ctx.user_id, workspace, repo_name).await?;
let mut client = BranchServiceClient::new(git.channel.clone());
let resp = client
.branch_ahead_behind(p::BranchAheadBehindRequest {
repo_id: repo.id.to_string(),
local_branch: local_branch.to_string(),
remote_branch: remote_branch.to_string(),
})
.await
.map_err(rpc_err)?
.into_inner();
Ok(json!({ "ahead": resp.ahead, "behind": resp.behind }))
}
}
pub struct GitBranchDeleteTool;
impl GitBranchDeleteTool {
pub fn new() -> Self {
Self
}
}
impl Default for GitBranchDeleteTool {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl FunctionCall for GitBranchDeleteTool {
type Context = AppAgentContext;
fn name(&self) -> &'static str {
"git_branch_delete"
}
fn description(&self) -> &'static str {
"Delete a branch from the repository. Requires write access."
}
fn schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"workspace": { "type": "string", "description": "Workspace name" },
"repo": { "type": "string", "description": "Repository name" },
"name": { "type": "string", "description": "Branch name to delete" },
"force": { "type": "boolean", "description": "Force delete (even if not merged)" }
},
"required": ["workspace", "repo", "name"]
})
}
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 name = arg_str(&args, "name")?;
let force =
args.get("force").and_then(|v| v.as_bool()).unwrap_or(false);
let repo =
require_repo_member(git, ctx.user_id, workspace, repo_name).await?;
let mut client = BranchServiceClient::new(git.channel.clone());
client
.branch_delete(p::BranchDeleteRequest {
repo_id: repo.id.to_string(),
params: Some(p::BranchDeleteParams {
name: name.to_string(),
force,
}),
})
.await
.map_err(rpc_err)?;
Ok(json!({ "success": true, "branch": name }))
}
}
pub struct GitCreateBranchTool;
impl GitCreateBranchTool {
pub fn new() -> Self {
Self
}
}
impl Default for GitCreateBranchTool {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl FunctionCall for GitCreateBranchTool {
type Context = AppAgentContext;
fn name(&self) -> &'static str {
"git_create_branch"
}
fn description(&self) -> &'static str {
"Create a new branch in a repository. Requires write access."
}
fn schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"workspace": { "type": "string", "description": "Workspace name" },
"repo": { "type": "string", "description": "Repository name" },
"name": { "type": "string", "description": "New branch name" },
"oid": { "type": "string", "description": "Commit OID to branch from" },
"force": { "type": "boolean", "description": "Force create (overwrite existing)" }
},
"required": ["workspace", "repo", "name", "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 name = arg_str(&args, "name")?;
let oid = arg_str(&args, "oid")?;
let force =
args.get("force").and_then(|v| v.as_bool()).unwrap_or(false);
let repo =
require_repo_member(git, ctx.user_id, workspace, repo_name).await?;
let mut client = BranchServiceClient::new(git.channel.clone());
client
.branch_fork(p::BranchForkRequest {
repo_id: repo.id.to_string(),
params: Some(p::BranchForkParams {
name: name.to_string(),
oid: Some(p::ObjectId {
value: oid.to_string(),
}),
force,
}),
})
.await
.map_err(rpc_err)?;
Ok(json!({ "success": true, "branch": name, "oid": oid }))
}
}