pub mod list; pub mod r#use; pub mod clear;
pub mod query;
pub mod sync;
pub mod use_branch;
pub mod add;
pub mod remove;
pub mod helpers; pub mod config;
use anyhow::Result;
use clap::{Args, Subcommand};
use std::{path::PathBuf, sync::Arc};
use crate::cli::commands::CliArgs;
use crate::config::AppConfig;
use crate::vectordb::qdrant_client_trait::QdrantClientTrait;
use std::fmt::Debug;
const COLLECTION_NAME_PREFIX: &str = "repo_";
pub(crate) const FIELD_BRANCH: &str = "branch";
pub(crate) const FIELD_COMMIT_HASH: &str = "commit_hash";
#[derive(Args, Debug)]
#[derive(Clone)]
pub struct RepoArgs {
#[command(subcommand)]
pub command: RepoCommand,
}
#[derive(Subcommand, Debug)]
#[derive(Clone)]
pub enum RepoCommand {
Add(add::AddRepoArgs),
List,
Use(r#use::UseRepoArgs),
Remove(remove::RemoveRepoArgs),
Clear(clear::ClearRepoArgs),
UseBranch(use_branch::UseBranchArgs),
Query(query::RepoQueryArgs),
Sync(sync::SyncRepoArgs),
Stats(super::stats::StatsArgs),
Config(config::ConfigArgs),
}
pub async fn handle_repo_command<C>(
args: RepoArgs,
cli_args: &CliArgs,
config: &mut AppConfig,
client: Arc<C>,
override_path: Option<&PathBuf>,
) -> Result<()>
where
C: QdrantClientTrait + Send + Sync + 'static,
{
match args.command {
RepoCommand::Add(add_args) => add::handle_repo_add(add_args, config, Arc::clone(&client), override_path).await?,
RepoCommand::List => list::list_repositories(config)?,
RepoCommand::Use(use_args) => r#use::use_repository(use_args, config, override_path)?,
RepoCommand::Remove(remove_args) => remove::handle_repo_remove(remove_args, config, Arc::clone(&client), override_path).await?,
RepoCommand::Clear(clear_args) => clear::handle_repo_clear(clear_args, config, client, override_path).await?,
RepoCommand::UseBranch(branch_args) => use_branch::handle_use_branch(branch_args, config, override_path).await?,
RepoCommand::Query(query_args) => query::handle_repo_query(query_args, config, Arc::clone(&client), cli_args).await?,
RepoCommand::Sync(sync_args) => sync::handle_repo_sync(sync_args, cli_args, config, Arc::clone(&client), override_path).await?,
RepoCommand::Stats(stats_args) => super::stats::handle_stats(stats_args, config.clone(), Arc::clone(&client)).await?,
RepoCommand::Config(config_args) => config::handle_config(config_args, config, override_path)?,
}
Ok(())
}
#[cfg(test)]
pub fn handle_repo_command_test(config: &AppConfig) -> Result<()> {
list::list_repositories(config)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{AppConfig, RepositoryConfig, load_config, save_config};
use crate::cli::commands::Commands;
use crate::cli::repo_commands::{RepoArgs, RepoCommand};
use crate::cli::repo_commands::remove::RemoveRepoArgs;
use qdrant_client::{Qdrant};
use std::sync::Arc;
use tokio::runtime::Runtime;
use std::collections::HashMap;
use std::path::{PathBuf};
use std::fs;
use tempfile::{tempdir};
use crate::vectordb::qdrant_client_trait::MockQdrantClientTrait;
use mockall::predicate::eq;
fn create_test_config_data() -> AppConfig {
AppConfig {
repositories: vec![
RepositoryConfig { name: "repo1".to_string(), url: "url1".to_string(), local_path: PathBuf::from("/tmp/vectordb_test_repo1"), default_branch: "main".to_string(), tracked_branches: vec!["main".to_string()], active_branch: Some("main".to_string()), remote_name: Some("origin".to_string()), ssh_key_path: None, ssh_key_passphrase: None, last_synced_commits: HashMap::new(), indexed_languages: None },
RepositoryConfig { name: "repo2".to_string(), url: "url2".to_string(), local_path: PathBuf::from("/tmp/vectordb_test_repo2"), default_branch: "dev".to_string(), tracked_branches: vec!["dev".to_string()], active_branch: Some("dev".to_string()), remote_name: Some("origin".to_string()), ssh_key_path: None, ssh_key_passphrase: None, last_synced_commits: HashMap::new(), indexed_languages: None },
],
active_repository: None,
qdrant_url: "http://localhost:6334".to_string(),
onnx_model_path: None,
onnx_tokenizer_path: None,
server_api_key_path: None,
repositories_base_path: None,
}
}
fn create_dummy_cli_args(repo_command: RepoCommand) -> CliArgs {
let dummy_model_path = Some(PathBuf::from("/tmp/dummy_model.onnx"));
let dummy_tokenizer_dir = Some(PathBuf::from("/tmp/dummy_tokenizer/"));
CliArgs {
command: Commands::Repo(RepoArgs { command: repo_command }),
onnx_model_path_arg: dummy_model_path.map(|p| p.to_string_lossy().into_owned()),
onnx_tokenizer_dir_arg: dummy_tokenizer_dir.map(|p| p.to_string_lossy().into_owned()),
}
}
#[test]
fn test_handle_repo_clear_specific_repo() {
let rt = Runtime::new().unwrap();
rt.block_on(async {
let test_repo_name = "my-test-repo-clear-specific".to_string();
let collection_name = helpers::get_collection_name(&test_repo_name);
let mut mock_client = MockQdrantClientTrait::new();
mock_client.expect_collection_exists()
.with(eq(collection_name.clone()))
.times(1)
.returning(|_| Ok(true));
mock_client.expect_delete_points_blocking()
.times(1)
.returning(|_, _| Ok(()));
let client = Arc::new(mock_client);
let mut config = create_test_config_data();
config.repositories.push(RepositoryConfig {
name: test_repo_name.clone(),
url: "url_clear".to_string(),
local_path: PathBuf::from("/tmp/clear_spec"),
default_branch: "main".to_string(),
tracked_branches: vec![],
active_branch: None,
remote_name: None,
ssh_key_path: None,
ssh_key_passphrase: None,
last_synced_commits: HashMap::from([("main".to_string(), "dummy_commit".to_string())]), indexed_languages: Some(vec!["rust".to_string()])
});
config.active_repository = Some("other_repo".to_string());
let args = clear::ClearRepoArgs { name: Some(test_repo_name.to_string()), yes: true };
let dummy_cli_args = create_dummy_cli_args(RepoCommand::Clear(args.clone()));
let result = handle_repo_command(RepoArgs{ command: RepoCommand::Clear(args)}, &dummy_cli_args, &mut config, client, None).await;
assert!(result.is_ok(), "handle_repo_command failed: {:?}", result.err());
let updated_repo = config.repositories.iter().find(|r| r.name == test_repo_name).expect("Test repo config not found after clear");
assert!(updated_repo.last_synced_commits.is_empty(), "Sync status was not cleared");
assert!(updated_repo.indexed_languages.is_none(), "Indexed languages were not cleared");
});
}
#[test]
fn test_handle_repo_clear_active_repo() {
let rt = Runtime::new().unwrap();
rt.block_on(async {
let active_repo_name = "my-test-repo-clear-active".to_string();
let collection_name = helpers::get_collection_name(&active_repo_name);
let mut mock_client = MockQdrantClientTrait::new();
mock_client.expect_collection_exists()
.with(eq(collection_name.clone()))
.times(1)
.returning(|_| Ok(true));
mock_client.expect_delete_points_blocking()
.times(1)
.returning(|_, _| Ok(()));
let client = Arc::new(mock_client);
let mut config = create_test_config_data();
config.repositories.push(RepositoryConfig {
name: active_repo_name.clone(),
url: "url_clear_active".to_string(),
local_path: PathBuf::from("/tmp/clear_active"),
default_branch: "main".to_string(),
tracked_branches: vec![],
active_branch: Some("main".to_string()),
remote_name: None,
ssh_key_path: None,
ssh_key_passphrase: None,
last_synced_commits: HashMap::from([("main".to_string(), "dummy_commit_active".to_string())]), indexed_languages: Some(vec!["python".to_string()])
});
config.active_repository = Some(active_repo_name.to_string());
let args = clear::ClearRepoArgs { name: None, yes: true }; let dummy_cli_args = create_dummy_cli_args(RepoCommand::Clear(args.clone()));
let result = handle_repo_command(RepoArgs{ command: RepoCommand::Clear(args)}, &dummy_cli_args, &mut config, client, None).await;
assert!(result.is_ok(), "handle_repo_command failed: {:?}", result.err());
let updated_repo = config.repositories.iter().find(|r| r.name == active_repo_name).expect("Active repo config not found after clear");
assert!(updated_repo.last_synced_commits.is_empty(), "Sync status was not cleared for active repo");
assert!(updated_repo.indexed_languages.is_none(), "Indexed languages were not cleared for active repo");
});
}
#[test]
fn test_handle_repo_clear_no_active_or_specified_fails() {
let rt = Runtime::new().unwrap();
rt.block_on(async {
let client = Arc::new(Qdrant::from_url("http://localhost:6334").build().unwrap());
let mut config = create_test_config_data();
config.repositories.clear();
config.active_repository = None;
let args = clear::ClearRepoArgs { name: None, yes: true };
let dummy_cli_args = create_dummy_cli_args(RepoCommand::Clear(args.clone()));
let result = handle_repo_command(RepoArgs{ command: RepoCommand::Clear(args)}, &dummy_cli_args, &mut config, client, None).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("No active repository set"));
});
}
#[test]
fn test_handle_repo_use_existing() {
let temp_dir = tempdir().unwrap(); let temp_path = temp_dir.path().join("test_config.toml");
let mut config = create_test_config_data();
config.active_repository = Some("repo1".to_string());
save_config(&config, Some(&temp_path)).unwrap();
let use_args = r#use::UseRepoArgs { name: "repo2".to_string() };
let client = Arc::new(Qdrant::from_url("http://localhost:6334").build().unwrap());
let dummy_cli_args = create_dummy_cli_args(RepoCommand::Use(use_args.clone()));
let result = tokio::runtime::Runtime::new().unwrap().block_on(async {
handle_repo_command(RepoArgs{ command: RepoCommand::Use(use_args)}, &dummy_cli_args, &mut config, client, Some(&temp_path)).await
});
assert!(result.is_ok());
let saved_config = load_config(Some(&temp_path)).unwrap();
assert_eq!(saved_config.active_repository, Some("repo2".to_string()));
}
#[test]
fn test_handle_repo_use_nonexistent() {
let temp_dir = tempdir().unwrap(); let temp_path = temp_dir.path().join("test_config.toml");
let mut config = create_test_config_data();
save_config(&config, Some(&temp_path)).unwrap();
let initial_config_state = config.clone();
let use_args = r#use::UseRepoArgs { name: "repo3".to_string() };
let client = Arc::new(Qdrant::from_url("http://localhost:6334").build().unwrap());
let dummy_cli_args = create_dummy_cli_args(RepoCommand::Use(use_args.clone()));
let result = tokio::runtime::Runtime::new().unwrap().block_on(async {
handle_repo_command(RepoArgs{ command: RepoCommand::Use(use_args)}, &dummy_cli_args, &mut config, client, Some(&temp_path)).await
});
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Repository 'repo3' not found"));
let saved_config = load_config(Some(&temp_path)).unwrap();
assert_eq!(saved_config.repositories, initial_config_state.repositories);
assert_eq!(saved_config.active_repository, initial_config_state.active_repository);
}
#[test]
fn test_handle_repo_remove_config_only_non_active() {
let rt = Runtime::new().unwrap();
rt.block_on(async {
let client = Arc::new(Qdrant::from_url("http://localhost:6334").build().unwrap());
let temp_dir = tempdir().unwrap(); let temp_path = temp_dir.path().join("test_config.toml");
let mut config = create_test_config_data();
config.active_repository = Some("repo1".to_string());
save_config(&config, Some(&temp_path)).unwrap();
let initial_repo_count = config.repositories.len();
let remove_args = RemoveRepoArgs { name: "repo2".to_string(), yes: true };
let dummy_cli_args = create_dummy_cli_args(RepoCommand::Remove(remove_args.clone()));
let _ = fs::remove_dir_all("/tmp/vectordb_test_repo2");
let result = handle_repo_command(RepoArgs{ command: RepoCommand::Remove(remove_args)}, &dummy_cli_args, &mut config, client.clone(), Some(&temp_path)).await;
assert!(result.is_ok());
let saved_config = load_config(Some(&temp_path)).unwrap();
assert_eq!(saved_config.repositories.len(), initial_repo_count - 1);
assert!(!saved_config.repositories.iter().any(|r| r.name == "repo2"));
assert_eq!(saved_config.active_repository, Some("repo1".to_string()));
});
}
#[test]
fn test_handle_repo_remove_config_only_active() {
let rt = Runtime::new().unwrap();
rt.block_on(async {
let client = Arc::new(Qdrant::from_url("http://localhost:6334").build().unwrap());
let temp_dir = tempdir().unwrap(); let temp_path = temp_dir.path().join("test_config.toml");
let mut config = create_test_config_data();
config.active_repository = Some("repo2".to_string());
config.repositories.push(RepositoryConfig { name: "repo3".to_string(), url: "url3".to_string(), local_path: PathBuf::from("/tmp/vectordb_test_repo3"), default_branch: "main".to_string(), tracked_branches: vec!["main".to_string()], active_branch: Some("main".to_string()), remote_name: Some("origin".to_string()), ssh_key_path: None, ssh_key_passphrase: None, last_synced_commits: HashMap::new(), indexed_languages: None });
save_config(&config, Some(&temp_path)).unwrap();
let initial_repo_count = config.repositories.len();
let remove_args = RemoveRepoArgs { name: "repo2".to_string(), yes: true };
let dummy_cli_args = create_dummy_cli_args(RepoCommand::Remove(remove_args.clone()));
let _ = fs::remove_dir_all("/tmp/vectordb_test_repo2");
let result = handle_repo_command(RepoArgs{ command: RepoCommand::Remove(remove_args)}, &dummy_cli_args, &mut config, client.clone(), Some(&temp_path)).await;
assert!(result.is_ok());
let saved_config = load_config(Some(&temp_path)).unwrap();
assert_eq!(saved_config.repositories.len(), initial_repo_count - 1);
assert!(!saved_config.repositories.iter().any(|r| r.name == "repo2"));
assert_eq!(saved_config.active_repository, Some("repo1".to_string()));
});
}
#[test]
fn test_handle_repo_remove_nonexistent() {
let rt = Runtime::new().unwrap();
rt.block_on(async {
let client = Arc::new(Qdrant::from_url("http://localhost:6334").build().unwrap());
let temp_dir = tempdir().unwrap(); let temp_path = temp_dir.path().join("test_config.toml");
let mut config = create_test_config_data();
save_config(&config, Some(&temp_path)).unwrap();
let initial_config_state = config.clone();
let initial_repo_count = config.repositories.len();
let remove_args = RemoveRepoArgs { name: "repo3".to_string(), yes: true };
let dummy_cli_args = create_dummy_cli_args(RepoCommand::Remove(remove_args.clone()));
let result = handle_repo_command(RepoArgs{ command: RepoCommand::Remove(remove_args)}, &dummy_cli_args, &mut config, client.clone(), Some(&temp_path)).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Repository 'repo3' not found"));
let saved_config = load_config(Some(&temp_path)).unwrap();
assert_eq!(saved_config.repositories, initial_config_state.repositories);
assert_eq!(saved_config.repositories.len(), initial_repo_count);
});
}
#[test]
fn test_handle_repo_list() {
let mut config = create_test_config_data();
config.active_repository = Some("repo1".to_string());
let list_args = RepoArgs { command: RepoCommand::List };
let dummy_cli_args = create_dummy_cli_args(RepoCommand::List);
let client = Arc::new(Qdrant::from_url("http://localhost:6334").build().unwrap());
let result = tokio::runtime::Runtime::new().unwrap().block_on(async {
handle_repo_command(list_args, &dummy_cli_args, &mut config, client, None).await });
assert!(result.is_ok());
}
}