use crate::core;
use crate::core::versions::MinOxenVersion;
use crate::error::OxenError;
use crate::model::LocalRepository;
use std::path::Path;
pub async fn add(repo: &LocalRepository, path: impl AsRef<Path>) -> Result<(), OxenError> {
add_all_with_version(repo, vec![path], repo.min_version()).await
}
pub async fn add_all<T: AsRef<Path>>(
repo: &LocalRepository,
paths: impl IntoIterator<Item = T>,
) -> Result<(), OxenError> {
add_all_with_version(repo, paths, repo.min_version()).await
}
pub async fn add_all_with_version<T: AsRef<Path>>(
repo: &LocalRepository,
paths: impl IntoIterator<Item = T>,
version: MinOxenVersion,
) -> Result<(), OxenError> {
match version {
MinOxenVersion::V0_10_0 => panic!("v0.10.0 no longer supported"),
_ => core::v_latest::add::add(repo, paths).await,
}
}
#[cfg(test)]
mod tests {
use crate::test;
use std::path::Path;
use std::path::PathBuf;
use crate::error::OxenError;
use crate::opts::clone_opts::CloneOpts;
use crate::repositories;
use crate::util;
#[tokio::test]
async fn test_clone_root_subtree_depth_1_add_file() -> Result<(), OxenError> {
test::run_training_data_fully_sync_remote(|_local_repo, remote_repo| async move {
let cloned_remote = remote_repo.clone();
test::run_empty_dir_test_async(|dir| async move {
let mut opts = CloneOpts::new(&remote_repo.remote.url, dir.join("new_repo"));
opts.fetch_opts.subtree_paths = Some(vec![PathBuf::from("")]);
opts.fetch_opts.depth = Some(1);
let local_repo = repositories::clone::clone(&opts).await?;
let hello_file = local_repo.path.join("clone_depth_1_add.txt");
util::fs::write_to_path(
&hello_file,
"Oxen.ai is the best data version control system.",
)?;
repositories::add(&local_repo, &hello_file).await?;
let status = repositories::status(&local_repo)?;
assert_eq!(status.staged_files.len(), 1);
assert!(
status
.staged_files
.contains_key(&PathBuf::from("clone_depth_1_add.txt"))
);
Ok(())
})
.await?;
Ok(cloned_remote)
})
.await
}
#[tokio::test]
async fn test_clone_annotations_test_subtree_add_file() -> Result<(), OxenError> {
test::run_training_data_fully_sync_remote(|_local_repo, remote_repo| async move {
let cloned_remote = remote_repo.clone();
test::run_empty_dir_test_async(|dir| async move {
let mut opts = CloneOpts::new(&remote_repo.remote.url, dir.join("new_repo"));
opts.fetch_opts.subtree_paths =
Some(vec![PathBuf::from("annotations").join("test")]);
let local_repo = repositories::clone::clone(&opts).await?;
let annotations_test_dir = local_repo.path.join("annotations").join("test");
let readme_file = annotations_test_dir.join("README.md");
util::fs::write_to_path(
&readme_file,
r"
Q: What is the best data version control system?
A: Oxen.ai
",
)?;
repositories::add(&local_repo, &readme_file).await?;
let status = repositories::status(&local_repo)?;
assert_eq!(status.staged_files.len(), 1);
assert!(
status
.staged_files
.contains_key(&PathBuf::from("annotations").join("test").join("README.md"))
);
assert_eq!(status.removed_files.len(), 0);
Ok(())
})
.await?;
Ok(cloned_remote)
})
.await
}
#[tokio::test]
async fn test_command_add_file() -> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let hello_file = repo.path.join("hello.txt");
util::fs::write_to_path(&hello_file, "Hello World")?;
repositories::add(&repo, &hello_file).await?;
let repo_status = repositories::status(&repo)?;
assert_eq!(repo_status.staged_dirs.len(), 1);
assert_eq!(repo_status.staged_files.len(), 1);
assert_eq!(repo_status.untracked_files.len(), 0);
assert_eq!(repo_status.untracked_dirs.len(), 0);
Ok(())
})
.await
}
#[tokio::test]
async fn test_command_add_modified_file_in_subdirectory() -> Result<(), OxenError> {
test::run_training_data_repo_test_fully_committed_async(|repo| async move {
let one_shot_path = repo.path.join("annotations/train/one_shot.csv");
let file_contents = "file,label\ntrain/cat_1.jpg,0";
test::modify_txt_file(one_shot_path, file_contents)?;
let status = repositories::status(&repo)?;
println!("status: {status:?}");
status.print();
assert_eq!(status.modified_files.len(), 1);
let annotation_dir_path = repo.path.join("annotations");
repositories::add(&repo, annotation_dir_path).await?;
let status = repositories::status(&repo)?;
status.print();
assert_eq!(status.staged_files.len(), 1);
repositories::commit(&repo, "Changing one shot")?;
let status = repositories::status(&repo)?;
assert!(status.is_clean());
Ok(())
})
.await
}
#[tokio::test]
async fn test_command_add_removed_file() -> Result<(), OxenError> {
test::run_training_data_repo_test_no_commits_async(|repo| async move {
let file_to_remove = repo.path.join("labels.txt");
repositories::add(&repo, &file_to_remove).await?;
repositories::commit(&repo, "Adding labels file")?;
util::fs::remove_file(&file_to_remove)?;
let status = repositories::status(&repo)?;
assert_eq!(status.removed_files.len(), 1);
Ok(())
})
.await
}
#[tokio::test]
async fn test_command_add_dot_should_not_add_new_files() -> Result<(), OxenError> {
test::run_training_data_repo_test_no_commits_async(|repo| async move {
let num_files = util::fs::count_files_in_dir(&repo.path);
repositories::add(&repo, &repo.path).await?;
let num_files_after_add = util::fs::count_files_in_dir(&repo.path);
assert_eq!(num_files, num_files_after_add);
Ok(())
})
.await
}
#[tokio::test]
async fn test_command_add_dot_can_add_removed_files() -> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let hello_file = repo.path.join("hello.txt");
util::fs::write_to_path(&hello_file, "Hello World")?;
repositories::add(&repo, &hello_file).await?;
repositories::commit(&repo, "Adding a file")?;
util::fs::remove_file(&hello_file)?;
repositories::add(&repo, &repo.path).await?;
let status = repositories::status(&repo)?;
status.print();
assert_eq!(status.staged_files.len(), 1);
assert!(!hello_file.exists());
Ok(())
})
.await
}
#[tokio::test]
async fn test_command_add_dot_only_adds_changed_files() -> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let hello_file = repo.path.join("hello.txt");
util::fs::write_to_path(&hello_file, "Hello")?;
let world_file = repo.path.join("world.txt");
util::fs::write_to_path(&world_file, "World")?;
let third_file = repo.path.join("third.txt");
util::fs::write_to_path(&third_file, "!")?;
repositories::add(&repo, &repo.path).await?;
let status = repositories::status(&repo)?;
assert_eq!(status.staged_files.len(), 3);
repositories::commit(&repo, "Adding 3 files")?;
util::fs::remove_file(&hello_file)?;
test::modify_txt_file(&world_file, "MODIFIED")?;
repositories::add(&repo, &repo.path).await?;
let status = repositories::status(&repo)?;
assert_eq!(status.staged_files.len(), 2);
Ok(())
})
.await
}
#[tokio::test]
async fn test_can_add_merge_conflict() -> Result<(), OxenError> {
test::run_select_data_repo_test_no_commits_async("labels", |repo| async move {
let labels_path = repo.path.join("labels.txt");
repositories::add(&repo, &labels_path).await?;
repositories::commit(&repo, "adding initial labels file")?;
let og_branch = repositories::branches::current_branch(&repo)?.unwrap();
let branch_name = "change-labels";
repositories::branches::create_checkout(&repo, branch_name)?;
test::modify_txt_file(&labels_path, "cat\ndog\nnone")?;
repositories::add(&repo, &labels_path).await?;
repositories::commit(&repo, "adding none category")?;
repositories::checkout(&repo, og_branch.name).await?;
test::modify_txt_file(&labels_path, "cat\ndog\nperson")?;
repositories::add(&repo, &labels_path).await?;
repositories::commit(&repo, "adding person category")?;
repositories::merge::merge(&repo, branch_name).await?;
let status = repositories::status(&repo)?;
assert_eq!(status.merge_conflicts.len(), 1);
let path = status.merge_conflicts[0].base_entry.path.clone();
let fullpath = repo.path.join(path);
repositories::add(&repo, fullpath).await?;
let status = repositories::status(&repo)?;
assert_eq!(status.staged_files.len(), 1);
assert_eq!(status.merge_conflicts.len(), 0);
Ok(())
})
.await
}
#[tokio::test]
async fn test_add_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);
Ok(())
})
.await
}
#[tokio::test]
async fn test_command_add_stage_with_wildcard() -> Result<(), OxenError> {
test::run_training_data_repo_test_fully_committed_async(|repo| async move {
let _objects_dir = repo.path.join(".oxen/objects");
let one_shot_path = repo.path.join("annotations/train/one_shot.csv");
let file_contents = "file,label\ntrain/cat_1.jpg,0";
test::modify_txt_file(one_shot_path, file_contents)?;
let status = repositories::status(&repo)?;
assert_eq!(status.modified_files.len(), 1);
let annotation_dir_path = repo.path.join("annotations/*");
repositories::add(&repo, annotation_dir_path).await?;
let status = repositories::status(&repo)?;
status.print();
assert_eq!(status.staged_files.len(), 1);
repositories::commit(&repo, "Changing one shot")?;
let status = repositories::status(&repo)?;
assert!(status.is_clean());
Ok(())
})
.await
}
#[tokio::test]
async fn test_wildcard_add_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)?;
status.print();
assert_eq!(status.removed_files.len(), 1);
assert_eq!(status.staged_files.len(), 0);
repositories::add(&repo, "nlp/*").await?;
let status = repositories::status(&repo)?;
assert_eq!(status.staged_dirs.len(), 1);
assert_eq!(status.staged_files.len(), 2);
Ok(())
})
.await
}
#[tokio::test]
async fn test_wildcard_add_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);
Ok(())
})
.await
}
#[tokio::test]
async fn test_add_empty_dir() -> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let empty_dir = repo.path.join("empty_dir");
util::fs::create_dir_all(&empty_dir)?;
let status = repositories::status(&repo)?;
status.print();
assert!(
status
.untracked_dirs
.iter()
.any(|(path, _)| *path == Path::new("empty_dir"))
);
repositories::add(&repo, &empty_dir).await?;
let status = repositories::status(&repo)?;
status.print();
assert_eq!(
status
.staged_dirs
.paths
.get(&PathBuf::from("empty_dir"))
.unwrap()
.len(),
1
);
assert!(!status.is_clean());
assert_eq!(status.untracked_dirs.len(), 0);
Ok(())
})
.await
}
#[tokio::test]
async fn test_add_all_files_in_sub_dir() -> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let repo_path = &repo.path;
let training_data_dir = PathBuf::from("training_data");
let sub_dir = repo_path.join(&training_data_dir);
util::fs::create_dir_all(&sub_dir)?;
let sub_file_1 = test::add_txt_file_to_dir(&sub_dir, "Hello 1")?;
let sub_file_2 = test::add_txt_file_to_dir(&sub_dir, "Hello 2")?;
let sub_file_3 = test::add_txt_file_to_dir(&sub_dir, "Hello 3")?;
let status = repositories::status(&repo)?;
let dirs = status.untracked_dirs;
assert_eq!(dirs.len(), 1);
repositories::add(&repo, &sub_file_1).await?;
repositories::add(&repo, &sub_file_2).await?;
repositories::add(&repo, &sub_file_3).await?;
let status = repositories::status(&repo)?;
println!("status after add: {status:?}");
status.print();
let dirs = status.untracked_dirs;
assert_eq!(dirs.len(), 0);
let staged_dirs = status.staged_dirs;
assert_eq!(staged_dirs.len(), 1);
Ok(())
})
.await
}
#[tokio::test]
async fn test_stager_add_dir_recursive() -> Result<(), OxenError> {
test::run_training_data_repo_test_no_commits_async(|repo| async move {
let annotations_dir = repo.path.join("annotations");
repositories::add(&repo, &annotations_dir).await?;
let status = repositories::status(&repo)?;
status.print();
let dirs = status.staged_dirs;
assert_eq!(dirs.len(), 3);
Ok(())
})
.await
}
#[tokio::test]
async fn test_cannot_add_if_not_modified() -> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let repo_path = &repo.path;
let hello_file = test::add_txt_file_to_dir(repo_path, "Hello World")?;
repositories::add(&repo, &hello_file).await?;
repositories::commit(&repo, "Add Hello World")?;
repositories::add(&repo, &hello_file).await?;
let status = repositories::status(&repo)?;
assert_eq!(status.staged_files.len(), 0);
Ok(())
})
.await
}
#[tokio::test]
async fn test_command_add_after_modified_file_in_subdirectory() -> Result<(), OxenError> {
test::run_select_data_repo_test_no_commits_async("annotations", |repo| async move {
let one_shot_path = repo.path.join("annotations/train/one_shot.csv");
repositories::add(&repo, &repo.path).await?;
repositories::commit(&repo, "Adding one shot")?;
let branch_name = "feature/modify-data";
repositories::branches::create_checkout(&repo, branch_name)?;
let file_contents = "file,label\ntrain/cat_1.jpg,0\n";
let one_shot_path = test::modify_txt_file(one_shot_path, file_contents)?;
let status = repositories::status(&repo)?;
status.print();
assert_eq!(status.modified_files.len(), 1);
assert!(
status
.modified_files
.contains(&PathBuf::from("annotations/train/one_shot.csv"))
);
repositories::add(&repo, &one_shot_path).await?;
let status = repositories::status(&repo)?;
status.print();
assert_eq!(status.staged_files.len(), 1);
assert_eq!(status.modified_files.len(), 0);
assert!(
status
.staged_files
.contains_key(&PathBuf::from("annotations/train/one_shot.csv"))
);
Ok(())
})
.await
}
#[tokio::test]
async fn test_add_wildcard_prefix_match() -> Result<(), OxenError> {
test::run_training_data_repo_test_no_commits_async(|repo| async move {
repositories::add(&repo, "train/cat_*.jpg").await?;
let status = repositories::status(&repo)?;
assert_eq!(status.staged_files.len(), 3);
repositories::add(&repo, "train/dog_*.jpg").await?;
let status = repositories::status(&repo)?;
assert_eq!(status.staged_files.len(), 7);
Ok(())
})
.await
}
}