use crate::db::traits::StoreChunks;
use crate::db::traits::StoreCore;
use crate::db::SqliteStore;
use crate::incremental::ignore::{load_ignore_patterns, IgnorePatternMatcher};
use anyhow::{Context, Result};
use std::path::PathBuf;
use tracing::{info, warn};
pub async fn clean_ignored(
store: &SqliteStore,
repo_name: &str,
worktree_name: &str,
dry_run: bool,
) -> Result<()> {
let repo = store
.get_repo_by_name(repo_name)
.await
.context("Failed to get repository")?
.ok_or_else(|| anyhow::anyhow!("Repository '{}' not found", repo_name))?;
let worktree = store
.get_worktree_by_name(repo.id, worktree_name)
.await
.context("Failed to get worktree")?
.ok_or_else(|| anyhow::anyhow!("Worktree '{}' not found", worktree_name))?;
let root = PathBuf::from(&worktree.abs_path);
let patterns = match load_ignore_patterns(&root) {
Ok(p) => p,
Err(e) => {
warn!("Failed to load ignore patterns: {}", e);
info!("No patterns loaded, nothing to clean");
return Ok(());
}
};
if patterns.is_empty() {
info!("No patterns in .maproomignore, nothing to clean");
return Ok(());
}
info!("Loaded {} ignore patterns", patterns.len());
let matcher = IgnorePatternMatcher::with_patterns(&patterns)
.context("Failed to compile ignore patterns")?;
let chunks = store
.get_chunks_for_worktree(worktree.id)
.await
.context("Failed to get chunks for worktree")?;
info!("Found {} total chunks in worktree", chunks.len());
let mut to_delete = Vec::new();
for (chunk_id, file_relpath) in chunks {
let relpath = PathBuf::from(&file_relpath);
if matcher.should_ignore(&relpath) {
to_delete.push((chunk_id, file_relpath));
}
}
info!("Found {} chunks matching ignore patterns", to_delete.len());
if dry_run {
println!("🔍 Dry run mode - showing what would be deleted:");
println!(" Repository: {}", repo_name);
println!(" Worktree: {}", worktree_name);
println!(" Chunks to delete: {}", to_delete.len());
println!();
for (chunk_id, relpath) in &to_delete {
println!(" Would delete chunk #{}: {}", chunk_id, relpath);
}
println!();
println!("⚠️ Run without --dry-run to actually delete these chunks");
} else {
if to_delete.is_empty() {
println!("✅ No chunks match ignore patterns - nothing to delete");
return Ok(());
}
let chunk_ids: Vec<i64> = to_delete.iter().map(|(id, _)| *id).collect();
let count = store
.delete_chunks_by_ids(worktree.id, &chunk_ids)
.await
.context("Failed to delete chunks")?;
println!(
"✅ Deleted {} chunks matching .maproomignore patterns",
count
);
println!(" Repository: {}", repo_name);
println!(" Worktree: {}", worktree_name);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db;
use std::io::Write;
use tempfile::TempDir;
async fn setup_test_repo(
ignore_content: &str,
) -> Result<(TempDir, SqliteStore, String, String)> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
let ignore_path = repo_path.join(".maproomignore");
let mut file = std::fs::File::create(&ignore_path)?;
file.write_all(ignore_content.as_bytes())?;
file.flush()?;
let store = db::SqliteStore::connect(":memory:").await?;
let repo_name = "test-repo".to_string();
let worktree_name = "main".to_string();
let repo_id = store
.get_or_create_repo(&repo_name, repo_path.to_str().unwrap())
.await?;
let _worktree_id = store
.get_or_create_worktree(repo_id, &worktree_name, repo_path.to_str().unwrap())
.await?;
Ok((temp_dir, store, repo_name, worktree_name))
}
#[tokio::test]
async fn test_clean_ignored_missing_file() {
let temp_dir = TempDir::new().unwrap();
let repo_path = temp_dir.path();
let store = db::SqliteStore::connect(":memory:").await.unwrap();
let repo_name = "test-repo";
let worktree_name = "main";
let repo_id = store
.get_or_create_repo(repo_name, repo_path.to_str().unwrap())
.await
.unwrap();
let _worktree_id = store
.get_or_create_worktree(repo_id, worktree_name, repo_path.to_str().unwrap())
.await
.unwrap();
let result = clean_ignored(&store, repo_name, worktree_name, false).await;
assert!(result.is_ok(), "Should succeed with missing .maproomignore");
}
#[tokio::test]
async fn test_clean_ignored_empty_file() {
let (_temp_dir, store, repo_name, worktree_name) = setup_test_repo("").await.unwrap();
let result = clean_ignored(&store, &repo_name, &worktree_name, false).await;
assert!(result.is_ok(), "Should succeed with empty .maproomignore");
}
#[tokio::test]
async fn test_clean_ignored_dry_run() {
let (_temp_dir, store, repo_name, worktree_name) =
setup_test_repo("test/**\n*.log\n").await.unwrap();
let result = clean_ignored(&store, &repo_name, &worktree_name, true).await;
assert!(result.is_ok(), "Dry run should succeed");
}
#[tokio::test]
async fn test_clean_ignored_invalid_repo() {
let store = db::SqliteStore::connect(":memory:").await.unwrap();
let result = clean_ignored(&store, "nonexistent-repo", "main", false).await;
assert!(result.is_err(), "Should fail with non-existent repo");
assert!(
result.unwrap_err().to_string().contains("not found"),
"Error should mention repository not found"
);
}
#[tokio::test]
async fn test_clean_ignored_invalid_worktree() {
let temp_dir = TempDir::new().unwrap();
let repo_path = temp_dir.path();
let store = db::SqliteStore::connect(":memory:").await.unwrap();
let repo_name = "test-repo";
let _repo_id = store
.get_or_create_repo(repo_name, repo_path.to_str().unwrap())
.await
.unwrap();
let result = clean_ignored(&store, repo_name, "nonexistent-worktree", false).await;
assert!(result.is_err(), "Should fail with non-existent worktree");
assert!(
result.unwrap_err().to_string().contains("not found"),
"Error should mention worktree not found"
);
}
}