From 2167bfce17c96c3d75e5b870644d6e4138641457 Mon Sep 17 00:00:00 2001 From: XL-Zhao-23 <1438502954@qq.com> Date: Wed, 30 Jul 2025 10:26:49 +0800 Subject: [PATCH 1/4] =?UTF-8?q?1.=20=E5=A2=9E=E5=8A=A0=20remove=20?= =?UTF-8?q?=E7=9A=84=20-f=20=E5=8A=9F=E8=83=BD=202.=20=E5=AE=8C=E5=96=84?= =?UTF-8?q?=20remove=20=E7=9A=84=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libra/Cargo.toml | 2 + libra/src/command/remove.rs | 50 +++-- libra/tests/command/remove_test.rs | 312 +++++++++++++++++++++++++++++ 3 files changed, 352 insertions(+), 12 deletions(-) diff --git a/libra/Cargo.toml b/libra/Cargo.toml index 400e9a115..d385fadec 100644 --- a/libra/Cargo.toml +++ b/libra/Cargo.toml @@ -52,6 +52,8 @@ tracing-subscriber = { workspace = true } wax = { workspace = true } url = { workspace = true } ignore = { workspace = true } +tempfile.workspace = true +serial_test.workspace = true [target.'cfg(unix)'.dependencies] # only on Unix pager = { workspace = true } diff --git a/libra/src/command/remove.rs b/libra/src/command/remove.rs index fea07ccd7..4d954090a 100644 --- a/libra/src/command/remove.rs +++ b/libra/src/command/remove.rs @@ -13,13 +13,16 @@ use mercury::internal::index::Index; #[derive(Parser, Debug)] pub struct RemoveArgs { /// file or dir to remove - pathspec: Vec, + pub pathspec: Vec, /// whether to remove from index #[clap(long)] - cached: bool, + pub cached: bool, /// indicate recursive remove dir #[clap(short, long)] - recursive: bool, + pub recursive: bool, + /// force removal, skip validation + #[clap(short, long)] + pub force: bool, } pub fn execute(args: RemoveArgs) -> Result<(), GitError> { @@ -28,11 +31,13 @@ pub fn execute(args: RemoveArgs) -> Result<(), GitError> { } let idx_file = path::index(); let mut index = Index::load(&idx_file)?; - // check if pathspec is all in index - if !validate_pathspec(&args.pathspec, &index) { + + // check if pathspec is all in index (skip if force is enabled) + if !args.force && !validate_pathspec(&args.pathspec, &index) { return Ok(()); } - let dirs = get_dirs(&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", @@ -44,6 +49,7 @@ pub fn execute(args: RemoveArgs) -> Result<(), GitError> { 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 let removed = index.remove_dir_files(&path_wd); @@ -56,8 +62,20 @@ pub fn execute(args: RemoveArgs) -> Result<(), GitError> { } } else { // file - index.remove(&path_wd, 0); - println!("rm '{}'", path_wd.bright_green()); + if args.force { + // In force mode, remove from index if tracked, otherwise just delete from filesystem + if index.tracked(&path_wd, 0) { + index.remove(&path_wd, 0); + println!("rm '{}'", path_wd.bright_green()); + } else { + println!("rm '{}'", path_wd.bright_yellow()); + } + } else { + // Normal mode - only remove if tracked + index.remove(&path_wd, 0); + println!("rm '{}'", path_wd.bright_green()); + } + if !args.cached { fs::remove_file(&path)?; } @@ -90,14 +108,22 @@ fn validate_pathspec(pathspec: &[String], index: &Index) -> bool { } /// run after `validate_pathspec` -fn get_dirs(pathspec: &[String], index: &Index) -> Vec { +fn get_dirs(pathspec: &[String], index: &Index, force: bool) -> Vec { let mut dirs = Vec::new(); for path_str in pathspec.iter() { let path = PathBuf::from(path_str); let path_wd = path.to_workdir().to_string_or_panic(); - // valid but not tracked, means a dir - if !index.tracked(&path_wd, 0) { - dirs.push(path_str.clone()); + + if force { + // In force mode, check if the path exists and is a directory + if path.exists() && path.is_dir() { + dirs.push(path_str.clone()); + } + } else { + // valid but not tracked, means a dir + if !index.tracked(&path_wd, 0) { + dirs.push(path_str.clone()); + } } } dirs diff --git a/libra/tests/command/remove_test.rs b/libra/tests/command/remove_test.rs index 8b1378917..b6a6cb4dd 100644 --- a/libra/tests/command/remove_test.rs +++ b/libra/tests/command/remove_test.rs @@ -1 +1,313 @@ +use libra::command::{add, commit, init, remove}; +use libra::utils::test; +use serial_test::serial; +use tempfile::tempdir; +use std::fs; +use std::path::Path; +async fn test_remove_file() { + println!("\n\x1b[1mTest remove file functionality.\x1b[0m"); + + // Create a test file + test::ensure_file("test_file.txt", Some("test content")); + + // Add file to index + let add_args = add::AddArgs { + all: false, + update: false, + verbose: false, + pathspec: vec!["test_file.txt".to_string()], + dry_run: false, + refresh: false, + ignore_errors: false, + }; + add::execute(add_args).await; + + // Remove file from index and filesystem + let remove_args = remove::RemoveArgs { + pathspec: vec!["test_file.txt".to_string()], + cached: false, + recursive: false, + force: false, + }; + let _ = remove::execute(remove_args); + + // Verify file is removed from filesystem + assert!(!Path::new("test_file.txt").exists()); +} + +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 { + all: false, + update: false, + verbose: false, + pathspec: vec!["cached_file.txt".to_string()], + dry_run: false, + refresh: false, + ignore_errors: false, + }; + add::execute(add_args).await; + + // Remove file from index only (keep in filesystem) + let remove_args = remove::RemoveArgs { + pathspec: vec!["cached_file.txt".to_string()], + cached: true, + recursive: false, + force: false, + }; + let _ = remove::execute(remove_args); + + // 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"); + + // 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")); + + // Add all files to index + let add_args = add::AddArgs { + all: true, + update: 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()], + cached: false, + recursive: true, + force: false, + }; + let _ = remove::execute(remove_args); + + // Verify directory is removed + assert!(!Path::new("test_dir").exists()); +} + +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, + update: false, + verbose: false, + pathspec: vec![], + dry_run: false, + refresh: false, + ignore_errors: false, + }; + add::execute(add_args).await; + + // Try to remove directory without recursive flag (should fail) + let remove_args = remove::RemoveArgs { + pathspec: vec!["test_dir2".to_string()], + cached: false, + recursive: false, + force: false, + }; + let _ = remove::execute(remove_args); + + // Directory should still exist because recursive flag was not used + assert!(Path::new("test_dir2").exists()); +} + +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()], + cached: false, + recursive: false, + force: true, + }; + let _ = remove::execute(remove_args); + + // Verify file is removed + assert!(!Path::new("untracked_file.txt").exists()); +} + +async fn test_no_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()], + 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()); +} + +async fn test_remove_multiple_files() { + println!("\n\x1b[1mTest remove multiple files.\x1b[0m"); + + // 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, + update: 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); + + // 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"); + + // Try to remove with empty pathspec (should fail) + let remove_args = remove::RemoveArgs { + pathspec: vec![], + 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"); +} From 96b12fbc580129f3105adb4540d0639043c908a8 Mon Sep 17 00:00:00 2001 From: XL-Zhao-23 <1438502954@qq.com> Date: Wed, 30 Jul 2025 10:32:41 +0800 Subject: [PATCH 2/4] =?UTF-8?q?1.=20=E4=BF=AE=E5=A4=8D=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libra/tests/command/remove_test.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libra/tests/command/remove_test.rs b/libra/tests/command/remove_test.rs index b6a6cb4dd..8159db518 100644 --- a/libra/tests/command/remove_test.rs +++ b/libra/tests/command/remove_test.rs @@ -297,7 +297,7 @@ async fn test_remove_command() { }; commit::execute(commit_args).await; - Run all tests + // Run all tests test_remove_file().await; test_remove_cached_file().await; test_remove_directory().await; From d768c104be82d8268e3e5cc204c8fec6ca648203 Mon Sep 17 00:00:00 2001 From: XL-Zhao-23 <1438502954@qq.com> Date: Wed, 30 Jul 2025 11:13:24 +0800 Subject: [PATCH 3/4] =?UTF-8?q?1.=20=E4=BF=AE=E5=A4=8D=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E7=9A=84=E6=B3=A8=E9=87=8A=202.=20=E4=BF=AE=E5=A4=8D=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E7=9A=84=E6=A0=BC=E5=BC=8F=203.=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E9=80=89=E9=A1=B9=E7=9A=84=E6=8F=8F=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aria/contents/docs/libra/command/rm/index.mdx | 3 +++ libra/Cargo.toml | 4 ++-- libra/tests/command/remove_test.rs | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/aria/contents/docs/libra/command/rm/index.mdx b/aria/contents/docs/libra/command/rm/index.mdx index 32ec0fa10..69694fbe8 100644 --- a/aria/contents/docs/libra/command/rm/index.mdx +++ b/aria/contents/docs/libra/command/rm/index.mdx @@ -26,3 +26,6 @@ that.) - `-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. diff --git a/libra/Cargo.toml b/libra/Cargo.toml index d385fadec..c373376d7 100644 --- a/libra/Cargo.toml +++ b/libra/Cargo.toml @@ -52,8 +52,8 @@ tracing-subscriber = { workspace = true } wax = { workspace = true } url = { workspace = true } ignore = { workspace = true } -tempfile.workspace = true -serial_test.workspace = true +tempfile = { workspace = true } +serial_test = { workspace = true } [target.'cfg(unix)'.dependencies] # only on Unix pager = { workspace = true } diff --git a/libra/tests/command/remove_test.rs b/libra/tests/command/remove_test.rs index 8159db518..db8153416 100644 --- a/libra/tests/command/remove_test.rs +++ b/libra/tests/command/remove_test.rs @@ -152,12 +152,12 @@ async fn test_force_remove_untracked_file() { } async fn test_no_force_remove_untracked_file() { - println!("\n\x1b[1mTest force remove untracked file.\x1b[0m"); + println!("\n\x1b[1mTest remove untracked file without force.\x1b[0m"); // Create an untracked file test::ensure_file("untracked_file.txt", Some("untracked content")); - // Force remove untracked file + // remove untracked file without force let remove_args = remove::RemoveArgs { pathspec: vec!["untracked_file.txt".to_string()], cached: false, From ec5f982899dcda660942c2b6f0af7600fde13ff4 Mon Sep 17 00:00:00 2001 From: XL-Zhao-23 <1438502954@qq.com> Date: Wed, 30 Jul 2025 11:46:22 +0800 Subject: [PATCH 4/4] =?UTF-8?q?1.=20=E5=8E=BB=E9=99=A4=E6=9C=AA=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E7=9A=84=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libra/tests/command/remove_test.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/libra/tests/command/remove_test.rs b/libra/tests/command/remove_test.rs index db8153416..c1649165e 100644 --- a/libra/tests/command/remove_test.rs +++ b/libra/tests/command/remove_test.rs @@ -2,7 +2,6 @@ use libra::command::{add, commit, init, remove}; use libra::utils::test; use serial_test::serial; use tempfile::tempdir; -use std::fs; use std::path::Path; async fn test_remove_file() {