use crate::core;
use crate::core::versions::MinOxenVersion;
use crate::error::OxenError;
use crate::model::LocalRepository;
use crate::opts::RestoreOpts;
pub async fn restore(repo: &LocalRepository, opts: RestoreOpts) -> Result<(), OxenError> {
match repo.min_version() {
MinOxenVersion::V0_10_0 => panic!("v0.10.0 no longer supported"),
_ => core::v_latest::restore::restore(repo, opts).await,
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use std::path::Path;
use std::path::PathBuf;
use crate::core::df::tabular;
use crate::error::OxenError;
use crate::opts::DFOpts;
use crate::opts::RestoreOpts;
use crate::opts::RmOpts;
use crate::repositories;
use crate::test;
use crate::test::append_line_txt_file;
use crate::util;
#[tokio::test]
async fn test_command_restore_removed_file_from_head() -> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let hello_filename = "hello.txt";
let hello_file = repo.path.join(hello_filename);
util::fs::write_to_path(&hello_file, "Hello World")?;
repositories::add(&repo, &hello_file).await?;
repositories::commit(&repo, "My message")?;
util::fs::remove_file(&hello_file)?;
assert!(!hello_file.exists());
repositories::restore::restore(&repo, RestoreOpts::from_path(hello_filename)).await?;
assert!(hello_file.exists());
Ok(())
})
.await
}
#[tokio::test]
async fn test_command_restore_file_from_commit_id() -> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let hello_filename = "hello.txt";
let hello_file = repo.path.join(hello_filename);
util::fs::write_to_path(&hello_file, "Hello World")?;
repositories::add(&repo, &hello_file).await?;
repositories::commit(&repo, "My message")?;
let first_modification = "Hola Mundo";
let hello_file = test::modify_txt_file(hello_file, first_modification)?;
repositories::add(&repo, &hello_file).await?;
let first_mod_commit = repositories::commit(&repo, "Changing to spanish")?;
let second_modification = "Bonjour le monde";
let hello_file = test::modify_txt_file(hello_file, second_modification)?;
repositories::add(&repo, &hello_file).await?;
repositories::commit(&repo, "Changing to french")?;
repositories::restore::restore(
&repo,
RestoreOpts::from_path_ref(hello_filename, first_mod_commit.id),
)
.await?;
let content = util::fs::read_from_path(&hello_file)?;
assert!(hello_file.exists());
assert_eq!(content, first_modification);
Ok(())
})
.await
}
#[tokio::test]
async fn test_command_restore_removed_file_from_branch_with_commits_between()
-> 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")?;
let orig_branch = repositories::branches::current_branch(&repo)?.unwrap();
let train_dir = repo.path.join("train");
repositories::add(&repo, train_dir).await?;
repositories::commit(&repo, "Adding train dir")?;
repositories::branches::create_checkout(&repo, "remove-labels")?;
util::fs::remove_file(&file_to_remove)?;
let status = repositories::status(&repo).await?;
assert_eq!(status.removed_files.len(), 1);
repositories::add(&repo, &file_to_remove).await?;
repositories::commit(&repo, "Removing labels file")?;
assert!(!file_to_remove.exists());
repositories::checkout(&repo, orig_branch.name).await?;
assert!(file_to_remove.exists());
Ok(())
})
.await
}
#[tokio::test]
async fn test_restore_aggregates_per_file_failures() -> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let succeeds = repo.path.join("succeeds.txt");
let missing_blob = repo.path.join("missing_blob.txt");
let other_error = repo.path.join("other_error.txt");
let succeeds_content = "alpha alpha alpha";
util::fs::write_to_path(&succeeds, succeeds_content)?;
util::fs::write_to_path(&missing_blob, "beta beta beta")?;
util::fs::write_to_path(&other_error, "gamma gamma gamma")?;
repositories::add(&repo, &succeeds).await?;
repositories::add(&repo, &missing_blob).await?;
repositories::add(&repo, &other_error).await?;
repositories::commit(&repo, "Add three files")?;
let head = repositories::commits::head_commit(&repo)?;
let node = repositories::tree::get_node_by_path(&repo, &head, "missing_blob.txt")?
.expect("missing_blob.txt must be in HEAD's tree");
let missing_hash = node.hash.to_string();
let missing_blob_data = repo
.path
.join(crate::constants::OXEN_HIDDEN_DIR)
.join("versions")
.join("files")
.join(&missing_hash[..2])
.join(&missing_hash[2..])
.join("data");
assert!(
missing_blob_data.exists(),
"test setup expected blob to exist: {missing_blob_data:?}"
);
util::fs::remove_file(&missing_blob_data)?;
util::fs::remove_file(&succeeds)?;
util::fs::remove_file(&missing_blob)?;
util::fs::remove_file(&other_error)?;
std::fs::create_dir(&other_error)?;
let mut paths = HashSet::new();
paths.insert(PathBuf::from("succeeds.txt"));
paths.insert(PathBuf::from("missing_blob.txt"));
paths.insert(PathBuf::from("other_error.txt"));
let result = repositories::restore::restore(
&repo,
RestoreOpts {
paths,
staged: false,
is_remote: false,
source_ref: None,
},
)
.await;
let Err(OxenError::RestoreFailed { failures }) = result else {
panic!("expected RestoreFailed, got: {result:?}");
};
assert_eq!(failures.len(), 2, "failures: {failures:#?}");
let by_path = |name: &str| -> &OxenError {
failures
.iter()
.find(|(p, _)| p.to_string() == name)
.map(|(_, e)| e.as_ref())
.unwrap_or_else(|| panic!("{name} should be in failures: {failures:#?}"))
};
assert!(
matches!(
by_path("missing_blob.txt"),
OxenError::VersionStoreDataMissing { .. }
),
"missing_blob.txt: expected VersionStoreDataMissing, got: {:?}",
by_path("missing_blob.txt")
);
assert!(
!matches!(
by_path("other_error.txt"),
OxenError::VersionStoreDataMissing { .. }
),
"other_error.txt: expected a non-missing-data error, got: {:?}",
by_path("other_error.txt")
);
let rendered = OxenError::RestoreFailed { failures }.to_string();
assert!(
rendered.starts_with("Failed to restore 2 file(s):"),
"rendered did not lead with the count summary: {rendered}"
);
assert!(
rendered.contains("missing_blob.txt"),
"missing_blob.txt missing in:\n{rendered}"
);
assert!(
rendered.contains("other_error.txt"),
"other_error.txt missing in:\n{rendered}"
);
assert_eq!(
rendered.matches("version-store data missing").count(),
1,
"expected exactly one missing-data line in:\n{rendered}"
);
assert!(succeeds.exists(), "succeeds.txt should have been restored");
assert_eq!(util::fs::read_from_path(&succeeds)?, succeeds_content);
assert!(!missing_blob.exists());
assert!(other_error.is_dir());
Ok(())
})
.await
}
#[tokio::test]
async fn test_restore_directory() -> Result<(), OxenError> {
test::run_training_data_repo_test_fully_committed_async(|repo| async move {
let history = repositories::commits::list(&repo)?;
let last_commit = history.first().unwrap();
let annotations_dir = Path::new("annotations");
let bbox_file = annotations_dir.join("train").join("bounding_box.csv");
let bbox_path = repo.path.join(bbox_file);
let og_bbox_contents = util::fs::read_from_path(&bbox_path)?;
util::fs::remove_file(&bbox_path)?;
let readme_file = annotations_dir.join("README.md");
let readme_path = repo.path.join(readme_file);
let og_readme_contents = util::fs::read_from_path(&readme_path)?;
let readme_path = test::append_line_txt_file(readme_path, "Adding s'more")?;
repositories::restore::restore(
&repo,
RestoreOpts::from_path_ref(annotations_dir, last_commit.id.clone()),
)
.await?;
let restored_contents = util::fs::read_from_path(&bbox_path)?;
assert_eq!(og_bbox_contents, restored_contents);
let restored_contents = util::fs::read_from_path(readme_path)?;
assert_eq!(og_readme_contents, restored_contents);
Ok(())
})
.await
}
#[tokio::test]
async fn test_restore_recreates_tracked_empty_directory() -> Result<(), OxenError> {
test::run_one_commit_local_repo_test_async(|repo| async move {
let subdir_rel = PathBuf::from("subdir");
let subdir = repo.path.join(&subdir_rel);
util::fs::create_dir_all(&subdir)?;
util::fs::write_to_path(subdir.join("a.txt"), "AAAA")?;
repositories::add(&repo, &subdir).await?;
repositories::commit(&repo, "Add subdir/a.txt")?;
let rm_opts = RmOpts {
path: PathBuf::from("subdir/a.txt"),
recursive: false,
staged: false,
};
repositories::rm(&repo, &rm_opts).await?;
repositories::commit(&repo, "Remove subdir/a.txt")?;
util::fs::remove_dir_all(&subdir)?;
assert!(
!subdir.exists(),
"test setup: subdir must be gone from disk"
);
let status_before = repositories::status(&repo).await?;
assert!(
status_before.removed_files.contains(&subdir_rel),
"test setup: status should report subdir as removed before restore; \
got removed_files={:?}",
status_before.removed_files
);
repositories::restore::restore(&repo, RestoreOpts::from_path(&subdir_rel)).await?;
assert!(
subdir.exists(),
"oxen restore <empty-dir> must recreate the tracked empty directory on disk"
);
let status_after = repositories::status(&repo).await?;
assert!(
status_after.is_clean(),
"after restoring tracked empty dir, status should be clean; got {status_after:?}"
);
Ok(())
})
.await
}
#[tokio::test]
async fn test_restore_directory_preserves_untracked_files() -> Result<(), OxenError> {
test::run_training_data_repo_test_fully_committed_async(|repo| async move {
let history = repositories::commits::list(&repo)?;
let last_commit = history.first().unwrap();
let annotations_dir = Path::new("annotations");
let annotations_path = repo.path.join(annotations_dir);
let untracked_top_level = annotations_path.join("scratch.txt");
util::fs::write_to_path(&untracked_top_level, "top-level scratch")?;
let untracked_subdir = annotations_path.join("scratch_dir");
util::fs::create_dir_all(&untracked_subdir)?;
let untracked_in_subdir = untracked_subdir.join("nested.txt");
util::fs::write_to_path(&untracked_in_subdir, "nested scratch")?;
repositories::restore::restore(
&repo,
RestoreOpts::from_path_ref(annotations_dir, last_commit.id.clone()),
)
.await?;
assert!(
untracked_top_level.exists(),
"top-level untracked file must survive `oxen restore <dir>`"
);
assert!(
untracked_in_subdir.exists(),
"untracked file inside an untracked subdir must survive `oxen restore <dir>`"
);
Ok(())
})
.await
}
#[tokio::test]
async fn test_restore_removed_tabular_data() -> Result<(), OxenError> {
test::run_training_data_repo_test_fully_committed_async(|repo| async move {
let history = repositories::commits::list(&repo)?;
let last_commit = history.first().unwrap();
let bbox_file = Path::new("annotations")
.join("train")
.join("bounding_box.csv");
let bbox_path = repo.path.join(&bbox_file);
let og_contents = util::fs::read_from_path(&bbox_path)?;
util::fs::remove_file(&bbox_path)?;
println!("restoring {bbox_file:?}");
repositories::restore::restore(
&repo,
RestoreOpts::from_path_ref(bbox_file, last_commit.id.clone()),
)
.await?;
let restored_contents = util::fs::read_from_path(&bbox_path)?;
assert_eq!(og_contents, restored_contents);
Ok(())
})
.await
}
#[tokio::test]
async fn test_restore_modified_tabular_data() -> Result<(), OxenError> {
test::run_training_data_repo_test_fully_committed_async(|repo| async move {
let history = repositories::commits::list(&repo)?;
let last_commit = history.first().unwrap();
let bbox_file = Path::new("annotations")
.join("train")
.join("bounding_box.csv");
let bbox_path = repo.path.join(&bbox_file);
let og_contents = util::fs::read_from_path(&bbox_path)?;
let mut opts = DFOpts::empty();
opts.add_row = Some("{\"file\": \"train/dog_99.jpg\", \"label\": \"dog\", \"min_x\": 101.5, \"min_y\": 32.0, \"width\": 385, \"height\": 330}".to_string());
let mut df = tabular::read_df(&bbox_path, opts).await?;
tabular::write_df(&mut df, &bbox_path)?;
repositories::restore::restore(
&repo,
RestoreOpts::from_path_ref(bbox_file, last_commit.id.clone()),
).await?;
let restored_contents = util::fs::read_from_path(&bbox_path)?;
assert_eq!(og_contents, restored_contents);
let status = repositories::status(&repo).await?;
assert_eq!(status.modified_files.len(), 0);
assert!(status.is_clean());
Ok(())
}).await
}
#[tokio::test]
async fn test_restore_modified_text_data() -> Result<(), OxenError> {
test::run_training_data_repo_test_fully_committed_async(|repo| async move {
let history = repositories::commits::list(&repo)?;
let last_commit = history.first().unwrap();
let bbox_file = Path::new("annotations")
.join("train")
.join("annotations.txt");
let bbox_path = repo.path.join(&bbox_file);
let og_contents = util::fs::read_from_path(&bbox_path)?;
let new_contents = format!("{og_contents}\nnew 0");
util::fs::write_to_path(&bbox_path, new_contents)?;
repositories::restore::restore(
&repo,
RestoreOpts::from_path_ref(bbox_file, last_commit.id.clone()),
)
.await?;
let restored_contents = util::fs::read_from_path(&bbox_path)?;
assert_eq!(og_contents, restored_contents);
let status = repositories::status(&repo).await?;
assert_eq!(status.modified_files.len(), 0);
assert!(status.is_clean());
Ok(())
})
.await
}
#[tokio::test]
async fn test_restore_fast_path_skips_when_mtime_and_size_match() -> Result<(), OxenError> {
test::run_training_data_repo_test_fully_committed_async(|repo| async move {
let filename = Path::new("annotations")
.join("train")
.join("annotations.txt");
let path = repo.path.join(&filename);
repositories::restore::restore(&repo, RestoreOpts::from_path(&filename)).await?;
let meta = std::fs::metadata(&path)?;
let expected_mtime = meta.modified()?;
let size = meta.len();
let garbage: Vec<u8> = (0..size).map(|_| b'X').collect();
std::fs::write(&path, &garbage)?;
filetime::set_file_mtime(&path, filetime::FileTime::from_system_time(expected_mtime))?;
repositories::restore::restore(&repo, RestoreOpts::from_path(&filename)).await?;
let after = std::fs::read(&path).expect("read back after fast-path restore");
assert_eq!(
after, garbage,
"fast path should have skipped the copy, leaving garbage in place"
);
Ok(())
})
.await
}
#[tokio::test]
async fn test_restore_overwrites_when_mtime_differs_but_size_matches() -> Result<(), OxenError>
{
test::run_training_data_repo_test_fully_committed_async(|repo| async move {
let filename = Path::new("annotations")
.join("train")
.join("annotations.txt");
let path = repo.path.join(&filename);
let og_contents = util::fs::read_from_path(&path)?;
let og_meta = std::fs::metadata(&path)?;
let size = og_meta.len();
let recorded_mtime = og_meta.modified()?;
let garbage: Vec<u8> = (0..size).map(|_| b'X').collect();
std::fs::write(&path, &garbage)?;
let tolerance = repo.mtime_tolerance().await;
let outside_tolerance = recorded_mtime + tolerance + std::time::Duration::from_secs(1);
filetime::set_file_mtime(
&path,
filetime::FileTime::from_system_time(outside_tolerance),
)?;
repositories::restore::restore(&repo, RestoreOpts::from_path(&filename)).await?;
let after = util::fs::read_from_path(&path)?;
assert_eq!(
og_contents, after,
"restore should overwrite when mtime differs, even if size matches"
);
Ok(())
})
.await
}
#[tokio::test]
async fn test_restore_staged_file() -> Result<(), OxenError> {
test::run_training_data_repo_test_no_commits_async(|repo| async move {
let bbox_file = Path::new("annotations")
.join("train")
.join("bounding_box.csv");
let bbox_path = repo.path.join(&bbox_file);
repositories::add(&repo, bbox_path).await?;
let status = repositories::status(&repo).await?;
assert_eq!(status.staged_files.len(), 1);
status.print();
repositories::restore::restore(&repo, RestoreOpts::from_staged_path(bbox_file)).await?;
let status = repositories::status(&repo).await?;
assert_eq!(status.staged_files.len(), 0);
Ok(())
})
.await
}
#[tokio::test]
async fn test_restore_data_frame_with_duplicates() -> Result<(), OxenError> {
test::run_training_data_repo_test_fully_committed_async(|repo| async move {
let ann_file = Path::new("nlp")
.join("classification")
.join("annotations")
.join("train.tsv");
let ann_path = repo.path.join(&ann_file);
let new_line = "new_data,123,456,789";
append_line_txt_file(&ann_path, new_line)?;
let orig_df = tabular::read_df(&ann_path, DFOpts::empty()).await?;
let og_contents = util::fs::read_from_path(&ann_path)?;
repositories::add(&repo, &ann_path).await?;
let commit = repositories::commit(&repo, "adding data with duplicates")?;
util::fs::remove_file(&ann_path)?;
repositories::restore::restore(&repo, RestoreOpts::from_path_ref(ann_file, commit.id))
.await?;
let restored_df = tabular::read_df(&ann_path, DFOpts::empty()).await?;
assert_eq!(restored_df.height(), orig_df.height());
assert_eq!(restored_df.width(), orig_df.width());
let restored_contents = util::fs::read_from_path(&ann_path)?;
assert_eq!(og_contents, restored_contents);
Ok(())
})
.await
}
#[tokio::test]
async fn test_restore_bounding_box_data_frame() -> Result<(), OxenError> {
test::run_training_data_repo_test_fully_committed_async(|repo| async move {
let ann_file = Path::new("annotations")
.join("train")
.join("bounding_box.csv");
let ann_path = repo.path.join(&ann_file);
let new_line = "new_data,123,456,789";
append_line_txt_file(&ann_path, new_line)?;
let orig_df = tabular::read_df(&ann_path, DFOpts::empty()).await?;
let og_contents = util::fs::read_from_path(&ann_path)?;
repositories::add(&repo, &ann_path).await?;
let commit = repositories::commit(&repo, "adding data with duplicates")?;
util::fs::remove_file(&ann_path)?;
repositories::restore::restore(&repo, RestoreOpts::from_path_ref(ann_file, commit.id))
.await?;
let restored_df = tabular::read_df(&ann_path, DFOpts::empty()).await?;
assert_eq!(restored_df.height(), orig_df.height());
assert_eq!(restored_df.width(), orig_df.width());
let restored_contents = util::fs::read_from_path(&ann_path)?;
assert_eq!(og_contents, restored_contents);
Ok(())
})
.await
}
#[tokio::test]
async fn test_restore_staged_directory() -> Result<(), OxenError> {
test::run_training_data_repo_test_no_commits_async(|repo| async move {
let relative_path = Path::new("annotations");
let annotations_dir = repo.path.join(relative_path);
repositories::add(&repo, annotations_dir).await?;
let status = repositories::status(&repo).await?;
assert_eq!(status.staged_dirs.len(), 3);
assert_eq!(status.staged_files.len(), 6);
status.print();
repositories::restore::restore(&repo, RestoreOpts::from_staged_path(relative_path))
.await?;
let status = repositories::status(&repo).await?;
assert_eq!(status.staged_dirs.len(), 0);
assert_eq!(status.staged_files.len(), 0);
Ok(())
})
.await
}
#[tokio::test]
async fn test_wildcard_restore_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).await?;
status.print();
assert_eq!(
status
.staged_dirs
.paths
.get(Path::new("nlp"))
.unwrap()
.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).await?;
assert_eq!(status.removed_files.len(), 1);
assert_eq!(status.staged_files.len(), 0);
repositories::add(&repo, "nlp/*").await?;
let status = repositories::status(&repo).await?;
assert_eq!(status.staged_dirs.len(), 1);
assert_eq!(status.staged_files.len(), 2);
Ok(())
})
.await
}
#[tokio::test]
async fn test_wildcard_restore_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")?;
let rm_opts = RmOpts {
path: PathBuf::from("images/*"),
..Default::default()
};
repositories::rm(&repo, &rm_opts).await?;
let status = repositories::status(&repo).await?;
assert_eq!(status.staged_files.len(), 7);
assert_eq!(status.removed_files.len(), 0);
let mut paths = HashSet::new();
paths.insert(PathBuf::from("images/*"));
let restore_opts = RestoreOpts {
paths,
staged: true,
source_ref: None,
is_remote: false,
};
repositories::restore::restore(&repo, restore_opts).await?;
let status = repositories::status(&repo).await?;
assert_eq!(status.removed_files.len(), 7);
assert_eq!(status.staged_files.len(), 0);
let mut paths = HashSet::new();
paths.insert(PathBuf::from("images/*"));
let restore_opts = RestoreOpts {
paths,
staged: false,
source_ref: None,
is_remote: false,
};
repositories::restore::restore(&repo, restore_opts).await?;
let status = repositories::status(&repo).await?;
assert_eq!(status.removed_files.len(), 0);
assert_eq!(status.staged_files.len(), 0);
Ok(())
})
.await
}
#[tokio::test]
async fn test_restore_wildcard_prefix_staged() -> Result<(), OxenError> {
test::run_training_data_repo_test_fully_committed_async(|repo| async move {
let rm_opts = RmOpts {
path: PathBuf::from("train/*"),
..Default::default()
};
repositories::rm(&repo, &rm_opts).await?;
let status = repositories::status(&repo).await?;
assert_eq!(status.staged_files.len(), 7);
let mut paths = HashSet::new();
paths.insert(PathBuf::from("train/dog_*.jpg"));
let restore_opts = RestoreOpts {
paths,
staged: true,
source_ref: None,
is_remote: false,
};
repositories::restore::restore(&repo, restore_opts).await?;
let status = repositories::status(&repo).await?;
assert_eq!(status.staged_files.len(), 3); assert_eq!(status.removed_files.len(), 4);
Ok(())
})
.await
}
#[tokio::test]
async fn test_restore_staged_schemas_with_wildcard() -> Result<(), OxenError> {
test::run_training_data_repo_test_fully_committed_async(|repo| async move {
let new_annotations_dir = repo.path.join("new_annotations");
let bbox_path = repo
.path
.join("annotations")
.join("train")
.join("bounding_box.csv");
let one_shot_path = repo
.path
.join("annotations")
.join("train")
.join("one_shot.csv");
util::fs::create_dir_all(&new_annotations_dir)?;
util::fs::copy(bbox_path, new_annotations_dir.join("bounding_box.csv"))?;
util::fs::copy(one_shot_path, new_annotations_dir.join("one_shot.csv"))?;
new_annotations_dir
.join("bounding_box.csv")
.file_name()
.unwrap();
new_annotations_dir
.join("one_shot.csv")
.file_name()
.unwrap();
repositories::add(&repo, &new_annotations_dir).await?;
let status = repositories::status(&repo).await?;
assert_eq!(status.staged_files.len(), 2);
assert_eq!(status.staged_schemas.len(), 2);
let mut paths = HashSet::new();
paths.insert(PathBuf::from("new_annotations").join(PathBuf::from("*.csv")));
let restore_opts = RestoreOpts {
paths,
staged: true,
source_ref: None,
is_remote: false,
};
repositories::restore::restore(&repo, restore_opts).await?;
let status = repositories::status(&repo).await?;
assert_eq!(status.staged_files.len(), 0);
assert_eq!(status.staged_schemas.len(), 0);
Ok(())
})
.await
}
}