diff --git a/src/cli.rs b/src/cli.rs index 1ea9b0b..8a2737b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -68,5 +68,13 @@ pub enum Commands { #[arg(short, long, group = "output")] #[arg(default_value_t = false)] remotes: bool - } + }, + /// Change directory to a tracked repository + Cd { + /// Name of the repository to change to + #[arg(required = true)] + repo_name: String, + }, + /// Enable CD functionality by adding an alias to your shell configuration + EnableCd } diff --git a/src/core/api.rs b/src/core/api.rs index 9c253ee..f89de89 100644 --- a/src/core/api.rs +++ b/src/core/api.rs @@ -17,6 +17,7 @@ use crate::utils::{ use std::fs::{self, OpenOptions}; use std::io::Write as _; use std::path::Path; +use std::env; /// Scans only specified directories pub fn scan_dirs(mut dirs: Vec, tracking_file: &TrackingFile, scan_hidden: bool) -> Result<(), String> { @@ -219,3 +220,59 @@ pub async fn check_all(tracking_file: &TrackingFile, flags: &[bool]) -> Result<( Ok(()) } + +pub fn cd_to_repo(repo_name: &str, tracking_file: &TrackingFile) -> Result { + if tracking_file.contents.is_empty() { + return Err(String::from("No repository is being tracked")); + } + + tracking_file.contents + .lines() + .find(|line| Path::new(line).file_name().and_then(|name| name.to_str()) == Some(repo_name)) + .map(String::from) + .ok_or_else(|| format!("Repository '{}' not found in tracking file", repo_name)) +} + +/// Enables the CD functionality by adding an alias or function to the user's shell configuration +pub fn enable_cd() -> Result<(), String> { + let home_dir = env::var("HOME").map_err(|_| "Failed to get home directory")?; + let shell = env::var("SHELL").map_err(|_| "Failed to get current shell")?; + + let (config_file, function_content) = if shell.ends_with("bash") || shell.ends_with("zsh") { + ( + format!("{}/.{}rc", home_dir, shell.split('/').last().unwrap()), + r#" +alias gls='git conform ls' +alias gcd='git_conform_cd' + +git_conform_cd() { + if [ -z "$1" ]; then + echo "Usage: git_conform_cd " + return 1 + fi + + local repo_path + repo_path=$(git-conform cd "$1") + + if [ $? -ne 0 ]; then + echo "$repo_path" # This will be the error message + return 1 + fi + + cd "$repo_path" || return 1 +} +"# + ) + } else { + return Err(format!("Unsupported shell: {}", shell)); + }; + + fs::OpenOptions::new() + .append(true) + .open(&config_file) + .and_then(|mut file| file.write_all(function_content.as_bytes())) + .map_err(|e| format!("Failed to update shell config: {}", e))?; + + println!("CD functionality enabled. Please restart your shell or run 'source {}' to apply changes.", config_file); + Ok(()) +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 7c1e77d..7574054 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,8 @@ use crate::core::api::{ scan_all, list, add, + cd_to_repo, + enable_cd, remove_repos, remove_all, check_repos, @@ -128,6 +130,17 @@ async fn main() { else if let Err(e) = check_repos(repos.to_owned(), &[*status, *remotes]).await { handle_error(&e, 6); } - } + }, + Commands::Cd { repo_name } => { + match cd_to_repo(&repo_name, &tracking_file) { + Ok(path) => println!("{}", path), + Err(e) => eprintln!("Error: {}", e), + } + }, + Commands::EnableCd => { + if let Err(e) = enable_cd() { + eprintln!("Error enabling CD functionality: {}", e); + } + }, }; } diff --git a/tests/cd.rs b/tests/cd.rs new file mode 100644 index 0000000..c55a8a5 --- /dev/null +++ b/tests/cd.rs @@ -0,0 +1,133 @@ +mod common; + +use git_conform::core::api::{add, cd_to_repo}; +use git_conform::utils::TrackingFile; +use std::fs; +use std::path::Path; +use serial_test::serial; + +#[test] +#[serial] +fn test_cd_to_existing_repo() { + let (_home_dir, mut tracking_file, tests_dir) = common::setup().unwrap(); + + // Add some repositories to the tracking file + let repos = vec![ + format!("{}/repo1", tests_dir), + format!("{}/repo2", tests_dir), + format!("{}/repo3", tests_dir), + ]; + assert!(add(repos.clone(), &tracking_file).is_ok()); + + // Manually update tracking_file contents + tracking_file.contents = repos.join("\n"); + + // Test cd_to_repo for each added repository + for n in 1..=3 { + let repo_name = format!("repo{}", n); + let expected_path = format!("{}/repo{}", tests_dir, n); + assert_eq!(cd_to_repo(&repo_name, &tracking_file), Ok(expected_path)); + } + + common::cleanup(&tests_dir).unwrap(); +} + +#[test] +#[serial] +fn test_cd_to_nonexistent_repo() { + let (_home_dir, mut tracking_file, tests_dir) = common::setup().unwrap(); + + // Add a repository to ensure the tracking file is not empty + let repo = format!("{}/repo1", tests_dir); + assert!(add(vec![repo.clone()], &tracking_file).is_ok()); + + // Manually update tracking_file contents + tracking_file.contents = repo; + + // Try to cd to a non-existent repository + let fake_repo = "fake_repo"; + assert_eq!( + cd_to_repo(fake_repo, &tracking_file), + Err(format!("Repository '{}' not found in tracking file", fake_repo)) + ); + + common::cleanup(&tests_dir).unwrap(); +} + +#[test] +#[serial] +fn test_cd_with_empty_tracking_file() { + let (_home_dir, tracking_file, tests_dir) = common::setup().unwrap(); + + // Ensure the tracking file is empty (this is already the case after setup) + + // Try to cd to any repository with an empty tracking file + let repo_name = "any_repo"; + assert_eq!( + cd_to_repo(repo_name, &tracking_file), + Err(String::from("No repository is being tracked")) + ); + + common::cleanup(&tests_dir).unwrap(); +} + +#[test] +#[serial] +fn test_cd_to_hidden_repo() { + let (_home_dir, mut tracking_file, tests_dir) = common::setup().unwrap(); + + // Add a hidden repository to the tracking file + let hidden_repo = format!("{}/.hidden/repo1", tests_dir); + assert!(add(vec![hidden_repo.clone()], &tracking_file).is_ok()); + + // Manually update tracking_file contents + tracking_file.contents = hidden_repo.clone(); + + // Test cd_to_repo for the hidden repository + assert_eq!(cd_to_repo("repo1", &tracking_file), Ok(hidden_repo)); + + common::cleanup(&tests_dir).unwrap(); +} + +#[test] +#[serial] +fn test_cd_multiple_repos_same_name() { + let (_home_dir, mut tracking_file, tests_dir) = common::setup().unwrap(); + + // Add repositories with the same name in different directories + let repos = vec![ + format!("{}/repo1", tests_dir), + format!("{}/.hidden/repo1", tests_dir), + ]; + assert!(add(repos.clone(), &tracking_file).is_ok()); + + // Manually update tracking_file contents + tracking_file.contents = repos.join("\n"); + + // Test cd_to_repo with a repo name that exists multiple times + let expected_path = format!("{}/repo1", tests_dir); + assert_eq!(cd_to_repo("repo1", &tracking_file), Ok(expected_path)); + + common::cleanup(&tests_dir).unwrap(); +} + +#[test] +#[serial] +fn test_cd_to_fake_repo() { + let (_home_dir, mut tracking_file, tests_dir) = common::setup().unwrap(); + + // Add a real repository to ensure the tracking file is not empty + let real_repo = format!("{}/repo1", tests_dir); + assert!(add(vec![real_repo.clone()], &tracking_file).is_ok()); + + // Manually update tracking_file contents + tracking_file.contents = real_repo; + + // Try to cd to a fake repository + assert_eq!( + cd_to_repo("fake_repo1", &tracking_file), + Err(String::from("Repository 'fake_repo1' not found in tracking file")) + ); + + common::cleanup(&tests_dir).unwrap(); +} \ No newline at end of file diff --git a/tests/common/mod.rs b/tests/common/mod.rs index f99f48d..7dd0d43 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -108,3 +108,9 @@ pub fn setup() -> Result<(String, TrackingFile, String), String> { Ok((home_dir, tracking_file, tests_dir)) } + +pub fn cleanup(tests_dir: &str) -> Result<(), String> { + fs::remove_dir_all(tests_dir) + .map_err(|e| format!("Failed to remove test directory: {}", e))?; + Ok(()) +} \ No newline at end of file