366 lines
10 KiB
Rust
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 }))
|
|
}
|
|
}
|