use proto::kenshell::v1 as pb; use serde::{Deserialize, Serialize}; use crate::context::facade::GitContext; use crate::service::GitService; use crate::service::rpc::{rpc_error, wait_job}; use crate::share::errors::GitError; use crate::share::validate::{validate_refspec, validate_remote_name, validate_remote_url}; #[derive(Debug, Clone, Serialize, utoipa::ToSchema)] pub struct ImportResultDto { pub default_branch: String, pub commit_count: i64, } #[derive(Debug, Clone, Serialize, utoipa::ToSchema)] pub struct FetchResultDto { pub updated: Vec, pub new: Vec, } #[derive(Debug, Clone, Serialize, utoipa::ToSchema)] pub struct PushResultDto { pub pushed: Vec, pub rejected: Vec, } #[derive(Debug, Clone, Deserialize, utoipa::ToSchema)] pub struct RemoteAuthDto { #[serde(default)] pub token: Option, #[serde(default)] pub username: Option, } #[derive(Debug, Clone, Deserialize)] pub struct RemoteConfigDto { pub url: String, #[serde(default)] pub auth: Option, #[serde(default)] pub name: Option, #[serde(default)] pub refspec: Option, } #[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)] pub struct RemoteDto { pub name: String, pub fetch_url: Option, pub push_url: Option, } fn remote_auth(auth: Option<&RemoteAuthDto>) -> Option { let auth = auth?; if auth.token.as_deref().unwrap_or("").is_empty() { return None; } Some(pb::RemoteAuth { username: auth.username.clone().unwrap_or_else(|| "git".to_string()), token: auth.token.clone().unwrap_or_default(), authorization_header: String::new(), ssh_key: String::new(), known_hosts: String::new(), resolved_address: String::new(), }) } impl GitService { pub async fn remote_clone_import( &self, ctx: &GitContext, url: String, auth: Option, ) -> Result { validate_remote_url(&url)?; let mut client = ctx.ks.remote(); let job = client .start_remote_import(pb::StartRemoteImportRequest { repository: Some(ctx.repository_ref()), url, auth: remote_auth(auth.as_ref()), actor: Some(ctx.actor()), idempotency_key: uuid::Uuid::now_v7().to_string(), }) .await .map_err(rpc_error)? .into_inner() .job .ok_or_else(|| GitError::Internal("import job missing".into()))?; let job = wait_job(&ctx.ks, &job.id, self.job_timeout()).await?; match job.result { Some(pb::job::Result::Remote(remote)) => Ok(ImportResultDto { default_branch: remote.default_branch, commit_count: remote.commit_count as i64, }), _ => Err(GitError::Internal("import job returned no result".into())), } } pub async fn remote_fetch( &self, ctx: &GitContext, remote: String, auth: Option, ) -> Result { validate_remote_name(&remote)?; let mut client = ctx.ks.remote(); let job = client .start_remote_fetch(pb::StartRemoteFetchRequest { repository: Some(ctx.repository_ref()), remote, auth: remote_auth(auth.as_ref()), actor: Some(ctx.actor()), idempotency_key: uuid::Uuid::now_v7().to_string(), }) .await .map_err(rpc_error)? .into_inner() .job .ok_or_else(|| GitError::Internal("fetch job missing".into()))?; let job = wait_job(&ctx.ks, &job.id, self.job_timeout()).await?; match job.result { Some(pb::job::Result::Remote(remote)) => Ok(FetchResultDto { updated: remote.updated, new: remote.new_refs, }), _ => Err(GitError::Internal("fetch job returned no result".into())), } } pub async fn remote_push( &self, ctx: &GitContext, remote: String, refspec: Option, auth: Option, ) -> Result { validate_remote_name(&remote)?; if let Some(refspec) = refspec.as_deref() { validate_refspec(refspec)?; } let mut client = ctx.ks.remote(); let job = client .start_remote_push(pb::StartRemotePushRequest { repository: Some(ctx.repository_ref()), remote, refspec: refspec.unwrap_or_default(), force: false, auth: remote_auth(auth.as_ref()), actor: Some(ctx.actor()), idempotency_key: uuid::Uuid::now_v7().to_string(), }) .await .map_err(rpc_error)? .into_inner() .job .ok_or_else(|| GitError::Internal("push job missing".into()))?; let job = wait_job(&ctx.ks, &job.id, self.job_timeout()).await?; match job.result { Some(pb::job::Result::Remote(remote)) => Ok(PushResultDto { pushed: remote.pushed, rejected: remote.rejected, }), _ => Err(GitError::Internal("push job returned no result".into())), } } pub async fn remote_add( &self, ctx: &GitContext, name: String, url: String, ) -> Result<(), GitError> { validate_remote_name(&name)?; validate_remote_url(&url)?; let mut client = ctx.ks.remote(); client .upsert_remote(pb::UpsertRemoteRequest { repository: Some(ctx.repository_ref()), name, url, actor: Some(ctx.actor()), options: Some(ctx.write_options()), }) .await .map_err(rpc_error)?; Ok(()) } pub async fn remote_upsert( &self, ctx: &GitContext, name: String, url: String, ) -> Result<(), GitError> { self.remote_add(ctx, name, url).await } pub async fn remote_list(&self, ctx: &GitContext) -> Result, GitError> { let mut client = ctx.ks.remote(); let response = client .list_remotes(pb::ListRemotesRequest { repository: Some(ctx.repository_ref()), }) .await .map_err(rpc_error)? .into_inner(); let mut rows: Vec = response .remotes .into_iter() .map(|remote| RemoteDto { name: remote.name, fetch_url: Some(remote.fetch_url).filter(|url| !url.is_empty()), push_url: Some(remote.push_url).filter(|url| !url.is_empty()), }) .collect(); rows.sort_by(|a, b| a.name.cmp(&b.name)); Ok(rows) } pub async fn remote_remove(&self, ctx: &GitContext, name: String) -> Result<(), GitError> { validate_remote_name(&name)?; let mut client = ctx.ks.remote(); client .remove_remote(pb::RemoveRemoteRequest { repository: Some(ctx.repository_ref()), name, actor: Some(ctx.actor()), options: Some(ctx.write_options()), }) .await .map_err(rpc_error)?; Ok(()) } }