use std::collections::HashSet;
use crate::core::versions::MinOxenVersion;
use crate::error::OxenError;
use crate::model::LocalRepository;
use crate::opts::{GlobOpts, RmOpts};
use crate::{core, util};
use std::path::PathBuf;
pub fn rm(repo: &LocalRepository, opts: &RmOpts) -> Result<(), OxenError> {
log::debug!("Rm with opts: {opts:?}");
let path = &opts.path;
let glob_opts = GlobOpts {
paths: vec![path.to_path_buf()],
staged_db: opts.staged,
merkle_tree: !opts.staged,
working_dir: false,
walk_dirs: false,
};
let expanded_paths = util::glob::parse_glob_paths(&glob_opts, Some(repo))?;
p_rm(&expanded_paths, repo, opts)?;
Ok(())
}
fn p_rm(paths: &HashSet<PathBuf>, repo: &LocalRepository, opts: &RmOpts) -> Result<(), OxenError> {
match repo.min_version() {
MinOxenVersion::V0_10_0 => panic!("v0.10.0 no longer supported"),
_ => {
log::debug!("Version found: V0_19_0");
core::v_latest::rm::rm(paths, repo, opts)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use crate::test;
use std::path::Path;
use std::path::PathBuf;
use crate::api;
use crate::command;
use crate::constants::DEFAULT_BRANCH_NAME;
use crate::constants::DEFAULT_REMOTE_NAME;
use crate::constants::OXEN_HIDDEN_DIR;
use crate::error::OxenError;
use crate::model::NewCommitBody;
use crate::model::StagedEntryStatus;
use crate::opts::RestoreOpts;
use crate::opts::RmOpts;
use crate::repositories;
use crate::util;
#[tokio::test]
async fn test_rm_directory_restore_directory() -> Result<(), OxenError> {
test::run_training_data_repo_test_fully_committed_async(|repo| async move {
let rm_dir = PathBuf::from("train");
let full_path = repo.path.join(&rm_dir);
let num_files = util::fs::rcount_files_in_dir(&full_path);
let opts = RmOpts {
path: rm_dir.to_owned(),
recursive: true,
staged: false,
};
repositories::rm(&repo, &opts)?;
let status = repositories::status(&repo)?;
status.print();
assert_eq!(num_files, status.staged_files.len());
for (path, entry) in status.staged_files.iter() {
if path != Path::new("") {
assert_eq!(entry.status, StagedEntryStatus::Removed);
}
}
assert!(!full_path.exists());
let opts = RestoreOpts::from_staged_path(&rm_dir);
repositories::restore::restore(&repo, opts).await?;
let status = repositories::status(&repo)?;
status.print();
assert_eq!(0, status.staged_files.len());
assert_eq!(1, status.removed_files.len());
let opts = RestoreOpts::from_path(&rm_dir);
repositories::restore::restore(&repo, opts).await?;
let status = repositories::status(&repo)?;
status.print();
let num_restored = util::fs::rcount_files_in_dir(&full_path);
assert_eq!(num_restored, num_files);
Ok(())
})
.await
}
#[tokio::test]
async fn test_rm_r_dir_at_root() -> Result<(), OxenError> {
test::run_empty_data_repo_test_no_commits_async(|mut repo| async move {
let gemma_dir = repo.path.join("gemma-3");
util::fs::create_dir_all(&gemma_dir)?;
let chat_file = gemma_dir.join("chat.py");
util::fs::write(chat_file, "print('Hello, Gemma!')")?;
let mistral_dir = repo.path.join("mistral-small-3-1");
util::fs::create_dir_all(&mistral_dir)?;
let chat_file = mistral_dir.join("chat.py");
util::fs::write(chat_file, "print('Hello, Mistral!')")?;
let phi_dir = repo.path.join("phi-4");
util::fs::create_dir_all(&phi_dir)?;
let chat_file = phi_dir.join("chat.py");
util::fs::write(chat_file, "print('Hello, Phi!')")?;
let phi_multimodal_dir = repo.path.join("phi-4-multimodal");
util::fs::create_dir_all(&phi_multimodal_dir)?;
let chat_file = phi_multimodal_dir.join("chat.py");
util::fs::write(chat_file, "print('Hello, Phi Multimodal!')")?;
let ocr_bench_dir = phi_multimodal_dir.join("eval");
util::fs::create_dir_all(&ocr_bench_dir)?;
let ocr_file = ocr_bench_dir.join("ocr-bench-v2.py");
util::fs::write(ocr_file, "print('Hello, Phi OCR Bench!')")?;
let readme_file = repo.path.join("README.md");
util::fs::write(readme_file, "Hello, world!")?;
repositories::add(&repo, &repo.path).await?;
repositories::commit(&repo, "Adding initial files")?;
let remote_repo = test::create_remote_repo(&repo).await?;
let remote = test::repo_remote_url_from(&repo.dirname());
command::config::set_remote(&mut repo, DEFAULT_REMOTE_NAME, &remote)?;
repositories::push(&repo).await?;
let root_entries =
api::client::dir::list(&remote_repo, DEFAULT_BRANCH_NAME, Path::new(""), 1, 10)
.await?;
assert_eq!(root_entries.entries.len(), 5);
let workspace_id = "my_workspace";
let workspace =
api::client::workspaces::create(&remote_repo, DEFAULT_BRANCH_NAME, &workspace_id)
.await?;
assert_eq!(workspace.id, workspace_id);
let file_to_post = test::test_csv_file_with_name("emojis.csv");
let directory_name = "phi-4";
let result = api::client::workspaces::files::upload_single_file(
&remote_repo,
&workspace_id,
directory_name,
file_to_post,
)
.await;
assert!(result.is_ok());
let body = NewCommitBody {
message: "Add emojis data frame".to_string(),
author: "Test User".to_string(),
email: "test@oxen.ai".to_string(),
};
api::client::workspaces::commit(&remote_repo, DEFAULT_BRANCH_NAME, workspace_id, &body)
.await?;
let root_entries =
api::client::dir::list(&remote_repo, DEFAULT_BRANCH_NAME, Path::new(""), 1, 10)
.await?;
assert_eq!(root_entries.entries.len(), 5);
let cloned_remote_repo = remote_repo.clone();
test::run_empty_dir_test_async(|new_repo_dir| async move {
let new_repo_dir = new_repo_dir.join("new_repo");
let cloned_repo =
repositories::clone_url(&cloned_remote_repo.remote.url, &new_repo_dir).await?;
let rm_opts = RmOpts {
path: PathBuf::from("phi-4"),
recursive: true,
..Default::default()
};
repositories::rm(&cloned_repo, &rm_opts)?;
repositories::commit(&cloned_repo, "Removing phi-4")?;
repositories::push(&cloned_repo).await?;
let root_entries =
api::client::dir::list(&remote_repo, DEFAULT_BRANCH_NAME, Path::new(""), 1, 10)
.await?;
assert_eq!(root_entries.entries.len(), 4);
Ok(())
})
.await?;
Ok(())
})
.await
}
#[tokio::test]
async fn test_rm_sub_directory() -> Result<(), OxenError> {
test::run_empty_data_repo_test_no_commits_async(|repo| async move {
let images_dir = repo.path.join("images").join("cats");
util::fs::create_dir_all(&images_dir)?;
for i in 1..=3 {
let test_file = test::test_img_file_with_name(&format!("cat_{i}.jpg"));
let repo_filepath = images_dir.join(test_file.file_name().unwrap());
util::fs::copy(&test_file, &repo_filepath)?;
}
repositories::add(&repo, &images_dir).await?;
repositories::commit(&repo, "Adding initial cat images")?;
let branch_name = "remove-data";
repositories::branches::create_checkout(&repo, branch_name)?;
for i in 1..=3 {
let repo_filepath = images_dir.join(format!("cat_{i}.jpg"));
util::fs::remove_file(&repo_filepath)?;
}
let rm_opts = RmOpts {
path: PathBuf::from("images"),
recursive: true,
..Default::default()
};
repositories::rm(&repo, &rm_opts)?;
let commit = repositories::commit(&repo, "Removing cat images")?;
for i in 1..=3 {
let repo_filepath = images_dir.join(format!("cat_{i}.jpg"));
assert!(!repo_filepath.exists())
}
let tree = repositories::tree::get_root_with_children(&repo, &commit)?.unwrap();
let (files, dirs) = repositories::tree::list_files_and_dirs(&tree)?;
assert_eq!(files.len(), 0);
for dir in dirs.iter() {
println!("dir: {dir:?}");
}
assert_eq!(dirs.len(), 0);
Ok(())
})
.await
}
#[tokio::test]
async fn test_rm_multi_level_directory() -> Result<(), OxenError> {
test::run_empty_data_repo_test_no_commits_async(|repo| async move {
let images_dir = repo.path.join("images").join("cats");
util::fs::create_dir_all(&images_dir)?;
for i in 1..=3 {
let sub_dir = images_dir.join(format!("subdir{i}_level_1"));
util::fs::create_dir_all(&sub_dir)?;
}
for i in 1..=2 {
let sub_dir = images_dir
.join(format!("subdir{i}_level_1"))
.join(format!("subdir{i}_level_2"));
util::fs::create_dir_all(&sub_dir)?;
}
for i in 1..=1 {
let sub_dir = images_dir
.join(format!("subdir{i}_level_1"))
.join(format!("subdir{i}_level_2"))
.join(format!("subdir{i}_level_3"));
util::fs::create_dir_all(&sub_dir)?;
}
for i in 1..=3 {
let test_file = test::test_img_file_with_name(&format!("cat_{i}.jpg"));
let repo_filepath = images_dir.join(test_file.file_name().unwrap());
util::fs::copy(&test_file, &repo_filepath)?;
}
for j in 1..=3 {
for i in 1..=3 {
let test_file = test::test_img_file_with_name(&format!("cat_{i}.jpg"));
let repo_filepath = images_dir
.join(format!("subdir{j}_level_1"))
.join(test_file.file_name().unwrap());
util::fs::copy(&test_file, &repo_filepath)?;
}
}
for j in 1..=2 {
for i in 1..=3 {
let test_file = test::test_img_file_with_name(&format!("cat_{i}.jpg"));
let repo_filepath = images_dir
.join(format!("subdir{j}_level_1"))
.join(format!("subdir{j}_level_2"))
.join(test_file.file_name().unwrap());
util::fs::copy(&test_file, &repo_filepath)?;
}
}
for j in 1..=1 {
for i in 1..=3 {
let test_file = test::test_img_file_with_name(&format!("cat_{i}.jpg"));
let repo_filepath = images_dir
.join(format!("subdir{j}_level_1"))
.join(format!("subdir{j}_level_2"))
.join(format!("subdir{j}_level_3"))
.join(test_file.file_name().unwrap());
util::fs::copy(&test_file, &repo_filepath)?;
}
}
repositories::add(&repo, &images_dir).await?;
let status = repositories::status(&repo)?;
status.print();
assert_eq!(status.staged_dirs.len(), 7);
assert_eq!(status.staged_files.len(), 21);
repositories::commit(&repo, "Adding initial cat images")?;
let branch_name = "remove-data";
repositories::branches::create_checkout(&repo, branch_name)?;
let rm_opts = RmOpts {
path: PathBuf::from("images"),
recursive: true,
..Default::default()
};
repositories::rm(&repo, &rm_opts)?;
let commit = repositories::commit(&repo, "Removing cat images and sub_directories")?;
for i in 1..=3 {
let repo_filepath = images_dir.join(format!("cat_{i}.jpg"));
assert!(!repo_filepath.exists())
}
for j in 1..=3 {
for i in 1..=3 {
let repo_filepath = images_dir
.join(format!("subdir{j}_level_1"))
.join(format!("cat_{i}.jpg"));
assert!(!repo_filepath.exists())
}
}
for j in 1..=2 {
for i in 1..=3 {
let repo_filepath = images_dir
.join(format!("subdir{j}_level_1"))
.join(format!("subdir{j}_level_2"))
.join(format!("cat_{i}.jpg"));
assert!(!repo_filepath.exists())
}
}
for j in 1..=1 {
for i in 1..=3 {
let repo_filepath = images_dir
.join(format!("subdir{j}_level_1"))
.join(format!("subdir{j}_level_2"))
.join(format!("subdir{j}_level_3"))
.join(format!("cat_{i}.jpg"));
assert!(!repo_filepath.exists())
}
}
let tree = repositories::tree::get_root_with_children(&repo, &commit)?.unwrap();
let (files, dirs) = repositories::tree::list_files_and_dirs(&tree)?;
assert_eq!(files.len(), 0);
assert_eq!(dirs.len(), 0);
let dirs = tree.list_dir_paths()?;
println!("list_dir_paths got {} dirs", dirs.len());
for dir in dirs.iter() {
println!("dir: {dir:?}");
}
assert_eq!(dirs.len(), 1);
Ok(())
})
.await
}
#[tokio::test]
async fn test_rm_one_file_in_dir() -> Result<(), OxenError> {
test::run_empty_data_repo_test_no_commits_async(|repo| async move {
let images_dir = repo.path.join("images");
util::fs::create_dir_all(&images_dir)?;
for i in 1..=3 {
let test_file = test::test_img_file_with_name(&format!("cat_{i}.jpg"));
let repo_filepath = images_dir.join(test_file.file_name().unwrap());
util::fs::copy(&test_file, &repo_filepath)?;
}
repositories::add(&repo, &images_dir).await?;
repositories::commit(&repo, "Adding initial cat images")?;
for i in 1..=4 {
let test_file = test::test_img_file_with_name(&format!("dog_{i}.jpg"));
let repo_filepath = images_dir.join(test_file.file_name().unwrap());
util::fs::copy(&test_file, &repo_filepath)?;
}
repositories::add(&repo, &images_dir).await?;
repositories::commit(&repo, "Adding initial dog images")?;
let branch_name = "modify-data";
repositories::branches::create_checkout(&repo, branch_name)?;
for i in 1..=3 {
let repo_filepath = images_dir.join(format!("cat_{i}.jpg"));
let dims = 96;
util::image::resize_and_save(&repo_filepath, &repo_filepath, dims)?;
}
repositories::add(&repo, &images_dir).await?;
repositories::commit(&repo, "Resized all the cats")?;
let repo_filepath = PathBuf::from("images").join("dog_1.jpg");
let rm_opts = RmOpts::from_path(repo_filepath);
repositories::rm(&repo, &rm_opts)?;
let _commit = repositories::commit(&repo, "Removing dog")?;
let test_file = test::test_img_file_with_name("dwight_vince.jpeg");
let repo_filepath = images_dir.join(test_file.file_name().unwrap());
util::fs::copy(&test_file, repo_filepath)?;
repositories::add(&repo, &images_dir).await?;
let commit = repositories::commit(&repo, "Adding dwight and vince")?;
let tree = repositories::tree::get_root_with_children(&repo, &commit)?.unwrap();
let (files, dirs) = repositories::tree::list_files_and_dirs(&tree)?;
for dir in dirs.iter() {
log::debug!("dir: {dir:?}");
}
for file in files.iter() {
log::debug!("file: {file:?}");
}
assert_eq!(files.len(), 7);
assert_eq!(dirs.len(), 1);
Ok(())
})
.await
}
#[tokio::test]
async fn test_wildcard_remove_nested_nlp_dir() -> Result<(), OxenError> {
test::run_training_data_repo_test_no_commits_async(|repo| async move {
let dir = Path::new("nlp");
let repo_dir = repo.path.join(dir);
repositories::add(&repo, repo_dir).await?;
let status = repositories::status(&repo)?;
status.print();
assert_eq!(status.staged_dirs.len(), 1);
assert_eq!(status.staged_files.len(), 2);
repositories::commit(&repo, "Adding nlp dir")?;
let dir = Path::new("nlp");
let repo_nlp_dir = repo.path.join(dir);
std::fs::remove_dir_all(repo_nlp_dir)?;
let status = repositories::status(&repo)?;
assert_eq!(status.removed_files.len(), 1);
assert_eq!(status.staged_files.len(), 0);
status.print();
repositories::add(&repo, "nlp/*").await?;
status.print();
let status = repositories::status(&repo)?;
status.print();
assert_eq!(status.staged_dirs.len(), 1);
assert_eq!(status.staged_files.len(), 2);
Ok(())
})
.await
}
#[tokio::test]
async fn test_wildcard_rm_deleted_and_present() -> Result<(), OxenError> {
test::run_empty_data_repo_test_no_commits_async(|repo| async move {
let images_dir = repo.path.join("images");
util::fs::create_dir_all(&images_dir)?;
for i in 1..=3 {
let test_file = test::test_img_file_with_name(&format!("cat_{i}.jpg"));
let repo_filepath = images_dir.join(test_file.file_name().unwrap());
util::fs::copy(&test_file, &repo_filepath)?;
}
repositories::add(&repo, &images_dir).await?;
repositories::commit(&repo, "Adding initial cat images")?;
for i in 1..=4 {
let test_file = test::test_img_file_with_name(&format!("dog_{i}.jpg"));
let repo_filepath = images_dir.join(test_file.file_name().unwrap());
util::fs::copy(&test_file, &repo_filepath)?;
}
repositories::add(&repo, &images_dir).await?;
repositories::commit(&repo, "Adding initial dog images")?;
std::fs::remove_file(repo.path.join("images").join("cat_1.jpg"))?;
std::fs::remove_file(repo.path.join("images").join("cat_2.jpg"))?;
std::fs::remove_file(repo.path.join("images").join("dog_1.jpg"))?;
let status = repositories::status(&repo)?;
status.print();
assert_eq!(status.removed_files.len(), 3);
assert_eq!(status.staged_files.len(), 0);
let rm_opts = RmOpts {
path: PathBuf::from("images/*"),
..Default::default()
};
repositories::rm(&repo, &rm_opts)?;
let status = repositories::status(&repo)?;
status.print();
assert_eq!(status.staged_files.len(), 7);
assert_eq!(status.removed_files.len(), 0);
let rm_opts = RmOpts {
path: PathBuf::from("images/*"),
staged: true,
..Default::default()
};
repositories::rm(&repo, &rm_opts)?;
let status = repositories::status(&repo)?;
log::debug!("status: {status:?}");
status.print();
assert_eq!(status.staged_files.len(), 0);
assert_eq!(status.removed_files.len(), 7);
Ok(())
})
.await
}
#[tokio::test]
async fn test_add_and_rm_file_in_dir() -> Result<(), OxenError> {
test::run_select_data_repo_test_no_commits_async("README", |repo| async move {
let path = Path::new("dir").join("new_file.txt");
util::fs::write_to_path(repo.path.join(&path), "this is a new file")?;
repositories::add(&repo, &path).await?;
repositories::commit(&repo, "first_commit")?;
let opts = RmOpts::from_path(&path);
repositories::rm(&repo, &opts)?;
repositories::commit(&repo, "commit_message")?;
let result = repositories::add(&repo, &path).await;
assert!(result.is_err());
Ok(())
})
.await
}
#[tokio::test]
async fn test_rm_staged_file() -> Result<(), OxenError> {
test::run_select_data_repo_test_no_commits_async("README", |repo| async move {
let path = Path::new("README.md");
repositories::add(&repo, repo.path.join(path)).await?;
let status = repositories::status(&repo)?;
assert_eq!(status.staged_files.len(), 1);
assert!(status.staged_files.contains_key(path));
let opts = RmOpts::from_staged_path(path);
repositories::rm(&repo, &opts)?;
let status = repositories::status(&repo)?;
log::debug!("status: {status:?}");
assert_eq!(status.staged_files.len(), 0);
Ok(())
})
.await
}
#[tokio::test]
async fn test_rm_staged_dir_without_recursive_flag_should_be_error() -> Result<(), OxenError> {
test::run_select_data_repo_test_no_commits_async("train", |repo| async move {
let path = Path::new("train");
repositories::add(&repo, repo.path.join(path)).await?;
let status = repositories::status(&repo)?;
status.print();
assert_eq!(status.staged_dirs.len(), 1);
let opts = RmOpts {
path: path.to_path_buf(),
staged: true,
recursive: false, };
let result = repositories::rm(&repo, &opts);
assert!(result.is_err());
Ok(())
})
.await
}
#[tokio::test]
async fn test_rm_staged_annotations_train_dir() -> Result<(), OxenError> {
test::run_select_data_repo_test_no_commits_async("annotations", |repo| async move {
let path = Path::new("annotations").join("train");
repositories::add(&repo, repo.path.join(&path)).await?;
let status = repositories::status(&repo)?;
status.print();
assert_eq!(status.staged_dirs.len(), 1);
let opts = RmOpts {
path: path.to_path_buf(),
staged: true,
recursive: true, };
repositories::rm(&repo, &opts)?;
let status = repositories::status(&repo)?;
status.print();
assert_eq!(status.staged_dirs.len(), 0);
assert_eq!(status.staged_files.len(), 0);
Ok(())
})
.await
}
#[tokio::test]
async fn test_rm_staged_train_dir() -> Result<(), OxenError> {
test::run_select_data_repo_test_no_commits_async("train", |repo| async move {
let path = Path::new("train");
repositories::add(&repo, repo.path.join(path)).await?;
let status = repositories::status(&repo)?;
status.print();
assert_eq!(status.staged_dirs.len(), 1);
let opts = RmOpts {
path: path.to_path_buf(),
staged: true,
recursive: true, };
repositories::rm(&repo, &opts)?;
let status = repositories::status(&repo)?;
status.print();
assert_eq!(status.staged_dirs.len(), 0);
assert_eq!(status.staged_files.len(), 0);
Ok(())
})
.await
}
#[tokio::test]
async fn test_rm_staged_dir_with_slash() -> Result<(), OxenError> {
test::run_select_data_repo_test_no_commits_async("train", |repo| async move {
let path = Path::new("train/");
repositories::add(&repo, repo.path.join(path)).await?;
let status = repositories::status(&repo)?;
assert_eq!(status.staged_dirs.len(), 1);
let opts = RmOpts {
path: path.to_path_buf(),
staged: true,
recursive: true, };
let result = repositories::rm(&repo, &opts);
assert!(result.is_ok());
let status = repositories::status(&repo)?;
status.print();
assert_eq!(status.staged_dirs.len(), 0);
assert_eq!(status.staged_files.len(), 0);
Ok(())
})
.await
}
#[tokio::test]
async fn test_staged_rm_file() -> Result<(), OxenError> {
test::run_select_data_repo_test_committed_async("README", |repo| async move {
let path = Path::new("README.md");
let opts = RmOpts::from_path(path);
repositories::rm(&repo, &opts)?;
let status = repositories::status(&repo)?;
status.print();
assert_eq!(status.staged_files.len(), 1);
assert_eq!(
status.staged_files.get(path).unwrap().status,
StagedEntryStatus::Removed
);
Ok(())
})
.await
}
#[tokio::test]
async fn test_rm_dir_without_recursive_flag_should_be_error() -> Result<(), OxenError> {
test::run_select_data_repo_test_no_commits_async("train", |repo| async move {
let path = Path::new("train");
let opts = RmOpts {
path: path.to_path_buf(),
staged: false,
recursive: false, };
let result = repositories::rm(&repo, &opts);
assert!(result.is_err());
Ok(())
})
.await
}
#[tokio::test]
async fn test_rm_dir_that_is_not_committed_should_throw_error() -> Result<(), OxenError> {
test::run_select_data_repo_test_no_commits_async("train", |repo| async move {
let train_dir = Path::new("train");
let opts = RmOpts {
path: train_dir.to_path_buf(),
staged: false,
recursive: true, };
let result = repositories::rm(&repo, &opts);
assert!(result.is_err());
Ok(())
})
.await
}
#[tokio::test]
async fn test_rm_dir_with_modifications_should_throw_error() -> Result<(), OxenError> {
if std::env::consts::OS == "windows" {
return Ok(());
}
test::run_select_data_repo_test_committed_async("train", |repo| async move {
let train_dir = Path::new("train");
let opts = RmOpts {
path: train_dir.to_path_buf(),
staged: false,
recursive: true, };
util::fs::copy(
test::REPO_ROOT
.join("data")
.join("test")
.join("images")
.join("cat_1.jpg"),
repo.path.join(train_dir.join("dog_1.jpg")),
)?;
let status = repositories::status(&repo)?;
status.print();
assert_eq!(
status.modified_files.len(),
1,
"Expected 1 modified file but found: {:?}",
status.modified_files
);
let result = repositories::rm(&repo, &opts);
assert!(result.is_err(), "{result:?}");
Ok(())
})
.await
}
#[tokio::test]
async fn test_rm_train_dir() -> Result<(), OxenError> {
test::run_select_data_repo_test_committed_async("train", |repo| async move {
let path = Path::new("train");
let og_num_files = util::fs::rcount_files_in_dir(&repo.path.join(path));
let opts = RmOpts {
path: path.to_path_buf(),
staged: false,
recursive: true, };
repositories::rm(&repo, &opts)?;
let status = repositories::status(&repo)?;
status.print();
assert_eq!(status.staged_files.len(), og_num_files);
for (_, staged_entry) in status.staged_files.iter() {
assert_eq!(staged_entry.status, StagedEntryStatus::Removed);
}
let commit = repositories::commit(&repo, "removed train dir")?;
let has_dir = repositories::tree::has_dir(&repo, &commit, path)?;
assert!(!has_dir);
Ok(())
})
.await
}
#[tokio::test]
async fn test_rm_dir_with_slash() -> Result<(), OxenError> {
test::run_select_data_repo_test_committed_async("train", |repo| async move {
let path = Path::new("train/");
let og_num_files = util::fs::rcount_files_in_dir(&repo.path.join(path));
let opts = RmOpts {
path: path.to_path_buf(),
staged: false,
recursive: true, };
repositories::rm(&repo, &opts)?;
let status = repositories::status(&repo)?;
status.print();
assert_eq!(status.staged_files.len(), og_num_files);
for (_, staged_entry) in status.staged_files.iter() {
assert_eq!(staged_entry.status, StagedEntryStatus::Removed);
}
Ok(())
})
.await
}
#[tokio::test]
async fn test_rm_subdir() -> Result<(), OxenError> {
test::run_select_data_repo_test_committed_async("annotations", |repo| async move {
let path = Path::new("annotations").join("train");
let og_num_files = util::fs::rcount_files_in_dir(&repo.path.join(&path));
let opts = RmOpts {
path,
staged: false,
recursive: true, };
repositories::rm(&repo, &opts)?;
let status = repositories::status(&repo)?;
status.print();
assert_eq!(status.staged_files.len(), og_num_files);
for (_, staged_entry) in status.staged_files.iter() {
assert_eq!(staged_entry.status, StagedEntryStatus::Removed);
}
Ok(())
})
.await
}
#[tokio::test]
async fn test_rm_dot_excludes_oxen_hidden_dir() -> Result<(), OxenError> {
test::run_empty_data_repo_test_no_commits_async(|repo| async move {
let test_file1 = repo.path.join("test1.txt");
let test_file2 = repo.path.join("test2.txt");
util::fs::write_to_path(&test_file1, "test content 1")?;
util::fs::write_to_path(&test_file2, "test content 2")?;
let test_dir = repo.path.join("test_dir");
util::fs::create_dir_all(&test_dir)?;
let test_file_in_dir = test_dir.join("file_in_dir.txt");
util::fs::write_to_path(&test_file_in_dir, "file in directory")?;
repositories::add(&repo, &repo.path).await?;
repositories::commit(&repo, "Initial commit with test files")?;
let oxen_dir = repo.path.join(OXEN_HIDDEN_DIR);
assert!(oxen_dir.exists(), "OXEN_HIDDEN_DIR should exist");
let opts = RmOpts {
path: PathBuf::from("."),
staged: false,
recursive: true,
};
repositories::rm(&repo, &opts)?;
assert!(
oxen_dir.exists(),
"OXEN_HIDDEN_DIR should not be removed by 'oxen rm .'"
);
let status = repositories::status(&repo)?;
status.print();
assert!(
!status.staged_files.is_empty(),
"Some files should be staged for removal"
);
for (path, staged_entry) in status.staged_files.iter() {
assert_ne!(
path.to_str().unwrap_or(""),
OXEN_HIDDEN_DIR,
"OXEN_HIDDEN_DIR should not be staged for removal"
);
assert!(
!path.starts_with(OXEN_HIDDEN_DIR),
"No files within OXEN_HIDDEN_DIR should be staged for removal"
);
assert_eq!(staged_entry.status, StagedEntryStatus::Removed);
}
let direct_oxen_opts = RmOpts {
path: PathBuf::from(OXEN_HIDDEN_DIR),
staged: false,
recursive: true,
};
let err = repositories::rm(&repo, &direct_oxen_opts);
assert!(err.is_err());
assert!(
oxen_dir.exists(),
"OXEN_HIDDEN_DIR should not be removed by direct 'oxen rm .oxen'"
);
Ok(())
})
.await
}
#[tokio::test]
async fn test_rm_wildcard_multi_level() -> Result<(), OxenError> {
test::run_training_data_repo_test_fully_committed_async(|repo| async move {
let rm_opts = RmOpts {
path: PathBuf::from("annotations/test/*"),
..Default::default()
};
repositories::rm(&repo, &rm_opts)?;
let status = repositories::status(&repo)?;
assert_eq!(status.staged_files.len(), 1);
assert_eq!(
status.staged_files.keys().next().unwrap(),
&PathBuf::from("annotations/test/annotations.csv")
);
Ok(())
})
.await
}
#[tokio::test]
async fn test_rm_r_already_deleted_nested_dir() -> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let nested_dir = repo.path.join("1/2/3");
std::fs::create_dir_all(&nested_dir)?;
let file_path = nested_dir.join("file.txt");
util::fs::write(&file_path, "content")?;
repositories::add(&repo, &repo.path).await?;
repositories::commit(&repo, "init")?;
util::fs::remove_dir_all(&nested_dir)?;
assert!(!nested_dir.exists());
let rm_opts = RmOpts {
path: PathBuf::from("1/2/3"),
recursive: true,
staged: false,
};
repositories::rm(&repo, &rm_opts)?;
let status = repositories::status(&repo)?;
let has_staged_removal = status
.staged_files
.iter()
.any(|(_, entry)| entry.status == StagedEntryStatus::Removed);
assert!(has_staged_removal, "file should be staged for removal");
repositories::commit(&repo, "remove dir")?;
let status = repositories::status(&repo)?;
assert!(
status.removed_files.is_empty(),
"status should be clean after committing removal, but got removed_files: {:?}",
status.removed_files
);
assert!(
status.staged_files.is_empty(),
"no staged files should remain"
);
let head = repositories::commits::head_commit(&repo)?;
let dir_node = repositories::tree::get_dir_without_children(
&repo,
&head,
Path::new("1/2/3"),
None,
)?;
assert!(
dir_node.is_none(),
"directory 1/2/3 should not exist in the committed tree"
);
Ok(())
})
.await
}
#[tokio::test]
async fn test_add_dot_stages_deleted_nested_dir() -> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let nested_dir = repo.path.join("1/2/3");
std::fs::create_dir_all(&nested_dir)?;
let file_path = nested_dir.join("file.txt");
util::fs::write(&file_path, "content")?;
repositories::add(&repo, &repo.path).await?;
repositories::commit(&repo, "init")?;
util::fs::remove_dir_all(&nested_dir)?;
assert!(!nested_dir.exists());
assert!(repo.path.join("1/2").exists());
repositories::add(&repo, &repo.path).await?;
let status = repositories::status(&repo)?;
let has_staged_removal = status
.staged_files
.iter()
.any(|(_, entry)| entry.status == StagedEntryStatus::Removed);
assert!(
has_staged_removal,
"file should be staged for removal after `oxen add .`"
);
repositories::commit(&repo, "remove dir")?;
let status = repositories::status(&repo)?;
assert!(
status.removed_files.is_empty(),
"status should be clean after committing removal, but got removed_files: {:?}",
status.removed_files
);
let head = repositories::commits::head_commit(&repo)?;
let dir_node = repositories::tree::get_dir_without_children(
&repo,
&head,
Path::new("1/2/3"),
None,
)?;
assert!(
dir_node.is_none(),
"directory 1/2/3 should not exist in the committed tree"
);
let file_node =
repositories::tree::get_file_by_path(&repo, &head, Path::new("1/2/3/file.txt"))?;
assert!(
file_node.is_none(),
"file 1/2/3/file.txt should not exist in the committed tree"
);
Ok(())
})
.await
}
#[tokio::test]
async fn test_add_dot_stages_deleted_file_in_nested_dir() -> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let nested_dir = repo.path.join("1/2/3");
std::fs::create_dir_all(&nested_dir)?;
let file_path = nested_dir.join("file.txt");
util::fs::write(&file_path, "content")?;
repositories::add(&repo, &repo.path).await?;
repositories::commit(&repo, "init")?;
util::fs::remove_file(&file_path)?;
assert!(!file_path.exists());
assert!(nested_dir.exists());
repositories::add(&repo, &repo.path).await?;
let status = repositories::status(&repo)?;
let has_staged_removal = status
.staged_files
.iter()
.any(|(_, entry)| entry.status == StagedEntryStatus::Removed);
assert!(
has_staged_removal,
"file should be staged for removal after `oxen add .`"
);
repositories::commit(&repo, "remove file")?;
let status = repositories::status(&repo)?;
assert!(
status.removed_files.is_empty(),
"status should be clean after committing removal, but got removed_files: {:?}",
status.removed_files
);
let head = repositories::commits::head_commit(&repo)?;
let file_node =
repositories::tree::get_file_by_path(&repo, &head, Path::new("1/2/3/file.txt"))?;
assert!(
file_node.is_none(),
"file 1/2/3/file.txt should not exist in the committed tree"
);
Ok(())
})
.await
}
}