use serde::Deserialize;
use std::path::Path;
use std::time::Duration;
use tokio::process::Command;
use super::PackageInfo;
const CARGO_OPERATION_TIMEOUT_SECS: u64 = 600;
#[derive(Deserialize)]
struct CargoToml {
package: CargoPackage,
}
#[derive(Deserialize)]
struct CargoPackage {
name: String,
version: String,
}
pub async fn get_package_info(repo_path: &Path) -> Option<PackageInfo> {
let cargo_toml_path = repo_path.join("Cargo.toml");
let content = tokio::fs::read_to_string(&cargo_toml_path).await.ok()?;
let cargo: CargoToml = toml::from_str(&content).ok()?;
Some(PackageInfo {
manager: super::PackageManager::Cargo,
name: cargo.package.name,
version: cargo.package.version,
})
}
pub async fn publish(repo_path: &Path, dry_run: bool) -> (bool, String) {
let mut args = vec!["publish"];
if dry_run {
args.push("--dry-run");
}
let timeout_duration = Duration::from_secs(CARGO_OPERATION_TIMEOUT_SECS);
let result = tokio::time::timeout(
timeout_duration,
Command::new("cargo")
.args(&args)
.current_dir(repo_path)
.output(),
)
.await;
match result {
Ok(Ok(output)) => {
if output.status.success() {
if dry_run {
(true, "dry-run ok".to_string())
} else {
(true, "published".to_string())
}
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
let error_message = clean_cargo_error(&stderr);
if stderr.contains("already uploaded") || stderr.contains("crate version") && stderr.contains("already exists") {
(true, "already published".to_string())
} else {
(false, error_message)
}
}
}
Ok(Err(e)) => (false, format!("cargo command failed: {}", e)),
Err(_) => (false, "cargo operation timed out".to_string()),
}
}
fn clean_cargo_error(error: &str) -> String {
if error.contains("already uploaded") || error.contains("already exists") {
"already published".to_string()
} else if error.contains("no token found") || error.contains("not logged in") {
"not authenticated (run: cargo login)".to_string()
} else if error.contains("forbidden") || error.contains("403") {
"permission denied (check crates.io permissions)".to_string()
} else if error.contains("Caused by:") {
error
.lines()
.skip_while(|line| !line.contains("Caused by:"))
.nth(1)
.map(|line| line.trim().to_string())
.unwrap_or_else(|| error.trim().to_string())
} else {
error
.lines()
.find(|line| {
!line.trim().is_empty()
&& !line.contains("Uploading")
&& !line.contains("Packaging")
})
.map(|line| line.trim().to_string())
.unwrap_or_else(|| error.trim().to_string())
}
}