use serde::{Deserialize, Serialize}; use crate::service::GitService; use crate::service::clone::{GitCredential, mirror_fetch}; use crate::service::gitcmd::{run_git, run_git_soft}; use crate::share::errors::{GitError, map_baby}; 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, } impl GitService { pub async fn remote_clone_import( &self, space_name: String, repo_name: String, url: String, auth: Option, ) -> Result { validate_remote_url(&url)?; let ctx = self.context(space_name, repo_name).await?; let dir = ctx.repo_dir(); let timeout = self.state.config.git.git_command_timeout_secs; let credential = auth .as_ref() .and_then(|a| a.token.clone()) .filter(|t| !t.is_empty()) .map(|token| GitCredential { username: auth .as_ref() .and_then(|a| a.username.clone()) .map(|u| u.trim().to_string()) .filter(|u| !u.is_empty()) .unwrap_or_else(|| "git".to_string()), secret: token, }); mirror_fetch("remotes::import", &dir, &url, credential.as_ref(), timeout).await?; let baby = self.baby(&ctx); let default = baby.get_default_branch().await.map_err(map_baby)?; let commit_count = count_refs(&dir).await?; Ok(ImportResultDto { default_branch: default, commit_count, }) } pub async fn remote_fetch( &self, space_name: String, repo_name: String, remote: String, auth: Option, ) -> Result { let ctx = self.context(space_name, repo_name).await?; validate_remote_name(&remote)?; let mut env: Vec<(String, String)> = Vec::new(); if let Some(a) = &auth && let Some(t) = &a.token { env.push(("GIT_ASKPASS".to_string(), "/bin/true".to_string())); env.push(("GIT_TOKEN".to_string(), t.clone())); } let timeout = self.state.config.git.git_command_timeout_secs; let out = run_git( "remotes::fetch", &ctx.repo_dir(), vec![ "fetch".to_string(), "--prune".to_string(), "--".to_string(), remote, ], &env, timeout, ) .await?; let updated: Vec = String::from_utf8_lossy(&out.stdout) .lines() .filter_map(|l| l.split_whitespace().nth(1).map(|s| s.to_string())) .collect(); Ok(FetchResultDto { updated, new: Vec::new(), }) } pub async fn remote_push( &self, space_name: String, repo_name: String, remote: String, refspec: Option, auth: Option, ) -> Result { let ctx = self.context(space_name, repo_name).await?; validate_remote_name(&remote)?; let mut env: Vec<(String, String)> = Vec::new(); if let Some(a) = &auth && let Some(t) = &a.token { env.push(("GIT_ASKPASS".to_string(), "/bin/true".to_string())); env.push(("GIT_TOKEN".to_string(), t.clone())); } let mut args = vec!["push".to_string(), "--".to_string(), remote]; if let Some(r) = refspec { validate_refspec(&r)?; args.push(r); } let timeout = self.state.config.git.git_command_timeout_secs; let out = run_git_soft("remotes::push", &ctx.repo_dir(), args, &env, timeout).await?; let stdout = String::from_utf8_lossy(&out.stdout); let pushed: Vec = stdout .lines() .filter_map(|l| { l.strip_prefix(" ") .map(|s| s.to_string()) .or(Some(l.to_string())) }) .filter(|l| l.contains("->")) .collect(); let rejected: Vec = stdout .lines() .filter(|l| l.starts_with("!")) .map(|l| l.to_string()) .collect(); if !out.status.success() && pushed.is_empty() { return Err(GitError::Internal(format!( "push failed: {}", String::from_utf8_lossy(&out.stderr).trim() ))); } Ok(PushResultDto { pushed, rejected }) } pub async fn remote_add( &self, space_name: String, repo_name: String, name: String, url: String, ) -> Result<(), GitError> { validate_remote_name(&name)?; validate_remote_url(&url)?; let ctx = self.context(space_name, repo_name).await?; self.baby(&ctx) .add_remote(&name, &url, gitbaby::remote::types::RemoteOption::default()) .await .map_err(map_baby) } pub async fn remote_upsert( &self, space_name: String, repo_name: String, name: String, url: String, ) -> Result<(), GitError> { validate_remote_name(&name)?; validate_remote_url(&url)?; let ctx = self.context(space_name, repo_name).await?; let baby = self.baby(&ctx); if baby.get_remote_address(&name).await.map_err(map_baby)?.is_some() { run_git_soft( "remotes::set_url", &ctx.repo_root, vec!["remote".to_string(), "set-url".to_string(), name, url], &[], 30, ) .await?; Ok(()) } else { baby.add_remote(&name, &url, gitbaby::remote::types::RemoteOption::default()) .await .map_err(map_baby) } } pub async fn remote_list( &self, space_name: String, repo_name: String, ) -> Result, GitError> { let ctx = self.context(space_name, repo_name).await?; let out = run_git_soft( "remotes::list", &ctx.repo_root, vec!["remote".to_string(), "-v".to_string()], &[], 30, ) .await?; let text = String::from_utf8_lossy(&out.stdout); let mut map: std::collections::HashMap = std::collections::HashMap::new(); for line in text.lines() { let mut parts = line.split_whitespace(); let (Some(name), Some(url), Some(kind)) = (parts.next(), parts.next(), parts.next()) else { continue; }; let entry = map .entry(name.to_string()) .or_insert_with(|| RemoteDto { name: name.to_string(), fetch_url: None, push_url: None, }); if kind.starts_with("(fetch") { entry.fetch_url = Some(url.to_string()); } else if kind.starts_with("(push") { entry.push_url = Some(url.to_string()); } } let mut rows: Vec = map.into_values().collect(); rows.sort_by(|a, b| a.name.cmp(&b.name)); Ok(rows) } pub async fn remote_remove( &self, space_name: String, repo_name: String, name: String, ) -> Result<(), GitError> { validate_remote_name(&name)?; let ctx = self.context(space_name, repo_name).await?; self.baby(&ctx).remove_remote(&name).await.map_err(map_baby) } } #[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)] pub struct RemoteDto { pub name: String, pub fetch_url: Option, pub push_url: Option, } async fn count_refs(dir: &std::path::Path) -> Result { let timeout = 60; let out = run_git_soft( "remotes::count_refs", dir, vec![ "for-each-ref".to_string(), "--count".to_string(), "--format=%(objectname)".to_string(), ], &[], timeout, ) .await?; if out.status.success() { Ok(String::from_utf8_lossy(&out.stdout).lines().count() as i64) } else { Ok(0) } }