use crate::db::Db;
use crate::secrets::SecretsManager;
use anyhow::{Result, anyhow};
use fs_extra::dir::{CopyOptions, copy};
use std::path::Path;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command;
use tracing::{debug, info};
pub struct CloneOptions<'a> {
pub url: &'a str,
pub branch: &'a str,
pub token: Option<&'a str>,
pub username: Option<&'a str>,
pub dir: &'a Path,
}
pub struct CloneProgress {
pub _phase: String,
pub percent: u8,
pub description: String,
}
pub async fn clone(
opts: CloneOptions<'_>,
mut on_progress: impl FnMut(CloneProgress),
) -> Result<()> {
if opts.dir.exists() {
std::fs::remove_dir_all(opts.dir)
.map_err(|e| anyhow!("falha ao limpar diretório de clone: {e}"))?;
}
std::fs::create_dir_all(opts.dir)?;
if let Some(local_path_str) = opts.url.strip_prefix("file://") {
let source_path = Path::new(local_path_str);
if !source_path.exists() {
return Err(anyhow!(
"repositório local não encontrado: {}",
local_path_str
));
}
info!(
url = %redact_url(opts.url),
dir = %opts.dir.display(),
"copiando repositório local"
);
let mut options = CopyOptions::new();
options.overwrite = true; options.content_only = true; copy(source_path, opts.dir, &options)
.map_err(|e| anyhow!("falha ao copiar repositório local: {e}"))?;
info!(
url = %redact_url(opts.url),
dir = %opts.dir.display(),
"repositório local copiado com sucesso"
);
return Ok(());
}
let effective_url = inject_token(opts.url, opts.token, opts.username);
info!(
url = %redact_url(opts.url),
branch = %opts.branch,
dir = %opts.dir.display(),
"git::clone: iniciando"
);
let msg = format!(
"{:?}",
[
"clone",
"--branch",
opts.branch,
"--progress",
"--",
&effective_url,
".",
]
);
info!(args = %msg);
let mut child = Command::new("git")
.args([
"clone",
"--branch",
opts.branch,
"--progress",
"--",
&effective_url,
".",
])
.current_dir(opts.dir)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.env("GIT_TERMINAL_PROMPT", "0")
.kill_on_drop(true)
.spawn()
.map_err(|e| anyhow!("falha ao iniciar git: {e} — git está instalado?"))?;
let stderr = child.stderr.take().expect("stderr foi capturado acima");
let mut reader = BufReader::new(stderr);
let mut line_buf = String::new();
let mut tail: Vec<String> = Vec::new();
loop {
line_buf.clear();
match reader.read_line(&mut line_buf).await {
Ok(0) | Err(_) => break,
Ok(_) => {}
}
for part in line_buf.split(['\r', '\n']) {
let part = part.trim();
if part.is_empty() {
continue;
}
debug!(line = %part, "git::clone: stderr");
on_progress(parse_progress(part));
if tail.len() >= 10 {
tail.remove(0);
}
tail.push(part.to_string());
}
}
let status = child
.wait()
.await
.map_err(|e| anyhow!("falha ao aguardar processo git: {e}"))?;
if !status.success() {
let detail = tail.join(" | ");
return Err(anyhow!(
"git clone falhou (exit {}): {}",
status.code().unwrap_or(-1),
detail
));
}
info!(
url = %redact_url(opts.url),
branch = %opts.branch,
"git::clone: concluído"
);
Ok(())
}
pub async fn resolve_clone_credentials(
db: &Db,
secrets: &SecretsManager,
provider_id: Option<&str>,
credentials: Option<&str>,
manual_username: Option<&str>,
project_id: &str,
) -> (Option<String>, Option<String>) {
let mut provider_login: Option<String> = None;
let token = if let Some(pid) = provider_id {
match crate::db::git_providers::get(db, pid).await {
Ok(Some(p)) => {
provider_login = p.account_login.clone();
crate::git_providers::usable_token(secrets, &p).ok()
}
_ => None,
}
} else if let Some(name) = credentials {
secrets.get_raw(project_id, name).await.ok()
} else {
None
};
let username = provider_login.or_else(|| manual_username.map(String::from));
(token, username)
}
fn inject_token(url: &str, token: Option<&str>, username: Option<&str>) -> String {
let Some(tok) = token else {
return url.to_string();
};
if let Some(rest) = url.strip_prefix("https://") {
let host_path = rest.split_once('@').map_or(rest, |(_, hp)| hp);
let user = username.unwrap_or("x-token-auth");
return format!("https://{user}:{tok}@{host_path}");
}
url.to_string()
}
fn redact_url(url: &str) -> String {
if let Some(rest) = url.strip_prefix("https://") {
if let Some((creds, host_path)) = rest.split_once('@') {
let user = creds.split(':').next().unwrap_or("user");
return format!("https://{user}:***@{host_path}");
}
}
url.to_string()
}
fn parse_progress(line: &str) -> CloneProgress {
let percent = extract_percent(line);
let phase = if line.contains("Counting") {
"Counting objects"
} else if line.contains("Compressing") {
"Compressing objects"
} else if line.contains("Receiving") {
"Receiving objects"
} else if line.contains("Resolving") {
"Resolving deltas"
} else {
"Cloning"
};
CloneProgress {
_phase: phase.to_string(),
percent,
description: line.to_string(),
}
}
fn extract_percent(line: &str) -> u8 {
let Some(pct_pos) = line.find('%') else {
return 0;
};
let before = &line[..pct_pos];
let num_start = before
.rfind(|c: char| !c.is_ascii_digit() && c != ' ')
.map_or(0, |i| i + 1);
before[num_start..].trim().parse().unwrap_or(0)
}