use std::path::Path;
use crate::core::df::tabular;
use crate::core::v_latest::branches::OnConflict;
use crate::error::OxenError;
use crate::model::{Branch, LocalRepository};
use crate::opts::{DFOpts, RestoreOpts};
use crate::{repositories, util};
pub async fn checkout(
repo: &LocalRepository,
value: impl AsRef<str>,
) -> Result<Option<Branch>, OxenError> {
let value = value.as_ref();
log::debug!("--- CHECKOUT START {value} ----");
if repositories::branches::exists(repo, value)? {
if repositories::branches::is_checked_out(repo, value) {
println!("Already on branch {value}");
return Ok(Some(repositories::branches::get_by_name(repo, value)?));
}
println!("Checkout branch: {value}");
let commit = repositories::revisions::get(repo, value)?
.ok_or_else(|| OxenError::RevisionNotFound(value.into()))?;
let subtree_paths = match repo.subtree_paths() {
Some(paths_vec) => paths_vec, None => vec![Path::new("").to_path_buf()],
};
let depth = repo.depth().unwrap_or(i32::MAX); repositories::branches::checkout_subtrees_to_commit(repo, &commit, &subtree_paths, depth)
.await?;
repositories::branches::set_head(repo, value)?;
Ok(Some(repositories::branches::get_by_name(repo, value)?))
} else {
if repositories::branches::is_checked_out(repo, value) {
eprintln!("Commit already checked out {value}");
return Ok(None);
}
let commit = repositories::revisions::get(repo, value)?
.ok_or_else(|| OxenError::RevisionNotFound(value.into()))?;
let previous_head_commit = repositories::commits::head_commit_maybe(repo)?;
repositories::branches::checkout_commit_from_commit(
repo,
&commit,
&previous_head_commit,
OnConflict::Abort,
)
.await?;
repositories::branches::update(repo, value, &commit.id)?;
repositories::branches::set_head(repo, value)?;
if repo.is_remote_mode() {
let mut mut_repo = repo.clone();
mut_repo.set_workspace(value)?;
mut_repo.save()?;
}
Ok(None)
}
}
pub async fn checkout_theirs(
repo: &LocalRepository,
path: impl AsRef<Path>,
) -> Result<(), OxenError> {
let conflicts = repositories::merge::list_conflicts(repo)?;
log::debug!(
"checkout_theirs {:?} conflicts.len() {}",
path.as_ref(),
conflicts.len()
);
if let Some(conflict) = conflicts
.iter()
.find(|c| c.merge_entry.path == path.as_ref())
{
repositories::restore::restore(
repo,
RestoreOpts::from_path_ref(path, conflict.merge_entry.commit_id.clone()),
)
.await
} else {
Err(OxenError::could_not_find_merge_conflict(path))
}
}
pub async fn checkout_ours(
repo: &LocalRepository,
path: impl AsRef<Path>,
) -> Result<(), OxenError> {
let conflicts = repositories::merge::list_conflicts(repo)?;
log::debug!(
"checkout_ours {:?} conflicts.len() {}",
path.as_ref(),
conflicts.len()
);
if let Some(conflict) = conflicts
.iter()
.find(|c| c.merge_entry.path == path.as_ref())
{
repositories::restore(
repo,
RestoreOpts::from_path_ref(path, conflict.base_entry.commit_id.clone()),
)
.await
} else {
Err(OxenError::could_not_find_merge_conflict(path))
}
}
pub async fn checkout_combine<P: AsRef<Path>>(
repo: &LocalRepository,
path: P,
) -> Result<(), OxenError> {
let conflicts = repositories::merge::list_conflicts(repo)?;
log::debug!(
"checkout_combine checking path {:?} -> [{}] conflicts",
path.as_ref(),
conflicts.len()
);
if let Some(conflict) = conflicts
.iter()
.find(|c| c.merge_entry.path == path.as_ref())
{
if util::fs::is_tabular(&conflict.base_entry.path) {
let version_store = repo.version_store();
let df_base_path = version_store
.get_version_path(&conflict.base_entry.hash)
.await?;
let df_base = tabular::maybe_read_df_with_extension(
repo,
&df_base_path,
&conflict.base_entry.path,
&conflict.base_entry.commit_id,
&DFOpts::empty(),
)
.await?;
let df_merge_path = version_store
.get_version_path(&conflict.merge_entry.hash)
.await?;
let df_merge = tabular::maybe_read_df_with_extension(
repo,
df_merge_path,
&conflict.merge_entry.path,
&conflict.merge_entry.commit_id,
&DFOpts::empty(),
)
.await?;
log::debug!("GOT DF HEAD {df_base}");
log::debug!("GOT DF MERGE {df_merge}");
match df_base.vstack(&df_merge) {
Ok(result) => {
log::debug!("GOT DF COMBINED {result}");
match result.unique_stable(None, polars::frame::UniqueKeepStrategy::First, None)
{
Ok(mut uniq) => {
log::debug!("GOT DF COMBINED UNIQUE {uniq}");
let output_path = repo.path.join(&conflict.base_entry.path);
tabular::write_df(&mut uniq, &output_path)
}
_ => Err(OxenError::basic_str("Could not uniq data")),
}
}
_ => Err(OxenError::basic_str(
"Could not combine data, make sure schema's match",
)),
}
} else {
Err(OxenError::basic_str(
"Cannot use --combine on non-tabular data file.",
))
}
} else {
Err(OxenError::could_not_find_merge_conflict(path))
}
}
#[cfg(test)]
mod tests {
use crate::api;
use crate::constants::DEFAULT_BRANCH_NAME;
use crate::error::OxenError;
use crate::opts::FetchOpts;
use crate::repositories;
use crate::test;
use crate::util;
#[tokio::test]
async fn test_command_checkout_non_existant_commit_id() -> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let checkout_result = repositories::checkout(&repo, "non-existent").await;
assert!(checkout_result.is_err());
Ok(())
})
.await
}
#[tokio::test]
async fn test_command_checkout_commit_id() -> 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")?;
repositories::add(&repo, &hello_file).await?;
let first_commit = repositories::commit(&repo, "Adding hello")?;
let world_file = repo.path.join("world.txt");
util::fs::write_to_path(&world_file, "World")?;
repositories::add(&repo, &world_file).await?;
repositories::commit(&repo, "Adding world")?;
assert!(world_file.exists());
repositories::checkout(&repo, first_commit.id).await?;
assert!(!world_file.exists());
let status = repositories::status(&repo).await?;
assert!(status.is_clean());
Ok(())
})
.await
}
#[tokio::test]
async fn test_command_checkout_commit_then_merge_main() -> 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")?;
repositories::add(&repo, &hello_file).await?;
let first_commit = repositories::commit(&repo, "Added hello.txt")?;
let world_file = repo.path.join("world.txt");
util::fs::write_to_path(&world_file, "World")?;
repositories::add(&repo, &world_file).await?;
let _ = repositories::commit(&repo, "Adding world")?;
let branch_name = "feature/my-branch";
repositories::branches::create_checkout(&repo, branch_name)?;
let branch_file = repo.path.join("branch.txt");
util::fs::write_to_path(&branch_file, "Branch file")?;
repositories::add(&repo, &branch_file).await?;
repositories::commit(&repo, "Adding branch file")?;
assert!(branch_file.exists());
repositories::checkout(&repo, first_commit.id).await?;
assert!(!branch_file.exists());
assert!(!world_file.exists());
let status = repositories::status(&repo).await?;
assert!(status.is_clean());
repositories::checkout(&repo, branch_name).await?;
let head_before_merge = repositories::commits::head_commit(&repo)?;
let merge_result = repositories::merge::merge(&repo, DEFAULT_BRANCH_NAME)
.await?
.expect("merge should succeed (already up to date)");
assert_eq!(merge_result.id, head_before_merge.id);
let head_after_merge = repositories::commits::head_commit(&repo)?;
assert_eq!(head_after_merge.id, head_before_merge.id);
assert!(world_file.exists());
assert!(hello_file.exists());
Ok(())
})
.await
}
#[tokio::test]
async fn test_command_checkout_current_branch_name_does_nothing() -> 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")?;
repositories::add(&repo, &hello_file).await?;
repositories::commit(&repo, "Added hello.txt")?;
let branch_name = "feature/world-explorer";
repositories::branches::create_checkout(&repo, branch_name)?;
repositories::checkout(&repo, branch_name).await?;
Ok(())
})
.await
}
#[tokio::test]
async fn test_cannot_checkout_branch_with_dots_in_name() -> 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")?;
repositories::add(&repo, &hello_file).await?;
repositories::commit(&repo, "Added hello.txt")?;
let branch_name = "test..ing";
let result = repositories::branches::create_checkout(&repo, branch_name);
assert!(result.is_err());
Ok(())
})
.await
}
#[tokio::test]
async fn test_command_checkout_added_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")?;
repositories::add(&repo, &hello_file).await?;
repositories::commit(&repo, "Added hello.txt")?;
let orig_branch = repositories::branches::current_branch(&repo)?.unwrap();
let branch_name = "feature/world-explorer";
repositories::branches::create_checkout(&repo, branch_name)?;
let world_file = repo.path.join("world.txt");
util::fs::write_to_path(&world_file, "World")?;
repositories::add(&repo, &world_file).await?;
repositories::commit(&repo, "Added world.txt")?;
let commits = repositories::commits::list(&repo)?;
assert_eq!(commits.len(), 2);
let branches = repositories::branches::list(&repo)?;
assert_eq!(branches.len(), 2);
assert!(hello_file.exists());
assert!(world_file.exists());
repositories::checkout(&repo, orig_branch.name).await?;
assert!(hello_file.exists());
assert!(!world_file.exists());
repositories::checkout(&repo, branch_name).await?;
assert!(hello_file.exists());
assert!(world_file.exists());
Ok(())
})
.await
}
#[tokio::test]
async fn test_command_merge_does_not_overwrite_modified_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")?;
repositories::add(&repo, &hello_file).await?;
repositories::commit(&repo, "Added hello.txt")?;
let orig_branch = repositories::branches::current_branch(&repo)?.unwrap();
let branch_name = "feature/world-explorer";
repositories::branches::create_checkout(&repo, branch_name)?;
let world_file = repo.path.join("world.txt");
util::fs::write_to_path(&world_file, "World")?;
repositories::add(&repo, &world_file).await?;
repositories::commit(&repo, "Added world.txt")?;
let hello_file = test::modify_txt_file(hello_file, "Hello from branch")?;
repositories::add(&repo, &hello_file).await?;
repositories::commit(&repo, "Changed hello.txt on branch")?;
repositories::checkout(&repo, orig_branch.name).await?;
let hello_file = test::modify_txt_file(hello_file, "Hello from main")?;
let result = repositories::merge::merge(&repo, branch_name).await;
assert!(result.is_err());
assert_eq!(util::fs::read_from_path(&hello_file)?, "Hello from main");
Ok(())
})
.await
}
#[tokio::test]
async fn test_command_merge_does_not_overwrite_new_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")?;
repositories::add(&repo, &hello_file).await?;
repositories::commit(&repo, "Added hello.txt")?;
let orig_branch = repositories::branches::current_branch(&repo)?.unwrap();
let branch_name = "feature/world-explorer";
repositories::branches::create_checkout(&repo, branch_name)?;
let world_file = repo.path.join("world.txt");
util::fs::write_to_path(&world_file, "World")?;
repositories::add(&repo, &world_file).await?;
repositories::commit(&repo, "Added world.txt")?;
let hello_file = test::modify_txt_file(hello_file, "Hello from branch")?;
repositories::add(&repo, &hello_file).await?;
repositories::commit(&repo, "Changed hello.txt on branch")?;
repositories::checkout(&repo, orig_branch.name).await?;
let new_file = repo.path.join("new_file.txt");
util::fs::write_to_path(&new_file, "New file")?;
repositories::merge::merge(&repo, branch_name).await?;
assert_eq!(util::fs::read_from_path(&new_file)?, "New file");
Ok(())
})
.await
}
#[tokio::test]
async fn test_command_checkout_added_file_keep_untracked() -> 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 keep_file = repo.path.join("keep_me.txt");
util::fs::write_to_path(&keep_file, "I am untracked, don't remove me")?;
repositories::add(&repo, &hello_file).await?;
repositories::commit(&repo, "Added hello.txt")?;
let orig_branch = repositories::branches::current_branch(&repo)?.unwrap();
let branch_name = "feature/world-explorer";
repositories::branches::create_checkout(&repo, branch_name)?;
let world_file = repo.path.join("world.txt");
util::fs::write_to_path(&world_file, "World")?;
repositories::add(&repo, &world_file).await?;
repositories::commit(&repo, "Added world.txt")?;
let commits = repositories::commits::list(&repo)?;
assert_eq!(commits.len(), 2);
let branches = repositories::branches::list(&repo)?;
assert_eq!(branches.len(), 2);
assert!(hello_file.exists());
assert!(world_file.exists());
assert!(keep_file.exists());
repositories::checkout(&repo, orig_branch.name).await?;
assert!(hello_file.exists());
assert!(!world_file.exists());
assert!(keep_file.exists());
repositories::checkout(&repo, branch_name).await?;
assert!(hello_file.exists());
assert!(world_file.exists());
assert!(keep_file.exists());
Ok(())
})
.await
}
#[tokio::test]
async fn test_command_checkout_modified_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")?;
repositories::add(&repo, &hello_file).await?;
repositories::commit(&repo, "Added hello.txt")?;
let orig_branch = repositories::branches::current_branch(&repo)?.unwrap();
let branch_name = "feature/world-explorer";
repositories::branches::create_checkout(&repo, branch_name)?;
let hello_file = test::modify_txt_file(hello_file, "World")?;
repositories::add(&repo, &hello_file).await?;
repositories::commit(&repo, "Changed file to world")?;
assert_eq!(util::fs::read_from_path(&hello_file)?, "World");
repositories::checkout(&repo, orig_branch.name).await?;
log::debug!("HELLO FILE NAME: {hello_file:?}");
assert!(hello_file.exists());
assert_eq!(util::fs::read_from_path(&hello_file)?, "Hello");
Ok(())
})
.await
}
#[tokio::test]
async fn test_command_checkout_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, &one_shot_path).await?;
repositories::commit(&repo, "Adding one shot")?;
let orig_branch = repositories::branches::current_branch(&repo)?.unwrap();
let og_content = util::fs::read_from_path(&one_shot_path)?;
let branch_name = "feature/change-the-shot";
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).await?;
assert_eq!(status.modified_files.len(), 1);
status.print();
repositories::add(&repo, &one_shot_path).await?;
let status = repositories::status(&repo).await?;
status.print();
repositories::commit(&repo, "Changing one shot")?;
repositories::checkout(&repo, orig_branch.name).await?;
let updated_content = util::fs::read_from_path(&one_shot_path)?;
assert_eq!(og_content, updated_content);
repositories::checkout(&repo, branch_name).await?;
let updated_content = util::fs::read_from_path(&one_shot_path)?;
assert_eq!(file_contents, updated_content);
Ok(())
})
.await
}
#[tokio::test]
async fn test_command_checkout_modified_file_from_fully_committed_repo() -> 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 orig_branch = repositories::branches::current_branch(&repo)?.unwrap();
let og_content = util::fs::read_from_path(&one_shot_path)?;
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).await?;
assert_eq!(status.modified_files.len(), 1);
repositories::add(&repo, &one_shot_path).await?;
let status = repositories::status(&repo).await?;
status.print();
assert_eq!(status.modified_files.len(), 0);
assert_eq!(status.staged_files.len(), 1);
let status = repositories::status(&repo).await?;
status.print();
repositories::commit(&repo, "Changing one shot")?;
repositories::checkout(&repo, orig_branch.name).await?;
let updated_content = util::fs::read_from_path(&one_shot_path)?;
assert_eq!(og_content, updated_content);
repositories::checkout(&repo, branch_name).await?;
let updated_content = util::fs::read_from_path(&one_shot_path)?;
assert_eq!(file_contents, updated_content);
Ok(())
})
.await
}
#[tokio::test]
async fn test_command_remove_dir_then_revert() -> Result<(), OxenError> {
test::run_select_data_repo_test_no_commits_async("train", |repo| async move {
let dir_to_remove = repo.path.join("train");
let og_num_files = util::fs::rcount_files_in_dir(&dir_to_remove);
repositories::add(&repo, &dir_to_remove).await?;
repositories::commit(&repo, "Adding train dir")?;
let orig_branch = repositories::branches::current_branch(&repo)?.unwrap();
let branch_name = "feature/removing-train";
repositories::branches::create_checkout(&repo, branch_name)?;
util::fs::remove_dir_all(&dir_to_remove)?;
repositories::add(&repo, &dir_to_remove).await?;
repositories::commit(&repo, "Removing train dir")?;
repositories::checkout(&repo, orig_branch.name).await?;
assert!(dir_to_remove.exists());
assert_eq!(util::fs::rcount_files_in_dir(&dir_to_remove), og_num_files);
repositories::checkout(&repo, branch_name).await?;
assert!(!dir_to_remove.exists());
Ok(())
})
.await
}
#[tokio::test]
async fn test_checkout_deleted_after_clone() -> Result<(), OxenError> {
test::run_training_data_fully_sync_remote(|local_repo, remote_repo| async move {
let cloned_remote = remote_repo.clone();
let og_commits = repositories::commits::list_all(&local_repo)?;
test::run_empty_dir_test_async(|new_repo_dir| async move {
let cloned_repo = repositories::clone_url(
&remote_repo.remote.url,
&new_repo_dir.join("new_repo"),
)
.await?;
let cloned_commits = repositories::commits::list_all(&cloned_repo)?;
assert_eq!(og_commits.len(), cloned_commits.len());
let head_commit = repositories::commits::head_commit(&cloned_repo);
assert!(head_commit.is_ok());
let test_dir_path = cloned_repo.path.join("test");
let commit = repositories::commits::first_by_message(&cloned_repo, "Adding test/")?;
assert!(commit.is_some());
assert!(!test_dir_path.exists());
repositories::checkout(&cloned_repo, &commit.unwrap().id).await?;
assert!(test_dir_path.exists());
let test_dir_files = util::fs::list_files_in_dir(&test_dir_path).await?;
println!("test_dir_files: {:?}", test_dir_files.len());
for file in test_dir_files.iter() {
println!("file: {file:?}");
}
assert_eq!(test_dir_files.len(), 4);
assert!(test_dir_path.join("1.jpg").exists());
assert!(test_dir_path.join("2.jpg").exists());
assert!(test_dir_path.join("3.jpg").exists());
assert!(test_dir_path.join("4.jpg").exists());
Ok(())
})
.await?;
Ok(cloned_remote)
})
.await
}
#[tokio::test]
async fn test_clone_checkout_old_commit_checkout_new_commit() -> Result<(), OxenError> {
test::run_training_data_fully_sync_remote(|_, remote_repo| async move {
let remote_repo_copy = remote_repo.clone();
test::run_empty_dir_test_async(|repo_dir| async move {
let cloned_repo =
repositories::clone_url(&remote_repo.remote.url, &repo_dir.join("new_repo"))
.await?;
let commits = repositories::commits::list(&cloned_repo)?;
for commit in commits.iter().rev() {
println!(
"TEST checking out commit: {} -> '{}'",
commit.id, commit.message
);
repositories::checkout(&cloned_repo, &commit.id).await?;
}
Ok(())
})
.await?;
Ok(remote_repo_copy)
})
.await
}
#[tokio::test]
async fn test_checkout_local_does_not_remove_untracked_files() -> Result<(), OxenError> {
test::run_one_commit_sync_repo_test(|_, remote_repo| async move {
let remote_repo_copy = remote_repo.clone();
test::run_empty_dir_test_async(|user_a_repo_dir| async move {
let user_a_repo_dir_copy = user_a_repo_dir.join("user_a_repo");
let user_a_repo =
repositories::clone_url(&remote_repo.remote.url, &user_a_repo_dir_copy).await?;
let branch_name = "test-branch";
repositories::branches::create_checkout(&user_a_repo, branch_name)?;
repositories::checkout(&user_a_repo, DEFAULT_BRANCH_NAME).await?;
let file_1 = user_a_repo.path.join("file_1.txt");
let dir_1 = user_a_repo.path.join("dir_1");
let file_in_dir_1 = dir_1.join("file_in_dir_1.txt");
let dir_2 = user_a_repo.path.join("dir_2");
let subdir_2 = dir_2.join("subdir_2");
let file_in_dir_2 = subdir_2.join("file_in_dir_2.txt");
let file_in_subdir_2 = subdir_2.join("file_in_subdir_2.txt");
std::fs::create_dir(&dir_1)?;
std::fs::create_dir(&dir_2)?;
std::fs::create_dir(&subdir_2)?;
test::write_txt_file_to_path(&file_1, "this is file 1")?;
test::write_txt_file_to_path(&file_in_dir_1, "this is file in dir 1")?;
test::write_txt_file_to_path(&file_in_dir_2, "this is file in dir 2")?;
test::write_txt_file_to_path(&file_in_subdir_2, "this is file in subdir 2")?;
repositories::checkout(&user_a_repo, branch_name).await?;
assert!(file_1.exists());
assert!(file_in_dir_1.exists());
assert!(file_in_dir_2.exists());
assert!(file_in_subdir_2.exists());
Ok(())
})
.await?;
Ok(remote_repo_copy)
})
.await
}
#[tokio::test]
async fn test_checkout_remote_does_not_remove_untracked_files() -> 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(|new_repo_dir| async move {
let cloned_repo = repositories::deep_clone_url(
&remote_repo.remote.url,
&new_repo_dir.join("new_repo"),
)
.await?;
let file_1 = cloned_repo.path.join("file_1.txt");
let dir_1 = cloned_repo.path.join("dir_1");
let file_in_dir_1 = dir_1.join("file_in_dir_1.txt");
let dir_2 = cloned_repo.path.join("dir_2");
let subdir_2 = dir_2.join("subdir_2");
let file_in_dir_2 = subdir_2.join("file_in_dir_2.txt");
std::fs::create_dir(&dir_1)?;
std::fs::create_dir(&dir_2)?;
std::fs::create_dir(&subdir_2)?;
test::write_txt_file_to_path(&file_1, "this is file 1")?;
test::write_txt_file_to_path(&file_in_dir_1, "this is file in dir 1")?;
test::write_txt_file_to_path(&file_in_dir_2, "this is file in dir 2")?;
let branch_name = "test-branch";
api::client::branches::create_from_branch(
&remote_repo,
branch_name,
DEFAULT_BRANCH_NAME,
)
.await?;
repositories::fetch_all(&cloned_repo, &FetchOpts::new()).await?;
repositories::checkout(&cloned_repo, branch_name).await?;
assert!(file_1.exists());
assert!(file_in_dir_1.exists());
assert!(file_in_dir_2.exists());
Ok(())
})
.await?;
Ok(cloned_remote)
})
.await
}
#[tokio::test]
async fn test_checkout_old_commit_does_not_overwrite_untracked_files() -> Result<(), OxenError>
{
test::run_training_data_fully_sync_remote(|_local_repo, remote_repo| async move {
let branch_name = "test-branch";
api::client::branches::create_from_branch(
&remote_repo,
branch_name,
DEFAULT_BRANCH_NAME,
)
.await?;
let cloned_remote = remote_repo.clone();
test::run_empty_dir_test_async(|new_repo_dir| async move {
let cloned_repo = repositories::deep_clone_url(
&remote_repo.remote.url,
&new_repo_dir.join("new_repo"),
)
.await?;
let test_dir_path = cloned_repo.path.join("test");
let commit = repositories::commits::first_by_message(&cloned_repo, "Adding test/")?;
let file_1 = cloned_repo.path.join("file_1.txt");
let dir_1 = cloned_repo.path.join("dir_1");
let file_in_dir_1 = dir_1.join("file_in_dir_1.txt");
let dir_2 = cloned_repo.path.join("dir_2");
let subdir_2 = dir_2.join("subdir_2");
let file_in_dir_2 = subdir_2.join("file_in_dir_2.txt");
std::fs::create_dir(&dir_1)?;
std::fs::create_dir(&dir_2)?;
std::fs::create_dir(&subdir_2)?;
test::write_txt_file_to_path(&file_1, "this is file 1")?;
test::write_txt_file_to_path(&file_in_dir_1, "this is file in dir 1")?;
test::write_txt_file_to_path(&file_in_dir_2, "this is file in dir 2")?;
assert!(commit.is_some());
assert!(!test_dir_path.exists());
repositories::checkout(&cloned_repo, &commit.unwrap().id).await?;
assert!(test_dir_path.exists());
assert!(file_1.exists());
assert!(file_in_dir_1.exists());
assert!(file_in_dir_2.exists());
Ok(())
})
.await?;
Ok(cloned_remote)
})
.await
}
#[tokio::test]
async fn test_checkout_preserves_uncommitted_file_deletion() -> 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")?;
repositories::add(&repo, &repo.path).await?;
repositories::commit(&repo, "Adding files")?;
let orig_branch = repositories::branches::current_branch(&repo)?.unwrap();
let branch_name = "feature/new-stuff";
repositories::branches::create_checkout(&repo, branch_name)?;
let world_file = test::modify_txt_file(world_file, "World modified")?;
let new_file = repo.path.join("new.txt");
util::fs::write_to_path(&new_file, "New")?;
repositories::add(&repo, &repo.path).await?;
repositories::commit(&repo, "Modified world.txt and added new.txt")?;
repositories::checkout(&repo, &orig_branch.name).await?;
std::fs::remove_file(&hello_file)?;
assert!(!hello_file.exists());
repositories::checkout(&repo, branch_name).await?;
assert!(!hello_file.exists());
assert!(world_file.exists());
assert!(new_file.exists());
Ok(())
})
.await
}
#[tokio::test]
async fn test_checkout_conflicts_on_uncommitted_deletion_of_modified_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")?;
repositories::add(&repo, &hello_file).await?;
repositories::commit(&repo, "Adding hello.txt")?;
let orig_branch = repositories::branches::current_branch(&repo)?.unwrap();
let branch_name = "feature/modify-hello";
repositories::branches::create_checkout(&repo, branch_name)?;
test::modify_txt_file(hello_file.clone(), "Hello from feature")?;
repositories::add(&repo, &hello_file).await?;
repositories::commit(&repo, "Modified hello.txt")?;
repositories::checkout(&repo, &orig_branch.name).await?;
std::fs::remove_file(&hello_file)?;
assert!(!hello_file.exists());
let result = repositories::checkout(&repo, branch_name).await;
assert!(result.is_err());
Ok(())
})
.await
}
#[tokio::test]
async fn test_checkout_removes_branch_specific_file_when_switching() -> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let common_file = repo.path.join("common.txt");
util::fs::write_to_path(&common_file, "common content")?;
repositories::add(&repo, &repo.path).await?;
repositories::commit(&repo, "Initial commit with common.txt")?;
let main_branch = repositories::branches::current_branch(&repo)?.unwrap();
let branch_name = "feature/add-new";
repositories::branches::create_checkout(&repo, branch_name)?;
let new_file = repo.path.join("feature_only.txt");
util::fs::write_to_path(&new_file, "only on feature branch")?;
repositories::add(&repo, &repo.path).await?;
repositories::commit(&repo, "Add feature_only.txt")?;
assert!(new_file.exists());
let untracked_file = repo.path.join("untracked.txt");
util::fs::write_to_path(&untracked_file, "I will be deleted")?;
assert!(untracked_file.exists());
std::fs::remove_file(&untracked_file)?;
assert!(!untracked_file.exists());
repositories::checkout(&repo, &main_branch.name).await?;
assert!(
!new_file.exists(),
"feature_only.txt should be removed when checking out main"
);
assert!(
common_file.exists(),
"common.txt should exist when checking out"
);
Ok(())
})
.await
}
#[tokio::test]
async fn test_checkout_restores_directory_from_target_branch() -> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let dir = repo.path.join("data");
std::fs::create_dir_all(&dir)?;
let file1 = dir.join("file1.txt");
let file2 = dir.join("file2.txt");
util::fs::write_to_path(&file1, "data file 1")?;
util::fs::write_to_path(&file2, "data file 2")?;
repositories::add(&repo, &repo.path).await?;
repositories::commit(&repo, "Add data directory")?;
let main_branch = repositories::branches::current_branch(&repo)?.unwrap();
let branch_name = "feature/no-data";
repositories::branches::create_checkout(&repo, branch_name)?;
std::fs::remove_dir_all(&dir)?;
repositories::add(&repo, &repo.path).await?;
repositories::commit(&repo, "Remove data directory")?;
assert!(!dir.exists());
repositories::checkout(&repo, &main_branch.name).await?;
assert!(
dir.exists(),
"data/ directory should be restored when checking out main"
);
assert!(
file1.exists(),
"data/file1.txt should be restored when checking out main"
);
assert!(
file2.exists(),
"data/file2.txt should be restored when checking out main"
);
Ok(())
})
.await
}
#[tokio::test]
async fn test_checkout_roundtrip_restores_all_files() -> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let common_file = repo.path.join("common.txt");
util::fs::write_to_path(&common_file, "common")?;
repositories::add(&repo, &repo.path).await?;
repositories::commit(&repo, "Initial commit")?;
let main_branch = repositories::branches::current_branch(&repo)?.unwrap();
let branch_name = "feature/extras";
repositories::branches::create_checkout(&repo, branch_name)?;
let extra_file = repo.path.join("extra.txt");
util::fs::write_to_path(&extra_file, "extra content")?;
let subdir = repo.path.join("subdir");
std::fs::create_dir_all(&subdir)?;
let nested_file = subdir.join("nested.txt");
util::fs::write_to_path(&nested_file, "nested content")?;
let datadir = repo.path.join("datadir");
std::fs::create_dir_all(&datadir)?;
let data_a = datadir.join("a.txt");
let data_b = datadir.join("b.txt");
util::fs::write_to_path(&data_a, "data a")?;
util::fs::write_to_path(&data_b, "data b")?;
repositories::add(&repo, &repo.path).await?;
repositories::commit(&repo, "Add extra files")?;
repositories::checkout(&repo, &main_branch.name).await?;
assert!(common_file.exists(), "common.txt should exist on main");
assert!(!extra_file.exists(), "extra.txt should NOT exist on main");
assert!(
!nested_file.exists(),
"subdir/nested.txt should NOT exist on main"
);
assert!(!datadir.exists(), "datadir/ should NOT exist on main");
repositories::checkout(&repo, branch_name).await?;
assert!(common_file.exists(), "common.txt should exist on feature");
assert!(
extra_file.exists(),
"extra.txt should be restored on feature"
);
assert!(
nested_file.exists(),
"subdir/nested.txt should be restored on feature"
);
assert!(
datadir.is_dir(),
"datadir/ should be restored as a directory on feature"
);
assert!(
data_a.exists(),
"datadir/a.txt should be restored on feature"
);
assert!(
data_b.exists(),
"datadir/b.txt should be restored on feature"
);
repositories::checkout(&repo, &main_branch.name).await?;
assert!(
!extra_file.exists(),
"extra.txt should be removed again on main"
);
assert!(
!nested_file.exists(),
"subdir/nested.txt should be removed again on main"
);
assert!(
!datadir.exists(),
"datadir/ should be removed again on main"
);
Ok(())
})
.await
}
#[tokio::test]
async fn test_checkout_restores_deleted_directory_same_branch_content() -> Result<(), OxenError>
{
test::run_empty_local_repo_test_async(|repo| async move {
let dir = repo.path.join("models");
std::fs::create_dir_all(&dir)?;
let model_file = dir.join("model.bin");
util::fs::write_to_path(&model_file, "model data")?;
let config_file = dir.join("config.json");
util::fs::write_to_path(&config_file, r#"{"lr": 0.01}"#)?;
repositories::add(&repo, &repo.path).await?;
repositories::commit(&repo, "Add models directory")?;
let main_branch = repositories::branches::current_branch(&repo)?.unwrap();
let branch_name = "feature/new-stuff";
repositories::branches::create_checkout(&repo, branch_name)?;
let new_file = repo.path.join("new_feature.txt");
util::fs::write_to_path(&new_file, "new feature")?;
repositories::add(&repo, &repo.path).await?;
repositories::commit(&repo, "Add new_feature.txt")?;
repositories::checkout(&repo, &main_branch.name).await?;
std::fs::remove_dir_all(&dir)?;
assert!(!dir.exists());
repositories::checkout(&repo, branch_name).await?;
assert!(
dir.exists(),
"models/ directory should be restored when checking out feature branch"
);
assert!(model_file.exists(), "models/model.bin should be restored");
assert!(
config_file.exists(),
"models/config.json should be restored"
);
assert!(
new_file.exists(),
"new_feature.txt should exist on feature branch"
);
std::fs::remove_dir_all(&dir)?;
assert!(!dir.exists(), "models/ should be deleted");
repositories::checkout(&repo, branch_name).await?;
assert!(
!dir.exists(),
"models/ should remain deleted — same-branch checkout is a no-op"
);
repositories::checkout(&repo, &main_branch.name).await?;
repositories::checkout(&repo, branch_name).await?;
assert!(
dir.exists(),
"models/ directory should be restored after round-trip checkout"
);
assert!(
model_file.exists(),
"models/model.bin should be restored after round-trip checkout"
);
assert!(
config_file.exists(),
"models/config.json should be restored after round-trip checkout"
);
Ok(())
})
.await
}
#[tokio::test]
async fn test_checkout_removes_duplicate_content_file_at_different_path()
-> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let file1 = repo.path.join("file1.txt");
util::fs::write_to_path(&file1, "shared content")?;
repositories::add(&repo, &repo.path).await?;
repositories::commit(&repo, "Add file1.txt on main")?;
let main_branch = repositories::branches::current_branch(&repo)?.unwrap();
let branch_name = "feature/dup-content";
repositories::branches::create_checkout(&repo, branch_name)?;
let file2 = repo.path.join("file2.txt");
util::fs::write_to_path(&file2, "shared content")?;
repositories::add(&repo, &repo.path).await?;
repositories::commit(&repo, "Add file2.txt with same content as file1.txt")?;
assert!(file1.exists(), "file1.txt should exist on feature branch");
assert!(file2.exists(), "file2.txt should exist on feature branch");
repositories::checkout(&repo, &main_branch.name).await?;
assert!(file1.exists(), "file1.txt should exist on main");
assert!(
!file2.exists(),
"file2.txt should be removed on main even though it shares content hash with file1.txt"
);
Ok(())
})
.await
}
#[tokio::test]
async fn test_checkout_restores_directory_when_file_exists_at_same_path()
-> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let dir = repo.path.join("data");
std::fs::create_dir_all(&dir)?;
let file1 = dir.join("file1.txt");
let file2 = dir.join("file2.txt");
util::fs::write_to_path(&file1, "data file 1")?;
util::fs::write_to_path(&file2, "data file 2")?;
repositories::add(&repo, &repo.path).await?;
repositories::commit(&repo, "Add data directory with files")?;
let main_branch = repositories::branches::current_branch(&repo)?.unwrap();
repositories::branches::create_checkout(&repo, "feature/shared-data")?;
let extra = repo.path.join("extra.txt");
util::fs::write_to_path(&extra, "extra")?;
repositories::add(&repo, &repo.path).await?;
repositories::commit(&repo, "Add extra.txt, data/ is unchanged")?;
assert!(dir.is_dir());
assert!(file1.exists());
assert!(file2.exists());
std::fs::remove_dir_all(&dir)?;
util::fs::write_to_path(repo.path.join("data"), "I am a plain file, not a dir")?;
let data_path = repo.path.join("data");
assert!(data_path.is_file(), "data should be a regular file");
assert!(!data_path.is_dir(), "data should NOT be a directory");
repositories::checkout(&repo, &main_branch.name).await?;
assert!(
dir.is_dir(),
"data/ should be restored as a directory when checking out main"
);
assert!(
file1.exists(),
"data/file1.txt should be restored when checking out main"
);
assert!(
file2.exists(),
"data/file2.txt should be restored when checking out main"
);
Ok(())
})
.await
}
}