563bb99
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<String>,
pub new: Vec<String>,
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct PushResultDto {
pub pushed: Vec<String>,
pub rejected: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct RemoteAuthDto {
#[serde(default)]
pub token: Option<String>,
#[serde(default)]
pub username: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RemoteConfigDto {
pub url: String,
#[serde(default)]
pub auth: Option<RemoteAuthDto>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub refspec: Option<String>,
}
impl GitService {
pub async fn remote_clone_import(
&self,
space_name: String,
repo_name: String,
url: String,
auth: Option<RemoteAuthDto>,
) -> Result<ImportResultDto, GitError> {
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<RemoteAuthDto>,
) -> Result<FetchResultDto, GitError> {
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> = 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<String>,
auth: Option<RemoteAuthDto>,
) -> Result<PushResultDto, GitError> {
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<String> = 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<String> = 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<Vec<RemoteDto>, 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<String, RemoteDto> =
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<RemoteDto> = 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<String>,
pub push_url: Option<String>,
}
async fn count_refs(dir: &std::path::Path) -> Result<i64, GitError> {
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)
}
}