use anyhow::{Context, Result};
use colored::Colorize;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use toml_edit::{value, DocumentMut, Item, Table};
use crate::workspace::WorkspaceScanner;
#[derive(Debug, Clone)]
pub struct GitDependency {
pub name: String,
pub git_url: String,
pub branch_or_tag: Option<String>,
pub local_path: PathBuf,
}
pub struct PatchManager {
workspace_root: PathBuf,
}
impl PatchManager {
pub fn new(workspace_root: impl AsRef<Path>) -> Self {
Self {
workspace_root: workspace_root.as_ref().to_path_buf(),
}
}
pub fn discover_patchable_dependencies(&self) -> Result<Vec<GitDependency>> {
let scanner = WorkspaceScanner::new(&self.workspace_root);
let manifests = scanner.find_manifests()?;
let mut git_deps: HashMap<String, GitDependency> = HashMap::new();
let mut available_repos: HashSet<String> = HashSet::new();
for manifest in &manifests {
if manifest.package_name.starts_with("embeddenator") {
available_repos.insert(manifest.package_name.clone());
}
}
for manifest in &manifests {
let content = std::fs::read_to_string(&manifest.path)?;
let doc: DocumentMut = content.parse()?;
for section in &["dependencies", "dev-dependencies", "build-dependencies"] {
if let Some(Item::Table(deps_table)) = doc.get(section) {
for (name, dep_item) in deps_table.iter() {
if let Some(git_dep) = Self::parse_git_dependency(name, dep_item) {
if available_repos.contains(name) {
if let Some(local_path) = self.find_local_repo_path(name) {
git_deps.insert(
name.to_string(),
GitDependency {
name: name.to_string(),
git_url: git_dep.0,
branch_or_tag: git_dep.1,
local_path,
},
);
}
}
}
}
}
}
}
let mut deps: Vec<GitDependency> = git_deps.into_values().collect();
deps.sort_by(|a, b| a.name.cmp(&b.name));
Ok(deps)
}
fn parse_git_dependency(_name: &str, item: &Item) -> Option<(String, Option<String>)> {
let git_url = item.get("git")?.as_str()?.to_string();
let branch_or_tag = item
.get("branch")
.or_else(|| item.get("tag"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
Some((git_url, branch_or_tag))
}
fn find_local_repo_path(&self, repo_name: &str) -> Option<PathBuf> {
let expected_path = self.workspace_root.join(repo_name);
if expected_path.join("Cargo.toml").exists() {
Some(expected_path)
} else {
None
}
}
pub fn apply_patches(&self, deps: &[GitDependency], verify: bool) -> Result<PatchReport> {
let cargo_dir = self.workspace_root.join(".cargo");
let config_path = cargo_dir.join("config.toml");
if !cargo_dir.exists() {
std::fs::create_dir(&cargo_dir).context("Failed to create .cargo directory")?;
}
let mut doc: DocumentMut = if config_path.exists() {
let content = std::fs::read_to_string(&config_path)?;
content.parse()?
} else {
DocumentMut::new()
};
let mut patched_count = 0;
let mut patches_by_url: HashMap<String, Vec<&GitDependency>> = HashMap::new();
for dep in deps {
patches_by_url
.entry(dep.git_url.clone())
.or_default()
.push(dep);
}
for (git_url, deps_for_url) in patches_by_url {
let patch_key = format!("patch.\"{}\"", git_url);
if doc.get(&patch_key).is_none() {
doc[&patch_key] = Item::Table(Table::new());
}
if let Some(Item::Table(patch_table)) = doc.get_mut(&patch_key) {
for dep in deps_for_url {
let mut dep_table = Table::new();
dep_table.insert("path", value(dep.local_path.to_string_lossy().to_string()));
patch_table.insert(&dep.name, Item::Table(dep_table));
patched_count += 1;
}
}
}
std::fs::write(&config_path, doc.to_string())
.context("Failed to write .cargo/config.toml")?;
let mut report = PatchReport {
patched_count,
config_path: config_path.clone(),
verified: false,
verification_error: None,
};
if verify {
match self.verify_patches() {
Ok(_) => report.verified = true,
Err(e) => report.verification_error = Some(e.to_string()),
}
}
Ok(report)
}
pub fn remove_patches(&self) -> Result<ResetReport> {
let cargo_dir = self.workspace_root.join(".cargo");
let config_path = cargo_dir.join("config.toml");
if !config_path.exists() {
return Ok(ResetReport {
removed_count: 0,
config_path,
config_deleted: false,
});
}
let content = std::fs::read_to_string(&config_path)?;
let mut doc: DocumentMut = content.parse()?;
let mut removed_count = 0;
let mut keys_to_remove = Vec::new();
for (key, _) in doc.as_table().iter() {
if key == "patch" {
if let Some(Item::Table(patch_table)) = doc.get("patch") {
for (_source_url, dep_item) in patch_table.iter() {
if let Item::Table(deps) = dep_item {
removed_count += deps.len();
}
}
}
keys_to_remove.push(key.to_string());
} else if key.starts_with("patch.") {
if let Some(Item::Table(patch_deps)) = doc.get(key) {
removed_count += patch_deps.len();
}
keys_to_remove.push(key.to_string());
}
}
for key in keys_to_remove {
doc.remove(&key);
}
let is_empty = doc.as_table().is_empty();
if is_empty {
std::fs::remove_file(&config_path)?;
Ok(ResetReport {
removed_count,
config_path,
config_deleted: true,
})
} else {
std::fs::write(&config_path, doc.to_string())?;
Ok(ResetReport {
removed_count,
config_path,
config_deleted: false,
})
}
}
fn verify_patches(&self) -> Result<()> {
use std::process::Command;
let output = Command::new("cargo")
.arg("metadata")
.arg("--format-version=1")
.current_dir(&self.workspace_root)
.output()
.context("Failed to run cargo metadata")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("cargo metadata failed:\n{}", stderr);
}
Ok(())
}
pub fn clean_cache(&self) -> Result<()> {
use std::process::Command;
println!("{}", " Cleaning cargo cache...".dimmed());
let output = Command::new("cargo")
.arg("clean")
.current_dir(&self.workspace_root)
.output()
.context("Failed to run cargo clean")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("cargo clean failed:\n{}", stderr);
}
Ok(())
}
}
#[derive(Debug)]
pub struct PatchReport {
pub patched_count: usize,
pub config_path: PathBuf,
pub verified: bool,
pub verification_error: Option<String>,
}
#[derive(Debug)]
pub struct ResetReport {
pub removed_count: usize,
pub config_path: PathBuf,
pub config_deleted: bool,
}
impl PatchReport {
pub fn print(&self) {
println!(
"\n{} {} patches written to {}",
"✓".green().bold(),
self.patched_count,
self.config_path.display().to_string().bright_white()
);
if self.verified {
println!("{} Patches verified successfully", "✓".green().bold());
} else if let Some(err) = &self.verification_error {
println!("{} Verification failed: {}", "✗".red().bold(), err);
println!(
"\n{} Run 'cargo build' to diagnose the issue",
"Suggestion:".cyan().bold()
);
}
}
}
impl ResetReport {
pub fn print(&self) {
if self.removed_count == 0 {
println!("{} No patches found to remove", "Info:".blue().bold());
} else {
println!(
"\n{} {} patches removed",
"✓".green().bold(),
self.removed_count
);
if self.config_deleted {
println!(
" {} deleted (empty)",
self.config_path.display().to_string().dimmed()
);
} else {
println!(
" {} updated",
self.config_path.display().to_string().dimmed()
);
}
}
}
}
#[cfg(test)]
#[path = "patch_tests.rs"]
mod tests;