diff --git a/aria/contents/docs/libra/command/rm/index.mdx b/aria/contents/docs/libra/command/rm/index.mdx
index 69694fbe8..b4ee63e3d 100644
--- a/aria/contents/docs/libra/command/rm/index.mdx
+++ b/aria/contents/docs/libra/command/rm/index.mdx
@@ -24,8 +24,54 @@ that.)
- `--cached`
Use this option to unstage and remove paths only from the index. Working tree files, whether modified or not, will be left alone.
+- `--dry-run`
+ Don't actually remove any files; instead just show if they exist in the index and would otherwise be removed by the command. This option is useful for previewing what files would be removed before actually executing the removal. It works with all other options (`--cached`, `--recursive`, `--force`) to show exactly what the actual command would do.
+
- `-r`, `--recursive`
Allow recursive removal when a leading directory name is given.
- `-f`, `--force`
Force removal even if the specified paths are not tracked. This skips validation checks and deletes files or directories regardless of their index status.
+
+### Examples
+
+#### Basic Usage
+```bash
+# Remove a file from both index and working tree
+libra rm file.txt
+
+# Remove multiple files
+libra rm file1.txt file2.txt
+```
+
+#### Using --cached
+```bash
+# Remove from index only, keep in working tree
+libra rm --cached file.txt
+```
+
+#### Using --dry-run
+```bash
+# Preview what would be removed
+libra rm --dry-run file1.txt file2.txt
+
+# Preview cached removal
+libra rm --dry-run --cached *.txt
+
+# Preview recursive directory removal
+libra rm --dry-run -r old_directory/
+```
+
+The `--dry-run` option will output something like:
+```
+Would remove the following:
+rm 'file1.txt'
+rm 'file2.txt'
+```
+
+#### Recursive Removal
+```bash
+# Remove entire directory recursively (preview first!)
+libra rm --dry-run -r directory/
+libra rm -r directory/
+```
diff --git a/libra/examples/test_dry_run.rs b/libra/examples/test_dry_run.rs
new file mode 100644
index 000000000..34554f69d
--- /dev/null
+++ b/libra/examples/test_dry_run.rs
@@ -0,0 +1,55 @@
+// Simple test program to verify rm --dry-run functionality
+use libra::command::remove::{execute, RemoveArgs};
+use std::fs;
+use std::io::Write;
+use tempfile::TempDir;
+
+fn main() {
+ println!("Testing libra rm --dry-run functionality...");
+
+ // Create a temporary directory for test files
+ let temp_dir = TempDir::new().expect("Failed to create temporary directory");
+ let temp_path = temp_dir.path();
+
+ // Create test files in the temporary directory
+ let file1_path = temp_path.join("file1.txt");
+ let file2_path = temp_path.join("file2.txt");
+
+ let mut file1 = fs::File::create(&file1_path).unwrap();
+ file1.write_all(b"Test content 1").unwrap();
+
+ let mut file2 = fs::File::create(&file2_path).unwrap();
+ file2.write_all(b"Test content 2").unwrap();
+
+ println!("Created test files in temporary directory: {:?}", temp_path);
+
+ // Test dry-run functionality
+ let args = RemoveArgs {
+ pathspec: vec![file1_path.to_string_lossy().to_string(), file2_path.to_string_lossy().to_string()],
+ cached: false,
+ recursive: false,
+ force: true, // Use force mode to avoid requiring git repository
+ dry_run: true,
+ };
+
+ println!("\nExecuting: libra rm --dry-run --force on test files");
+
+ match execute(args) {
+ Ok(_) => {
+ println!("✓ dry-run executed successfully!");
+
+ // Verify files still exist
+ if file1_path.exists() && file2_path.exists() {
+ println!(" Files still exist after dry-run (correct behavior)");
+ } else {
+ println!(" Error: dry-run should not actually delete files");
+ }
+ }
+ Err(e) => {
+ println!("✗ dry-run execution failed: {:?}", e);
+ }
+ }
+
+ println!("\nTest completed, temporary directory will be automatically cleaned up");
+ // The temporary directory will be automatically cleaned up when temp_dir goes out of scope
+}
diff --git a/libra/src/command/remove.rs b/libra/src/command/remove.rs
index 5a600dd1d..6b2e112ec 100644
--- a/libra/src/command/remove.rs
+++ b/libra/src/command/remove.rs
@@ -23,6 +23,9 @@ pub struct RemoveArgs {
/// force removal, skip validation
#[clap(short, long)]
pub force: bool,
+ /// show what would be removed without actually removing
+ #[clap(long)]
+ pub dry_run: bool,
}
pub fn execute(args: RemoveArgs) -> Result<(), GitError> {
@@ -44,6 +47,53 @@ pub fn execute(args: RemoveArgs) -> Result<(), GitError> {
return Err(GitError::CustomError(error_msg));
}
+ // In dry-run mode, show what would be removed but don't actually remove anything
+ if args.dry_run {
+ println!("Would remove the following:");
+ for path_str in args.pathspec.iter() {
+ let path = PathBuf::from(path_str);
+ let path_wd = path.to_workdir().to_string_or_panic();
+
+ if dirs.contains(path_str) {
+ // dir - find all files in this directory that are tracked
+ let entries = index.tracked_entries(0);
+ // Create directory prefix with proper path separator for cross-platform compatibility
+ let dir_prefix = if path_wd.is_empty() {
+ String::new()
+ } else if path_wd.ends_with(std::path::MAIN_SEPARATOR) {
+ path_wd.clone()
+ } else {
+ format!("{}{}", path_wd, std::path::MAIN_SEPARATOR)
+ };
+ for entry in entries.iter() {
+ if entry.name.starts_with(&dir_prefix) {
+ println!("rm '{}'", entry.name.bright_yellow());
+ }
+ }
+ if !args.cached {
+ println!("rm directory '{}'", path_str.bright_yellow());
+ }
+ } else {
+ // file
+ if args.force {
+ // In force mode: always try to remove (matches actual execution logic)
+ // - If tracked, would be removed from index
+ // - If not tracked, would still be processed (and filesystem file deleted if not cached)
+ if index.tracked(&path_wd, 0) {
+ println!("rm '{}'", path_wd.bright_yellow());
+ } else {
+ // Even untracked files are processed in force mode
+ println!("rm '{}'", path_wd.bright_yellow());
+ }
+ } else if index.tracked(&path_wd, 0) {
+ // Normal mode - only show if tracked (matches actual execution logic)
+ println!("rm '{}'", path_wd.bright_yellow());
+ }
+ }
+ }
+ return Ok(());
+ }
+
for path_str in args.pathspec.iter() {
let path = PathBuf::from(path_str);
let path_wd = path.to_workdir().to_string_or_panic();
diff --git a/libra/tests/command/remove_test.rs b/libra/tests/command/remove_test.rs
index c2d2d5889..76d0bf57a 100644
--- a/libra/tests/command/remove_test.rs
+++ b/libra/tests/command/remove_test.rs
@@ -45,6 +45,7 @@ async fn test_remove_single_file() {
cached: false,
recursive: false,
force: false,
+ dry_run: false,
};
remove::execute(args).unwrap();
@@ -110,6 +111,7 @@ async fn test_remove_cached() {
cached: true,
recursive: false,
force: false,
+ dry_run: false,
};
remove::execute(args).unwrap();
@@ -164,6 +166,7 @@ async fn test_remove_directory_recursive() {
cached: false,
recursive: true,
force: false,
+ dry_run: false,
};
remove::execute(args).unwrap();
@@ -236,6 +239,7 @@ async fn test_remove_directory_without_recursive() {
cached: false,
recursive: false,
force: false,
+ dry_run: false,
};
assert!(
remove::execute(args).is_err(),
@@ -271,6 +275,7 @@ async fn test_remove_untracked_file() {
cached: false,
recursive: false,
force: false,
+ dry_run: false,
};
let result = remove::execute(args);
assert!(
@@ -318,6 +323,7 @@ async fn test_remove_modified_file() {
cached: false,
recursive: false,
force: false,
+ dry_run: false,
};
remove::execute(args).unwrap();
@@ -385,6 +391,7 @@ async fn test_remove_multiple_files() {
cached: false,
recursive: false,
force: false,
+ dry_run: false,
};
remove::execute(args).unwrap();
// Verify the specified files were removed
@@ -392,3 +399,165 @@ async fn test_remove_multiple_files() {
assert!(file2.exists(), "File 2 should still exist");
assert!(!file3.exists(), "File 3 should be removed");
}
+
+#[tokio::test]
+#[serial]
+/// Tests the --dry-run flag which shows what would be removed without actually removing anything
+async fn test_remove_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 multiple files
+ let file1 = create_file("file1.txt", "File 1 content");
+ let file2 = create_file("file2.txt", "File 2 content");
+ let file3 = create_file("subdir/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,
+ dry_run: false,
+ ignore_errors: false,
+ })
+ .await;
+
+ // Make sure all files exist before dry-run
+ assert!(file1.exists(), "File 1 should exist before dry-run");
+ assert!(file2.exists(), "File 2 should exist before dry-run");
+ assert!(file3.exists(), "File 3 should exist before dry-run");
+
+ // Run rm with --dry-run flag
+ let args = RemoveArgs {
+ pathspec: vec![String::from("file1.txt"), String::from("file2.txt")],
+ cached: false,
+ recursive: false,
+ force: false,
+ dry_run: true,
+ };
+ remove::execute(args).unwrap();
+
+ // Verify that no files were actually removed
+ assert!(file1.exists(), "File 1 should still exist after dry-run");
+ assert!(file2.exists(), "File 2 should still exist after dry-run");
+ assert!(file3.exists(), "File 3 should still exist after dry-run");
+
+ // Verify files are still in the index by checking they don't appear as deleted
+ let changes = changes_to_be_staged();
+ assert!(
+ !changes
+ .deleted
+ .iter()
+ .any(|x| x.to_str().unwrap() == "file1.txt"),
+ "File 1 should not appear as deleted"
+ );
+ assert!(
+ !changes
+ .deleted
+ .iter()
+ .any(|x| x.to_str().unwrap() == "file2.txt"),
+ "File 2 should not appear as deleted"
+ );
+}
+
+#[tokio::test]
+#[serial]
+/// Tests --dry-run with --cached flag
+async fn test_remove_dry_run_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,
+ dry_run: false,
+ ignore_errors: false,
+ })
+ .await;
+
+ // Run rm with --dry-run and --cached flags
+ let args = RemoveArgs {
+ pathspec: vec![String::from("test_file.txt")],
+ cached: true,
+ recursive: false,
+ force: false,
+ dry_run: true,
+ };
+ remove::execute(args).unwrap();
+
+ // Verify the file still exists in both filesystem and index
+ assert!(file_path.exists(), "File should still exist in filesystem");
+
+ // Verify file doesn't appear as deleted in changes
+ let changes = changes_to_be_staged();
+ assert!(
+ !changes
+ .deleted
+ .iter()
+ .any(|x| x.to_str().unwrap() == "test_file.txt"),
+ "File should not appear as deleted"
+ );
+}
+
+#[tokio::test]
+#[serial]
+/// Tests --dry-run with recursive directory removal
+async fn test_remove_dry_run_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,
+ dry_run: false,
+ ignore_errors: false,
+ })
+ .await;
+
+ // Run rm with --dry-run and --recursive flags
+ let args = RemoveArgs {
+ pathspec: vec![String::from("test_dir")],
+ cached: false,
+ recursive: true,
+ force: false,
+ dry_run: true,
+ };
+ remove::execute(args).unwrap();
+
+ // Verify that no files or directories were actually removed
+ assert!(file1.exists(), "File 1 should still exist after dry-run");
+ assert!(file2.exists(), "File 2 should still exist after dry-run");
+ assert!(file3.exists(), "File 3 should still exist after dry-run");
+ assert!(PathBuf::from("test_dir").exists(), "Directory should still exist");
+ assert!(PathBuf::from("test_dir/subdir").exists(), "Subdirectory should still exist");
+
+ // Verify files are still tracked by checking they don't appear as deleted
+ let changes = changes_to_be_staged();
+ assert!(
+ !changes
+ .deleted
+ .iter()
+ .any(|x| x.to_str().unwrap().starts_with("test_dir/")),
+ "No files in test_dir should appear as deleted"
+ );
+}