diff --git a/.gitignore b/.gitignore index eeff576d1..ca19e2ca3 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,9 @@ Cargo.lock .DS_Store **/*.dylib +.cursor +.md + # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. .VSCodeCounter diff --git a/libra/src/command/remove.rs b/libra/src/command/remove.rs index e3a06f2be..5a600dd1d 100644 --- a/libra/src/command/remove.rs +++ b/libra/src/command/remove.rs @@ -33,17 +33,15 @@ pub fn execute(args: RemoveArgs) -> Result<(), GitError> { let mut index = Index::load(&idx_file)?; // check if pathspec is all in index (skip if force is enabled) - if !args.force && !validate_pathspec(&args.pathspec, &index) { - return Ok(()); + if !args.force { + validate_pathspec(&args.pathspec, &index)? } let dirs = get_dirs(&args.pathspec, &index, args.force); if !dirs.is_empty() && !args.recursive { - println!( - "fatal: not removing '{}' recursively without -r", - dirs[0].bright_blue() - ); // Git print first - return Ok(()); + let error_msg = format!("not removing '{}' recursively without -r", dirs[0]); + println!("fatal: {error_msg}"); + return Err(GitError::CustomError(error_msg)); } for path_str in args.pathspec.iter() { @@ -87,10 +85,11 @@ pub fn execute(args: RemoveArgs) -> Result<(), GitError> { /// check if pathspec is all valid(in index) /// - if path is a dir, check if any file in the dir is in index -fn validate_pathspec(pathspec: &[String], index: &Index) -> bool { +fn validate_pathspec(pathspec: &[String], index: &Index) -> Result<(), GitError> { if pathspec.is_empty() { - println!("fatal: No pathspec was given. Which files should I remove?"); - return false; + let error_msg = "No pathspec was given. Which files should I remove?".to_string(); + println!("fatal: {error_msg}"); + return Err(GitError::CustomError(error_msg)); } for path_str in pathspec.iter() { let path = PathBuf::from(path_str); @@ -99,12 +98,13 @@ fn validate_pathspec(pathspec: &[String], index: &Index) -> bool { // not tracked, but path may be a directory // check if any tracked file in the directory if !index.contains_dir_file(&path_wd) { - println!("fatal: pathspec '{path_str}' did not match any files"); - return false; + let error_msg = format!("pathspec '{path_str}' did not match any files"); + println!("fatal: {error_msg}"); + return Err(GitError::CustomError(error_msg)); } } } - true + Ok(()) } /// run after `validate_pathspec` diff --git a/libra/tests/command/add_test.rs b/libra/tests/command/add_test.rs index 8b1378917..ccf6a55f0 100644 --- a/libra/tests/command/add_test.rs +++ b/libra/tests/command/add_test.rs @@ -1 +1,254 @@ +use super::*; +use std::fs; +use std::io::Write; +#[tokio::test] +#[serial] +/// Tests the basic functionality of add command by adding a single file +async fn test_add_single_file() { + let test_dir = tempdir().unwrap(); + test::setup_with_new_libra_in(test_dir.path()).await; + let _guard = test::ChangeDirGuard::new(test_dir.path()); + + // Create a new file + let file_content = "Hello, World!"; + let file_path = "test_file.txt"; + let mut file = fs::File::create(file_path).unwrap(); + file.write_all(file_content.as_bytes()).unwrap(); + + // Execute add command + add::execute(AddArgs { + pathspec: vec![String::from(file_path)], + all: false, + update: false, + refresh: false, + verbose: false, + dry_run: false, + ignore_errors: false, + }) + .await; + + // Verify the file was added to index. + let changes = changes_to_be_committed().await; + + assert!(changes.new.iter().any(|x| x.to_str().unwrap() == file_path)); +} + +#[tokio::test] +#[serial] +/// Tests adding multiple files at once +async fn test_add_multiple_files() { + let test_dir = tempdir().unwrap(); + test::setup_with_new_libra_in(test_dir.path()).await; + let _guard = test::ChangeDirGuard::new(test_dir.path()); + + // Create multiple files + for i in 1..=3 { + let file_content = format!("File content {i}"); + let file_path = format!("test_file_{i}.txt"); + let mut file = fs::File::create(&file_path).unwrap(); + file.write_all(file_content.as_bytes()).unwrap(); + } + + // Execute add command + add::execute(AddArgs { + pathspec: vec![ + String::from("test_file_1.txt"), + String::from("test_file_2.txt"), + String::from("test_file_3.txt"), + ], + all: false, + update: false, + refresh: false, + verbose: false, + dry_run: false, + ignore_errors: false, + }) + .await; + + // Verify all files were added to index + let changes = changes_to_be_committed().await; + assert!(changes.new.iter().any(|x| x.to_str().unwrap() == "test_file_1.txt")); + assert!(changes.new.iter().any(|x| x.to_str().unwrap() == "test_file_2.txt")); + assert!(changes.new.iter().any(|x| x.to_str().unwrap() == "test_file_3.txt")); +} + +#[tokio::test] +#[serial] +/// Tests the --all flag which adds all files in the working tree +async fn test_add_all_flag() { + let test_dir = tempdir().unwrap(); + test::setup_with_new_libra_in(test_dir.path()).await; + let _guard = test::ChangeDirGuard::new(test_dir.path()); + + // Create multiple files + for i in 1..=3 { + let file_content = format!("File content {i}"); + let file_path = format!("test_file_{i}.txt"); + let mut file = fs::File::create(&file_path).unwrap(); + file.write_all(file_content.as_bytes()).unwrap(); + } + + // Execute add command with --all flag + add::execute(AddArgs { + pathspec: vec![], + all: true, + update: false, + refresh: false, + verbose: false, + dry_run: false, + ignore_errors: false, + }) + .await; + + // Verify all files were added to index + let changes = changes_to_be_committed().await; + assert!(changes.new.iter().any(|x| x.to_str().unwrap() == "test_file_1.txt")); + assert!(changes.new.iter().any(|x| x.to_str().unwrap() == "test_file_2.txt")); + assert!(changes.new.iter().any(|x| x.to_str().unwrap() == "test_file_3.txt")); +} + +#[tokio::test] +#[serial] +/// Tests the --update flag which only updates files already in the index +async fn test_add_update_flag() { + let test_dir = tempdir().unwrap(); + test::setup_with_new_libra_in(test_dir.path()).await; + let _guard = test::ChangeDirGuard::new(test_dir.path()); + + // Create files and add one to the index + let tracked_file = "tracked_file.txt"; + let untracked_file = "untracked_file.txt"; + + // Create and write initial content + let mut file1 = fs::File::create(tracked_file).unwrap(); + file1.write_all(b"Initial content").unwrap(); + + let mut file2 = fs::File::create(untracked_file).unwrap(); + file2.write_all(b"Initial content").unwrap(); + + // Add only one file to the index + add::execute(AddArgs { + pathspec: vec![String::from(tracked_file)], + all: false, + update: false, + refresh: false, + verbose: false, + dry_run: false, + ignore_errors: false, + }) + .await; + + // Modify both files + let mut file1 = fs::OpenOptions::new().write(true).truncate(true).open(tracked_file).unwrap(); + file1.write_all(b" - Modified").unwrap(); + + let mut file2 = fs::OpenOptions::new().write(true).truncate(true).open(untracked_file).unwrap(); + file2.write_all(b" - Modified").unwrap(); + + // Execute add command with --update flag + add::execute(AddArgs { + pathspec: vec![String::from(".")], + all: false, + update: true, + refresh: false, + verbose: false, + dry_run: false, + ignore_errors: false, + }) + .await; + + // Verify only tracked file was updated + let changes = changes_to_be_staged(); + // Tracked file should not appear in changes (because it was updated in index) + assert!(!changes.modified.iter().any(|x| x.to_str().unwrap() == tracked_file)); + // Untracked file should still be untracked and show as new + assert!(changes.new.iter().any(|x| x.to_str().unwrap() == untracked_file)); +} + +#[tokio::test] +#[serial] +/// Tests adding files with respect to ignore patterns in .libraignore +async fn test_add_with_ignore_patterns() { + let test_dir = tempdir().unwrap(); + test::setup_with_new_libra_in(test_dir.path()).await; + let _guard = test::ChangeDirGuard::new(test_dir.path()); + + // Create .libraignore file + let mut ignore_file = fs::File::create(".libraignore").unwrap(); + ignore_file.write_all(b"ignored_*.txt\nignore_dir/**").unwrap(); + + // Create files that should be ignored and not ignored + let ignored_file = "ignored_file.txt"; + let tracked_file = "tracked_file.txt"; + + // Create directory that should be ignored + fs::create_dir("ignore_dir").unwrap(); + let ignored_dir_file = "ignore_dir/file.txt"; + + // Create and write content + let mut file1 = fs::File::create(ignored_file).unwrap(); + file1.write_all(b"Should be ignored").unwrap(); + + let mut file2 = fs::File::create(tracked_file).unwrap(); + file2.write_all(b"Should be tracked").unwrap(); + + let mut file3 = fs::File::create(ignored_dir_file).unwrap(); + file3.write_all(b"Should be ignored").unwrap(); + + // Execute add command with all files + add::execute(AddArgs { + pathspec: vec![String::from(".")], + all: true, + update: false, + refresh: false, + verbose: false, + dry_run: false, + ignore_errors: false, + }) + .await; + + // Verify only non-ignored files were added + let changes_staged = changes_to_be_staged(); + let changes_committed = changes_to_be_committed().await; + + // Ignored files should not appear in any status (they are ignored) + assert!(!changes_staged.new.iter().any(|x| x.to_str().unwrap() == ignored_file)); + assert!(!changes_staged.new.iter().any(|x| x.to_str().unwrap() == ignored_dir_file)); + assert!(!changes_committed.new.iter().any(|x| x.to_str().unwrap() == ignored_file)); + assert!(!changes_committed.new.iter().any(|x| x.to_str().unwrap() == ignored_dir_file)); + + // Non-ignored file should not show as new in staged (was added) but should show in committed + assert!(!changes_staged.new.iter().any(|x| x.to_str().unwrap() == tracked_file)); + assert!(changes_committed.new.iter().any(|x| x.to_str().unwrap() == tracked_file)); +} + +#[tokio::test] +#[serial] +/// Tests the dry-run flag which should not actually add files +async fn test_add_dry_run() { + let test_dir = tempdir().unwrap(); + test::setup_with_new_libra_in(test_dir.path()).await; + let _guard = test::ChangeDirGuard::new(test_dir.path()); + + // Create a file. + let file_path = "test_file.txt"; + let mut file = fs::File::create(file_path).unwrap(); + file.write_all(b"Test content").unwrap(); + + // Execute add command with dry-run + add::execute(AddArgs { + pathspec: vec![String::from(file_path)], + all: false, + update: false, + refresh: false, + verbose: false, + dry_run: true, + ignore_errors: false, + }) + .await; + + // Verify the file was not actually added to index + let changes = changes_to_be_staged(); + assert!(changes.new.iter().any(|x| x.to_str().unwrap() == file_path)); +} diff --git a/libra/tests/command/diff_test.rs b/libra/tests/command/diff_test.rs index 8b1378917..ad63fec8f 100644 --- a/libra/tests/command/diff_test.rs +++ b/libra/tests/command/diff_test.rs @@ -1 +1,356 @@ +use super::*; +use std::fs; +use std::io::Write; +use clap::Parser; +use libra::command::diff::{self, DiffArgs}; + +/// Helper function to create a file with content. +fn create_file(path: &str, content: &str) { + let mut file = fs::File::create(path).unwrap(); + file.write_all(content.as_bytes()).unwrap(); +} + +/// Helper function to modify a file with new content. +fn modify_file(path: &str, content: &str) { + let mut file = fs::OpenOptions::new().write(true).truncate(true).open(path).unwrap(); + file.write_all(content.as_bytes()).unwrap(); +} + +#[tokio::test] +#[serial] +/// Tests the basic diff functionality between working directory and HEAD. +async fn test_basic_diff() { + let test_dir = tempdir().unwrap(); + test::setup_with_new_libra_in(test_dir.path()).await; + let _guard = test::ChangeDirGuard::new(test_dir.path()); + + // Create a file and add it to index + create_file("file1.txt", "Initial content\nLine 2\nLine 3\n"); + + add::execute(AddArgs { + pathspec: vec![String::from("file1.txt")], + all: false, + update: false, + refresh: false, + verbose: false, + dry_run: false, + ignore_errors: false, + }) + .await; + + // Create initial commit + commit::execute(CommitArgs { + message: "Initial commit".to_string(), + allow_empty: false, + conventional: false, + amend: false, + signoff: false, + disable_pre: false, + }) + .await; + + // Modify the file + modify_file("file1.txt", "Modified content\nLine 2\nLine 3 changed\n"); + + // Run diff command + let args = DiffArgs::parse_from([ + "diff", "--algorithm", "histogram" + ]); + diff::execute(args).await; + + // We can't easily capture stdout, so we'll check that the command didn't panic +} + +#[tokio::test] +#[serial] +/// Tests diff with staged changes +async fn test_diff_staged() { + let test_dir = tempdir().unwrap(); + test::setup_with_new_libra_in(test_dir.path()).await; + let _guard = test::ChangeDirGuard::new(test_dir.path()); + + // Create a file and add it to index + create_file("file1.txt", "Initial content\nLine 2\nLine 3\n"); + + add::execute(AddArgs { + pathspec: vec![String::from("file1.txt")], + all: false, + update: false, + refresh: false, + verbose: false, + dry_run: false, + ignore_errors: false, + }) + .await; + + // Create initial commit + commit::execute(CommitArgs { + message: "Initial commit".to_string(), + allow_empty: false, + conventional: false, + amend: false, + signoff: false, + disable_pre: false, + }) + .await; + + // Modify the file and stage it + modify_file("file1.txt", "Modified content\nLine 2\nLine 3 changed\n"); + + add::execute(AddArgs { + pathspec: vec![String::from("file1.txt")], + all: false, + update: false, + refresh: false, + verbose: false, + dry_run: false, + ignore_errors: false, + }) + .await; + + // Modify the file again (so working dir differs from staged) + modify_file("file1.txt", "Modified content again\nLine 2\nLine 3 changed again\n"); + + // Run diff command with --staged flag + let args = DiffArgs::parse_from([ + "diff", "--staged", "--algorithm", "histogram" + ]); + diff::execute(args).await; + + // The command should complete without panicking +} + +#[tokio::test] +#[serial] +/// Tests diff between two specific commits +async fn test_diff_between_commits() { + let test_dir = tempdir().unwrap(); + test::setup_with_new_libra_in(test_dir.path()).await; + let _guard = test::ChangeDirGuard::new(test_dir.path()); + + // Create a file and make initial commit + create_file("file1.txt", "Initial content\nLine 2\nLine 3\n"); + + add::execute(AddArgs { + pathspec: vec![String::from("file1.txt")], + all: false, + update: false, + refresh: false, + verbose: false, + dry_run: false, + ignore_errors: false, + }) + .await; + + commit::execute(CommitArgs { + message: "Initial commit".to_string(), + allow_empty: false, + conventional: false, + amend: false, + signoff: false, + disable_pre: false, + }) + .await; + + // Get the first commit hash + let first_commit = Head::current_commit().await.unwrap(); + + // Modify file and create a second commit + modify_file("file1.txt", "Modified content\nLine 2\nLine 3 changed\n"); + + add::execute(AddArgs { + pathspec: vec![String::from("file1.txt")], + all: false, + update: false, + refresh: false, + verbose: false, + dry_run: false, + ignore_errors: false, + }) + .await; + + commit::execute(CommitArgs { + message: "Second commit".to_string(), + allow_empty: false, + conventional: false, + amend: false, + signoff: false, + disable_pre: false, + }) + .await; + + // Get the second commit hash + let second_commit = Head::current_commit().await.unwrap(); + + // Run diff command comparing the two commits + let args = DiffArgs::parse_from([ + "diff", "--old", &first_commit.to_string(), "--new", &second_commit.to_string(), "--algorithm", "histogram" + ]); + diff::execute(args).await; + + // The command should complete without panicking +} + +#[tokio::test] +#[serial] +/// Tests diff with specific file path +async fn test_diff_with_pathspec() { + let test_dir = tempdir().unwrap(); + test::setup_with_new_libra_in(test_dir.path()).await; + let _guard = test::ChangeDirGuard::new(test_dir.path()); + + // Create multiple files and commit them + create_file("file1.txt", "File 1 content\nLine 2\nLine 3\n"); + create_file("file2.txt", "File 2 content\nLine 2\nLine 3\n"); + + add::execute(AddArgs { + pathspec: vec![String::from(".")], + all: false, + update: false, + refresh: false, + verbose: false, + dry_run: false, + ignore_errors: false, + }) + .await; + + commit::execute(CommitArgs { + message: "Initial commit".to_string(), + allow_empty: false, + conventional: false, + amend: false, + signoff: false, + disable_pre: false, + }) + .await; + + // Modify both files + modify_file("file1.txt", "File 1 modified\nLine 2\nLine 3 changed\n"); + modify_file("file2.txt", "File 2 modified\nLine 2\nLine 3 changed\n"); + + // Run diff command with specific file path + let args = DiffArgs::parse_from([ + "diff", "--algorithm", "histogram", "file1.txt" + ]); + diff::execute(args).await; + + // The command should complete without panicking +} + +#[tokio::test] +#[serial] +/// Tests diff with output to a file +async fn test_diff_output_to_file() { + let test_dir = tempdir().unwrap(); + test::setup_with_new_libra_in(test_dir.path()).await; + let _guard = test::ChangeDirGuard::new(test_dir.path()); + + // Create a file and commit it + create_file("file1.txt", "Initial content\nLine 2\nLine 3\n"); + + add::execute(AddArgs { + pathspec: vec![String::from("file1.txt")], + all: false, + update: false, + refresh: false, + verbose: false, + dry_run: false, + ignore_errors: false, + }) + .await; + + commit::execute(CommitArgs { + message: "Initial commit".to_string(), + allow_empty: false, + conventional: false, + amend: false, + signoff: false, + disable_pre: false, + }) + .await; + + // Modify the file + modify_file("file1.txt", "Modified content\nLine 2\nLine 3 changed\n"); + + // Output file path + let output_file = "diff_output.txt"; + + // Run diff command with output to file + let args = DiffArgs::parse_from([ + "diff", "--algorithm", "histogram", "--output", output_file + ]); + diff::execute(args).await; + + // Verify the output file exists + assert!(fs::metadata(output_file).is_ok(), "Output file should exist"); + + // Read the file content to make sure it contains diff output + let content = fs::read_to_string(output_file).unwrap(); + assert!(content.contains("diff --git"), "Output should contain diff header"); +} + +#[tokio::test] +#[serial] +/// Tests diff with different algorithms +async fn test_diff_algorithms() { + let test_dir = tempdir().unwrap(); + test::setup_with_new_libra_in(test_dir.path()).await; + let _guard = test::ChangeDirGuard::new(test_dir.path()); + + // Create a file with some content to make a non-trivial diff + create_file( + "file1.txt", + "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\n", + ); + + add::execute(AddArgs { + pathspec: vec![String::from("file1.txt")], + all: false, + update: false, + refresh: false, + verbose: false, + dry_run: false, + ignore_errors: false, + }) + .await; + + commit::execute(CommitArgs { + message: "Initial commit".to_string(), + allow_empty: false, + conventional: false, + amend: false, + signoff: false, + disable_pre: false, + }) + .await; + + // Make complex changes to test different algorithms + modify_file( + "file1.txt", + "Line 1\nModified Line\nLine 3\nNew Line\nLine 5\nLine 6\nDeleted Line 7\n", + ); + + // Test histogram algorithm + let args = DiffArgs::parse_from([ + "diff", "--algorithm", "histogram", "--output", "histogram_diff.txt" + ]); + diff::execute(args).await; + + // Test myers algorithm + let args = DiffArgs::parse_from([ + "diff", "--algorithm", "myers", "--output", "myers_diff.txt" + ]); + diff::execute(args).await; + + // Test myersMinimal algorithm + let args = DiffArgs::parse_from([ + "diff", "--algorithm", "myersMinimal", "--output", "myersMinimal_diff.txt" + ]); + diff::execute(args).await; + + // Verify all output files exist + assert!(fs::metadata("histogram_diff.txt").is_ok(), "Histogram output file should exist"); + assert!(fs::metadata("myers_diff.txt").is_ok(), "Myers output file should exist"); + assert!(fs::metadata("myersMinimal_diff.txt").is_ok(), "MyersMinimal output file should exist"); +} diff --git a/libra/tests/command/mod.rs b/libra/tests/command/mod.rs index b2190860b..c44e1b89d 100644 --- a/libra/tests/command/mod.rs +++ b/libra/tests/command/mod.rs @@ -6,10 +6,11 @@ use libra::command::init::init; use libra::command::init::InitArgs; use libra::command::log::{get_reachable_commits, LogArgs}; use libra::command::save_object; -use libra::command::status::changes_to_be_staged; +use libra::command::status::{changes_to_be_staged, changes_to_be_committed}; use libra::command::switch::{self, check_status, SwitchArgs}; use libra::command::{ add::{self, AddArgs}, + remove::{self, RemoveArgs}, load_object, }; use libra::internal::branch::Branch; diff --git a/libra/tests/command/remove_test.rs b/libra/tests/command/remove_test.rs index 412720728..04a71f0e9 100644 --- a/libra/tests/command/remove_test.rs +++ b/libra/tests/command/remove_test.rs @@ -1,318 +1,331 @@ -use libra::command::{add, commit, init, remove}; -use libra::utils::test; -use serial_test::serial; -use std::path::Path; -use tempfile::tempdir; -async fn test_remove_file() { - println!("\n\x1b[1mTest remove file functionality.\x1b[0m"); +use super::*; +use std::fs; +use std::io::Write; +use std::path::PathBuf; - // Create a test file - test::ensure_file("test_file.txt", Some("test content")); - // Add file to index - let add_args = add::AddArgs { + +/// Helper function to create a file with content. +fn create_file(path: &str, content: &str) -> PathBuf { + let path = PathBuf::from(path); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).unwrap(); + } + let mut file = fs::File::create(&path).unwrap(); + file.write_all(content.as_bytes()).unwrap(); + path +} + +#[tokio::test] +#[serial] +/// Tests the basic remove functionality by removing a single file +async fn test_remove_single_file() { + let test_dir = tempdir().unwrap(); + test::setup_with_new_libra_in(test_dir.path()).await; + let _guard = test::ChangeDirGuard::new(test_dir.path()); + + // Create a file and add it to index + let file_path = create_file("test_file.txt", "Test content"); + + add::execute(AddArgs { + pathspec: vec![String::from("test_file.txt")], all: false, update: false, + refresh: false, verbose: false, - pathspec: vec!["test_file.txt".to_string()], dry_run: false, - refresh: false, ignore_errors: false, - }; - add::execute(add_args).await; + }) + .await; + + // Make sure the file exists + assert!(file_path.exists(), "File should exist before removal"); - // Remove file from index and filesystem - let remove_args = remove::RemoveArgs { - pathspec: vec!["test_file.txt".to_string()], + // Remove the file + let args = RemoveArgs { + pathspec: vec![String::from("test_file.txt")], cached: false, recursive: false, force: false, }; - let _ = remove::execute(remove_args); - - // Verify file is removed from filesystem - assert!(!Path::new("test_file.txt").exists()); + remove::execute(args).unwrap(); + + // Verify the file was removed from the filesystem + assert!(!file_path.exists(), "File should be removed from filesystem"); + + // Verify file is no longer in the index + let changes = changes_to_be_staged(); + assert!(!changes.new.iter().any(|x| x.to_str().unwrap() == "test_file.txt"), + "File should not appear in changes as new"); + assert!(!changes.modified.iter().any(|x| x.to_str().unwrap() == "test_file.txt"), + "File should not appear in changes as modified"); + assert!(!changes.deleted.iter().any(|x| x.to_str().unwrap() == "test_file.txt"), + "File should not appear in changes as deleted"); } -async fn test_remove_cached_file() { - println!("\n\x1b[1mTest remove cached file functionality.\x1b[0m"); - - // Create a test file - test::ensure_file("cached_file.txt", Some("cached content")); - - // Add file to index - let add_args = add::AddArgs { +#[tokio::test] +#[serial] +/// Tests removing a file with --cached flag, which only removes from the index but keeps the file +async fn test_remove_cached() { + let test_dir = tempdir().unwrap(); + test::setup_with_new_libra_in(test_dir.path()).await; + let _guard = test::ChangeDirGuard::new(test_dir.path()); + + // Create a file and add it to index + let file_path = create_file("test_file.txt", "Test content"); + + add::execute(AddArgs { + pathspec: vec![String::from("test_file.txt")], all: false, update: false, + refresh: false, verbose: false, - pathspec: vec!["cached_file.txt".to_string()], dry_run: false, - refresh: false, ignore_errors: false, - }; - add::execute(add_args).await; + }) + .await; + + // Make sure the file exists + assert!(file_path.exists(), "File should exist before removal"); - // Remove file from index only (keep in filesystem) - let remove_args = remove::RemoveArgs { - pathspec: vec!["cached_file.txt".to_string()], + // Remove the file with --cached flag + let args = RemoveArgs { + pathspec: vec![String::from("test_file.txt")], cached: true, recursive: false, force: false, }; - let _ = remove::execute(remove_args); + remove::execute(args).unwrap(); - // Verify file still exists in filesystem - assert!(Path::new("cached_file.txt").exists()); -} - -async fn test_remove_directory() { - println!("\n\x1b[1mTest remove directory functionality.\x1b[0m"); + // Verify the file still exists in the filesystem + assert!(file_path.exists(), "File should still exist in filesystem"); - // Create directory structure - test::ensure_file("test_dir/file1.txt", Some("file1 content")); - test::ensure_file("test_dir/file2.txt", Some("file2 content")); - test::ensure_file("test_dir/subdir/file3.txt", Some("file3 content")); + // Verify file appears as new (untracked) in the index + let changes = changes_to_be_staged(); + assert!(changes.new.iter().any(|x| x.to_str().unwrap() == "test_file.txt"), + "File should appear in changes as new/untracked"); +} - // Add all files to index - let add_args = add::AddArgs { - all: true, +#[tokio::test] +#[serial] +/// Tests recursive removal of a directory +async fn test_remove_directory_recursive() { + let test_dir = tempdir().unwrap(); + test::setup_with_new_libra_in(test_dir.path()).await; + let _guard = test::ChangeDirGuard::new(test_dir.path()); + + // Create a directory with files + let file1 = create_file("test_dir/file1.txt", "File 1 content"); + let file2 = create_file("test_dir/file2.txt", "File 2 content"); + let file3 = create_file("test_dir/subdir/file3.txt", "File 3 content"); + + // Add all files to the index + add::execute(AddArgs { + pathspec: vec![String::from("test_dir")], + all: false, update: false, + refresh: false, verbose: false, - pathspec: vec![], dry_run: false, - refresh: false, ignore_errors: false, - }; - add::execute(add_args).await; - - // Remove directory recursively - let remove_args = remove::RemoveArgs { - pathspec: vec!["test_dir".to_string()], + }) + .await; + + // Make sure the directory and files exist + assert!(fs::metadata("test_dir").is_ok(), "Directory should exist"); + assert!(file1.exists(), "File 1 should exist"); + assert!(file2.exists(), "File 2 should exist"); + assert!(file3.exists(), "File 3 should exist"); + + // Remove the directory recursively + let args = RemoveArgs { + pathspec: vec![String::from("test_dir")], cached: false, recursive: true, force: false, }; - let _ = remove::execute(remove_args); - - // Verify directory is removed - assert!(!Path::new("test_dir").exists()); + remove::execute(args).unwrap(); + + // Verify the directory and files were removed + assert!(fs::metadata("test_dir").is_err(), "Directory should be removed"); + assert!(!file1.exists(), "File 1 should be removed"); + assert!(!file2.exists(), "File 2 should be removed"); + assert!(!file3.exists(), "File 3 should be removed"); + + // Verify files are no longer in the index + let changes = changes_to_be_staged(); + for file in &[file1, file2, file3] { + let file_str = file.to_str().unwrap(); + assert!(!changes.new.iter().any(|x| x.to_str().unwrap() == file_str), + "File should not appear in changes as new"); + assert!(!changes.modified.iter().any(|x| x.to_str().unwrap() == file_str), + "File should not appear in changes as modified"); + assert!(!changes.deleted.iter().any(|x| x.to_str().unwrap() == file_str), + "File should not appear in changes as deleted"); + } } +#[tokio::test] +#[serial] +/// Tests attempting to remove a directory without -r flag should fail async fn test_remove_directory_without_recursive() { - println!("\n\x1b[1mTest remove directory without recursive flag.\x1b[0m"); - - // Create directory structure - test::ensure_file("test_dir2/file1.txt", Some("file1 content")); - test::ensure_file("test_dir2/file2.txt", Some("file2 content")); - - // Add all files to index - let add_args = add::AddArgs { - all: true, + let test_dir = tempdir().unwrap(); + test::setup_with_new_libra_in(test_dir.path()).await; + let _guard = test::ChangeDirGuard::new(test_dir.path()); + + // Create a directory with files + let file1 = create_file("test_dir/file1.txt", "File 1 content"); + let file2 = create_file("test_dir/file2.txt", "File 2 content"); + + // Add all files to the index + add::execute(AddArgs { + pathspec: vec![String::from("test_dir")], + all: false, update: false, + refresh: false, verbose: false, - pathspec: vec![], dry_run: false, - refresh: false, ignore_errors: false, - }; - add::execute(add_args).await; + }) + .await; - // Try to remove directory without recursive flag (should fail) - let remove_args = remove::RemoveArgs { - pathspec: vec!["test_dir2".to_string()], + // Make sure the directory and files exist + assert!(fs::metadata("test_dir").is_ok(), "Directory should exist"); + assert!(file1.exists(), "File 1 should exist"); + assert!(file2.exists(), "File 2 should exist"); + + // Attempt to remove the directory without recursive flag + let args = RemoveArgs { + pathspec: vec![String::from("test_dir")], cached: false, recursive: false, force: false, }; - let _ = remove::execute(remove_args); + assert!(remove::execute(args).is_err(), "Removing a directory without recursive should fail"); - // Directory should still exist because recursive flag was not used - assert!(Path::new("test_dir2").exists()); + // Verify the directory and files still exist + assert!(fs::metadata("test_dir").is_ok(), "Directory should still exist"); + assert!(file1.exists(), "File 1 should still exist"); + assert!(file2.exists(), "File 2 should still exist"); } -async fn test_force_remove_untracked_file() { - println!("\n\x1b[1mTest force remove untracked file.\x1b[0m"); - - // Create an untracked file - test::ensure_file("untracked_file.txt", Some("untracked content")); - - // Force remove untracked file - let remove_args = remove::RemoveArgs { - pathspec: vec!["untracked_file.txt".to_string()], +#[tokio::test] +#[serial] +/// Tests removing a file that does not exist in the index +async fn test_remove_untracked_file() { + let test_dir = tempdir().unwrap(); + test::setup_with_new_libra_in(test_dir.path()).await; + let _guard = test::ChangeDirGuard::new(test_dir.path()); + + // Create a file but don't add it to the index + let file_path = create_file("untracked_file.txt", "Untracked content"); + + // Make sure the file exists + assert!(file_path.exists(), "File should exist"); + + // Attempt to remove the untracked file (should fail/do nothing) + let args = RemoveArgs { + pathspec: vec![String::from("untracked_file.txt")], cached: false, recursive: false, - force: true, + force: false, }; - let _ = remove::execute(remove_args); + let result = remove::execute(args); + assert!(result.is_err(), "Removing an untracked file should return an error, not panic"); - // Verify file is removed - assert!(!Path::new("untracked_file.txt").exists()); + // Verify the file still exists + assert!(file_path.exists(), "File should still exist"); } -async fn test_no_force_remove_untracked_file() { - println!("\n\x1b[1mTest remove untracked file without force.\x1b[0m"); +#[tokio::test] +#[serial] +/// Tests removing a file that has been modified after being added to the index +async fn test_remove_modified_file() { + let test_dir = tempdir().unwrap(); + test::setup_with_new_libra_in(test_dir.path()).await; + let _guard = test::ChangeDirGuard::new(test_dir.path()); + + // Create a file and add it to index + let file_path = create_file("test_file.txt", "Initial content"); + + add::execute(AddArgs { + pathspec: vec![String::from("test_file.txt")], + all: false, + update: false, + refresh: false, + verbose: false, + dry_run: false, + ignore_errors: false, + }) + .await; - // Create an untracked file - test::ensure_file("untracked_file.txt", Some("untracked content")); + // Modify the file + let mut file = fs::OpenOptions::new().write(true).truncate(true).open(&file_path).unwrap(); + file.write_all(b" - Modified").unwrap(); - // remove untracked file without force - let remove_args = remove::RemoveArgs { - pathspec: vec!["untracked_file.txt".to_string()], + // Remove the file + let args = RemoveArgs { + pathspec: vec![String::from("test_file.txt")], cached: false, recursive: false, force: false, }; - let _ = remove::execute(remove_args); - - // Verify file is removed - assert!(Path::new("untracked_file.txt").exists()); -} - -async fn test_force_remove_untracked_directory() { - println!("\n\x1b[1mTest force remove untracked directory.\x1b[0m"); - - // Create untracked directory structure - test::ensure_file("untracked_dir/file1.txt", Some("file1 content")); - test::ensure_file("untracked_dir/file2.txt", Some("file2 content")); - test::ensure_file("untracked_dir/subdir/file3.txt", Some("file3 content")); - - // Force remove untracked directory - let remove_args = remove::RemoveArgs { - pathspec: vec!["untracked_dir".to_string()], - cached: false, - recursive: true, - force: true, - }; - let _ = remove::execute(remove_args); - - // Verify directory is removed - assert!(!Path::new("untracked_dir").exists()); + remove::execute(args).unwrap(); + + // Verify the file was removed. + assert!(!file_path.exists(), "File should be removed"); + + // Verify file is not in the index. + let changes = changes_to_be_staged(); + assert!(!changes.new.iter().any(|x| x.to_str().unwrap() == "test_file.txt"), + "File should not appear in changes as new"); + assert!(!changes.modified.iter().any(|x| x.to_str().unwrap() == "test_file.txt"), + "File should not appear in changes as modified"); + assert!(!changes.deleted.iter().any(|x| x.to_str().unwrap() == "test_file.txt"), + "File should not appear in changes as deleted"); } +#[tokio::test] +#[serial] +/// Tests removing multiple files at once async fn test_remove_multiple_files() { - println!("\n\x1b[1mTest remove multiple files.\x1b[0m"); + let test_dir = tempdir().unwrap(); + test::setup_with_new_libra_in(test_dir.path()).await; + let _guard = test::ChangeDirGuard::new(test_dir.path()); // Create multiple files - test::ensure_file("file1.txt", Some("content1")); - test::ensure_file("file2.txt", Some("content2")); - test::ensure_file("file3.txt", Some("content3")); - - // Add files to index - let add_args = add::AddArgs { - all: true, + let file1 = create_file("file1.txt", "File 1 content"); + let file2 = create_file("file2.txt", "File 2 content"); + let file3 = create_file("file3.txt", "File 3 content"); + + // Add all files to the index + add::execute(AddArgs { + pathspec: vec![String::from(".")], + all: false, update: false, + refresh: false, verbose: false, - pathspec: vec![], dry_run: false, - refresh: false, ignore_errors: false, - }; - add::execute(add_args).await; - - // Remove multiple files - let remove_args = remove::RemoveArgs { - pathspec: vec![ - "file1.txt".to_string(), - "file2.txt".to_string(), - "file3.txt".to_string(), - ], - cached: false, - recursive: false, - force: false, - }; - let _ = remove::execute(remove_args); - - // Verify all files are removed - assert!(!Path::new("file1.txt").exists()); - assert!(!Path::new("file2.txt").exists()); - assert!(!Path::new("file3.txt").exists()); -} - -async fn test_remove_nonexistent_file() { - println!("\n\x1b[1mTest remove nonexistent file.\x1b[0m"); - - // Try to remove a file that doesn't exist (should fail without force) - let remove_args = remove::RemoveArgs { - pathspec: vec!["nonexistent.txt".to_string()], - cached: false, - recursive: false, - force: false, - }; - let _ = remove::execute(remove_args); + }) + .await; - // With force flag, should not fail - let remove_args_force = remove::RemoveArgs { - pathspec: vec!["nonexistent.txt".to_string()], - cached: false, - recursive: false, - force: true, - }; - let _ = remove::execute(remove_args_force); -} - -async fn test_remove_empty_pathspec() { - println!("\n\x1b[1mTest remove with empty pathspec.\x1b[0m"); + // Make sure all files exist + assert!(file1.exists(), "File 1 should exist"); + assert!(file2.exists(), "File 2 should exist"); + assert!(file3.exists(), "File 3 should exist"); - // Try to remove with empty pathspec (should fail) - let remove_args = remove::RemoveArgs { - pathspec: vec![], + // Remove multiple files at once + let args = RemoveArgs { + pathspec: vec![String::from("file1.txt"), String::from("file3.txt")], cached: false, recursive: false, force: false, }; - let _ = remove::execute(remove_args); -} - -#[tokio::test] -#[serial] -/// Tests the remove command functionality including: -/// - Basic file removal -/// - Cached removal (keep in filesystem) -/// - Directory removal with recursive flag -/// - Force removal of untracked files -/// - Multiple file removal -/// - Error handling for invalid paths -async fn test_remove_command() { - println!("\n\x1b[1mTest remove command functionality.\x1b[0m"); - - let temp_path = tempdir().unwrap(); - test::setup_clean_testing_env_in(temp_path.path()); - let _guard = test::ChangeDirGuard::new(temp_path.path()); - - // Initialize repository - let init_args = init::InitArgs { - bare: false, - initial_branch: Some("main".to_string()), - repo_directory: temp_path.path().to_str().unwrap().to_string(), - quiet: false, - }; - init::init(init_args) - .await - .expect("Error initializing repository"); - - // Create initial commit - let commit_args = commit::CommitArgs { - message: "Initial commit".to_string(), - allow_empty: true, - conventional: false, - amend: false, - signoff: false, - disable_pre: true, - }; - commit::execute(commit_args).await; - - // Run all tests - test_remove_file().await; - test_remove_cached_file().await; - test_remove_directory().await; - test_remove_directory_without_recursive().await; - test_force_remove_untracked_file().await; - test_force_remove_untracked_directory().await; - test_no_force_remove_untracked_file().await; - test_remove_multiple_files().await; - test_remove_nonexistent_file().await; - test_remove_empty_pathspec().await; - - println!("\n\x1b[32m✓ All remove command tests passed!\x1b[0m"); + remove::execute(args).unwrap(); + // Verify the specified files were removed + assert!(!file1.exists(), "File 1 should be removed"); + assert!(file2.exists(), "File 2 should still exist"); + assert!(!file3.exists(), "File 3 should be removed"); }