use crate::lockfile::{LockFile, LockedResource};
use crate::utils::fs::atomic_write;
use crate::utils::normalize_path_for_storage;
use anyhow::{Context, Result};
use std::collections::HashSet;
use std::fs;
use std::path::Path;
use std::sync::Arc;
use tokio::sync::Mutex;
pub async fn add_path_to_gitignore(
project_dir: &Path,
path: &str,
lock: &Arc<Mutex<()>>,
) -> Result<()> {
let _guard = lock.lock().await;
let gitignore_path = project_dir.join(".gitignore");
let mut before_agpm = Vec::new();
let mut agpm_paths = std::collections::HashSet::new();
let mut after_agpm = Vec::new();
if gitignore_path.exists() {
let content = tokio::fs::read_to_string(&gitignore_path)
.await
.with_context(|| format!("Failed to read {}", gitignore_path.display()))?;
let mut in_agpm_section = false;
let mut past_agpm_section = false;
for line in content.lines() {
if line == "# AGPM managed entries - do not edit below this line"
|| line == "# CCPM managed entries - do not edit below this line"
{
in_agpm_section = true;
} else if line == "# End of AGPM managed entries"
|| line == "# End of CCPM managed entries"
{
in_agpm_section = false;
past_agpm_section = true;
} else if in_agpm_section {
if !line.is_empty() && !line.starts_with('#') {
agpm_paths.insert(line.to_string());
}
} else if !past_agpm_section {
before_agpm.push(line.to_string());
} else {
after_agpm.push(line.to_string());
}
}
}
let normalized_path = normalize_path_for_storage(path);
if agpm_paths.contains(&normalized_path) {
return Ok(());
}
agpm_paths.insert(normalized_path);
agpm_paths.insert("agpm.private.toml".to_string());
agpm_paths.insert("agpm.private.lock".to_string());
let mut new_content = String::new();
if before_agpm.is_empty() && after_agpm.is_empty() {
new_content.push_str("# .gitignore - AGPM managed entries\n");
new_content.push_str("# AGPM entries are automatically generated\n");
new_content.push('\n');
} else {
for line in &before_agpm {
new_content.push_str(line);
new_content.push('\n');
}
if !before_agpm.is_empty() && !before_agpm.last().unwrap().trim().is_empty() {
new_content.push('\n');
}
}
new_content.push_str("# AGPM managed entries - do not edit below this line\n");
let mut sorted_paths: Vec<_> = agpm_paths.into_iter().collect();
sorted_paths.sort();
for p in sorted_paths {
new_content.push_str(&p);
new_content.push('\n');
}
new_content.push_str("# End of AGPM managed entries\n");
if !after_agpm.is_empty() {
new_content.push('\n');
for line in &after_agpm {
new_content.push_str(line);
new_content.push('\n');
}
}
atomic_write(&gitignore_path, new_content.as_bytes())
.with_context(|| format!("Failed to update {}", gitignore_path.display()))?;
Ok(())
}
pub fn update_gitignore(lockfile: &LockFile, project_dir: &Path, enabled: bool) -> Result<()> {
if !enabled {
return Ok(());
}
let gitignore_path = project_dir.join(".gitignore");
let mut paths_to_ignore = HashSet::new();
let mut add_resource_paths = |resources: &[LockedResource]| {
for resource in resources {
if resource.install == Some(false) {
continue;
}
if !resource.installed_at.is_empty() {
paths_to_ignore.insert(resource.installed_at.clone());
}
}
};
add_resource_paths(&lockfile.agents);
add_resource_paths(&lockfile.snippets);
add_resource_paths(&lockfile.commands);
add_resource_paths(&lockfile.scripts);
let mut before_agpm_section = Vec::new();
let mut after_agpm_section = Vec::new();
if gitignore_path.exists() {
let content = fs::read_to_string(&gitignore_path)
.with_context(|| format!("Failed to read {}", gitignore_path.display()))?;
let mut in_agpm_section = false;
let mut past_agpm_section = false;
for line in content.lines() {
if line == "# AGPM managed entries - do not edit below this line"
|| line == "# CCPM managed entries - do not edit below this line"
{
in_agpm_section = true;
continue;
} else if line == "# End of AGPM managed entries"
|| line == "# End of CCPM managed entries"
{
in_agpm_section = false;
past_agpm_section = true;
continue;
}
if !in_agpm_section && !past_agpm_section {
before_agpm_section.push(line.to_string());
} else if in_agpm_section {
} else {
after_agpm_section.push(line.to_string());
}
}
}
let mut new_content = String::new();
if !before_agpm_section.is_empty() {
for line in &before_agpm_section {
new_content.push_str(line);
new_content.push('\n');
}
if !before_agpm_section.is_empty() && !before_agpm_section.last().unwrap().trim().is_empty()
{
new_content.push('\n');
}
}
new_content.push_str("# AGPM managed entries - do not edit below this line\n");
let mut sorted_paths: Vec<_> = paths_to_ignore.into_iter().collect();
sorted_paths.sort();
for path in &sorted_paths {
let ignore_path = if path.starts_with("./") {
path.strip_prefix("./").unwrap_or(path).to_string()
} else {
path.clone()
};
let normalized_path = normalize_path_for_storage(&ignore_path);
new_content.push_str(&normalized_path);
new_content.push('\n');
}
new_content.push_str("# End of AGPM managed entries\n");
if !after_agpm_section.is_empty() {
new_content.push('\n');
for line in &after_agpm_section {
new_content.push_str(line);
new_content.push('\n');
}
}
if before_agpm_section.is_empty() && after_agpm_section.is_empty() {
let mut default_content = String::new();
default_content.push_str("# .gitignore - AGPM managed entries\n");
default_content.push_str("# AGPM entries are automatically generated\n");
default_content.push('\n');
default_content.push_str("# AGPM managed entries - do not edit below this line\n");
for path in &sorted_paths {
let ignore_path = if path.starts_with("./") {
path.strip_prefix("./").unwrap_or(path).to_string()
} else {
path.clone()
};
let normalized_path = ignore_path.replace('\\', "/");
default_content.push_str(&normalized_path);
default_content.push('\n');
}
default_content.push_str("# End of AGPM managed entries\n");
new_content = default_content;
}
atomic_write(&gitignore_path, new_content.as_bytes())
.with_context(|| format!("Failed to update {}", gitignore_path.display()))?;
Ok(())
}