use crate::error::OxenError;
use crate::model::{LocalRepository, PartialNode};
use crate::{api, repositories};
use colored::Colorize;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use uuid::Uuid;
pub async fn checkout(repo: &mut LocalRepository, name: &str) -> Result<(), OxenError> {
match repositories::checkout(repo, name).await {
Ok(Some(branch)) => {
repo.set_workspace(branch.name.clone())?;
repo.save()?;
}
Ok(None) => {
}
Err(OxenError::RevisionNotFound(name)) => {
println!(
"Revision not found: {name}\n\nIf the branch exists on the remote, run\n\n oxen fetch -b {name}\n\nto update the local copy, then try again."
);
return Err(OxenError::RevisionNotFound(name));
}
Err(e) => {
return Err(e);
}
}
Ok(())
}
pub async fn create_checkout(
repo: &mut LocalRepository,
branch_name: &str,
) -> Result<(), OxenError> {
let head_commit = repositories::commits::head_commit(repo)?;
let mut partial_nodes: HashMap<PathBuf, PartialNode> = HashMap::new();
let _from_root = repositories::tree::get_root_with_children_and_partial_nodes(
repo,
&head_commit,
None,
None,
None,
&mut partial_nodes,
)?
.unwrap();
let version_store = repo.version_store();
for (path, node) in partial_nodes {
let full_path = repo.path.join(&path);
if full_path.exists() {
let file = tokio::fs::File::open(&full_path).await?;
let size = file.metadata().await?.len();
let reader = tokio::io::BufReader::new(file);
version_store
.store_version_from_reader(&node.hash.to_string(), Box::new(reader), size)
.await?;
}
}
let workspace_name = create_checkout_branch(repo, branch_name).await?;
repositories::checkout(repo, branch_name).await?;
repo.set_workspace(&workspace_name)?;
repo.save()?;
Ok(())
}
pub async fn create_checkout_branch(
repo: &mut LocalRepository,
branch_name: &str,
) -> Result<String, OxenError> {
repositories::branches::create_from_head(repo, branch_name)?;
let workspace_id = Uuid::new_v4().to_string();
let workspace_name = format!("{branch_name}: {workspace_id}");
let Some(remote) = repo.remote() else {
return Err(OxenError::basic_str(
"Error: local repository has no remote",
));
};
let remote_repo = api::client::repositories::get_by_remote(&remote).await?;
let head_commit = repositories::commits::head_commit(repo)?;
api::client::branches::create_from_commit(&remote_repo, &branch_name, &head_commit).await?;
let workspace = api::client::workspaces::create_with_path(
&remote_repo,
&branch_name,
&workspace_id,
Path::new("/"),
Some(workspace_name.clone()),
)
.await?;
match workspace.status.as_str() {
"resource_created" => {
println!(
"{}",
"Remote-mode repository initialized successfully!"
.green()
.bold()
);
}
"resource_found" => {
let err_msg = format!(
"Remote-mode repo for workspace {} already exists",
workspace_id.clone()
);
println!("{}", err_msg.yellow().bold());
return Err(OxenError::basic_str(format!(
"Error: Remote-mode repo already exists for workspace {workspace_id}"
)));
}
other => {
println!("{}", format!("Unexpected workspace status: {other}").red());
}
}
println!("{} {}", "Workspace ID:".green().bold(), workspace.id.bold());
repo.add_workspace(&workspace_name);
Ok(workspace_name)
}
#[cfg(test)]
mod tests {
use crate::error::OxenError;
use crate::{api, repositories, test, util};
use crate::model::NewCommitBody;
use crate::repositories::remote_mode;
use crate::config::UserConfig;
use crate::opts::CloneOpts;
#[tokio::test]
async fn test_remote_mode_checkout_non_existant_branch() -> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|mut repo| async move {
let checkout_result =
repositories::remote_mode::checkout(&mut repo, "non-existent").await;
assert!(checkout_result.is_err());
Ok(())
})
.await
}
#[tokio::test]
async fn test_remote_mode_checkout_current_branch_name_does_nothing() -> Result<(), OxenError> {
test::run_remote_repo_test_bounding_box_csv_pushed(
|mut _local_repo, remote_repo| async move {
let remote_repo_copy = 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.is_remote = true;
let mut cloned_repo = repositories::clone(&opts).await?;
assert!(cloned_repo.is_remote_mode());
let branch_name = "feature".to_string();
repositories::remote_mode::create_checkout(&mut cloned_repo, &branch_name)
.await?;
let checkout_branch = repositories::checkout(&cloned_repo, &branch_name)
.await?
.unwrap();
assert_eq!(checkout_branch.name, branch_name);
Ok(())
})
.await?;
Ok(remote_repo_copy)
},
)
.await
}
#[tokio::test]
async fn test_remote_mode_checkout_changes_workspace() -> Result<(), OxenError> {
test::run_remote_repo_test_bounding_box_csv_pushed(|_local_repo, remote_repo| async move {
let remote_repo_copy = 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.is_remote = true;
let mut cloned_repo = repositories::clone(&opts).await?;
assert!(cloned_repo.is_remote_mode());
let orig_branch_name = repositories::branches::current_branch(&cloned_repo)?
.unwrap()
.name
.clone();
let orig_workspace_name = cloned_repo.workspace_name.clone().unwrap();
let new_branch_name = "feature/workspace-change";
remote_mode::create_checkout(&mut cloned_repo, new_branch_name).await?;
let new_workspace_name = cloned_repo.workspace_name.clone().unwrap();
assert_ne!(orig_workspace_name, new_workspace_name);
repositories::remote_mode::checkout(&mut cloned_repo, &orig_branch_name).await?;
assert_eq!(
cloned_repo.workspace_name.clone().unwrap(),
orig_workspace_name
);
assert_eq!(cloned_repo.workspace_name.unwrap(), orig_workspace_name);
Ok(())
})
.await?;
Ok(remote_repo_copy)
})
.await
}
#[tokio::test]
async fn test_remote_mode_checkout_updates_branch() -> Result<(), OxenError> {
test::run_remote_repo_test_bounding_box_csv_pushed(|_local_repo, remote_repo| async move {
let remote_repo_copy = 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.is_remote = true;
let mut cloned_repo = repositories::clone(&opts).await?;
assert!(cloned_repo.is_remote_mode());
let orig_branch_name = repositories::branches::current_branch(&cloned_repo)?
.unwrap()
.name
.clone();
let new_branch_name = "feature/workspace-change";
remote_mode::create_checkout(&mut cloned_repo, new_branch_name).await?;
let current_branch = repositories::branches::current_branch(&cloned_repo)?.unwrap();
assert_ne!(current_branch.name, orig_branch_name);
repositories::remote_mode::checkout(&mut cloned_repo, &orig_branch_name).await?;
let current_branch = repositories::branches::current_branch(&cloned_repo)?.unwrap();
assert_eq!(current_branch.name, orig_branch_name);
Ok(())
})
.await?;
Ok(remote_repo_copy)
})
.await
}
#[tokio::test]
async fn test_remote_mode_checkout_added_file_and_workspace() -> Result<(), OxenError> {
test::run_remote_repo_test_bounding_box_csv_pushed(|_local_repo, remote_repo| async move {
let remote_repo_copy = 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.is_remote = true;
let mut cloned_repo = repositories::clone(&opts).await?;
assert!(cloned_repo.is_remote_mode());
let main_branch = repositories::branches::current_branch(&cloned_repo)?.unwrap();
let hello_file = cloned_repo.path.join("hello.txt");
let file_contents = "Hello";
util::fs::write_to_path(&hello_file, file_contents)?;
let workspace_id = cloned_repo.workspace_name.clone().unwrap();
let directory = ".".to_string();
api::client::workspaces::files::add(
&remote_repo,
&workspace_id,
&directory,
vec![hello_file.clone()],
&Some(cloned_repo.clone()),
)
.await?;
let commit_body =
NewCommitBody::from_config(&UserConfig::get()?, "Added hello.txt");
let _initial_commit =
repositories::remote_mode::commit(&cloned_repo, &commit_body).await?;
let branch_name = "feature";
repositories::remote_mode::create_checkout(&mut cloned_repo, branch_name).await?;
let branch_workspace = cloned_repo.workspace_name.clone();
let world_file = cloned_repo.path.join("world.txt");
util::fs::write_to_path(&world_file, "World")?;
let current_workspace_id = cloned_repo.workspace_name.clone().unwrap();
api::client::workspaces::files::add(
&remote_repo,
¤t_workspace_id,
&directory,
vec![world_file.clone()],
&Some(cloned_repo.clone()),
)
.await?;
let commit_body =
NewCommitBody::from_config(&UserConfig::get()?, "Added world.txt");
repositories::remote_mode::commit(&cloned_repo, &commit_body).await?;
repositories::remote_mode::checkout(&mut cloned_repo, &main_branch.name).await?;
assert_ne!(cloned_repo.workspace_name, branch_workspace);
assert!(hello_file.exists());
assert!(!world_file.exists());
repositories::remote_mode::checkout(&mut cloned_repo, branch_name).await?;
assert_eq!(cloned_repo.workspace_name, branch_workspace);
assert!(hello_file.exists());
assert!(world_file.exists());
Ok(())
})
.await?;
Ok(remote_repo_copy)
})
.await
}
#[tokio::test]
async fn test_remote_mode_checkout_added_file_keep_untracked() -> Result<(), OxenError> {
test::run_remote_repo_test_bounding_box_csv_pushed(|_local_repo, remote_repo| async move {
let remote_repo_copy = 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.is_remote = true;
let mut cloned_repo = repositories::clone(&opts).await?;
assert!(cloned_repo.is_remote_mode());
let main_branch = repositories::branches::current_branch(&cloned_repo)?.unwrap();
let hello_file = cloned_repo.path.join("hello.txt");
let file_contents = "Hello";
util::fs::write_to_path(&hello_file, file_contents)?;
let workspace_id = cloned_repo.workspace_name.clone().unwrap();
let directory = ".".to_string();
api::client::workspaces::files::add(
&remote_repo,
&workspace_id,
&directory,
vec![hello_file.clone()],
&Some(cloned_repo.clone()),
)
.await?;
let commit_body =
NewCommitBody::from_config(&UserConfig::get()?, "Added hello.txt");
let _initial_commit =
repositories::remote_mode::commit(&cloned_repo, &commit_body).await?;
let keep_file = cloned_repo.path.join("keep_me.txt");
util::fs::write_to_path(&keep_file, "I am untracked, don't remove me")?;
let branch_name = "feature";
repositories::remote_mode::create_checkout(&mut cloned_repo, branch_name).await?;
let world_file = cloned_repo.path.join("world.txt");
util::fs::write_to_path(&world_file, "World")?;
let current_workspace_id = cloned_repo.workspace_name.clone().unwrap();
api::client::workspaces::files::add(
&remote_repo,
¤t_workspace_id,
&directory,
vec![world_file.clone()],
&Some(cloned_repo.clone()),
)
.await?;
let commit_body =
NewCommitBody::from_config(&UserConfig::get()?, "Added world.txt");
repositories::remote_mode::commit(&cloned_repo, &commit_body).await?;
repositories::remote_mode::checkout(&mut cloned_repo, &main_branch.name).await?;
assert!(keep_file.exists());
assert!(hello_file.exists());
assert!(!world_file.exists());
repositories::remote_mode::checkout(&mut cloned_repo, branch_name).await?;
assert!(keep_file.exists());
assert!(hello_file.exists());
assert!(world_file.exists());
Ok(())
})
.await?;
Ok(remote_repo_copy)
})
.await
}
#[tokio::test]
async fn test_remote_mode_checkout_file_in_tree_but_not_on_disk() -> Result<(), OxenError> {
test::run_remote_repo_test_bounding_box_csv_pushed(|_local_repo, remote_repo| async move {
let remote_repo_copy = 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.is_remote = true;
let mut cloned_repo = repositories::clone(&opts).await?;
assert!(cloned_repo.is_remote_mode());
let main_branch = repositories::branches::current_branch(&cloned_repo)?.unwrap();
let hello_file = cloned_repo.path.join("hello.txt");
util::fs::write_to_path(&hello_file, "Hello")?;
let workspace_id = cloned_repo.workspace_name.clone().unwrap();
let directory = ".".to_string();
api::client::workspaces::files::add(
&remote_repo,
&workspace_id,
&directory,
vec![hello_file.clone()],
&Some(cloned_repo.clone()),
)
.await?;
let commit_body =
NewCommitBody::from_config(&UserConfig::get()?, "Added hello.txt");
repositories::remote_mode::commit(&cloned_repo, &commit_body).await?;
let branch_name = "feature";
repositories::remote_mode::create_checkout(&mut cloned_repo, branch_name).await?;
let feature_file = cloned_repo.path.join("feature_only.txt");
util::fs::write_to_path(&feature_file, "I only exist on feature")?;
let current_workspace_id = cloned_repo.workspace_name.clone().unwrap();
api::client::workspaces::files::add(
&remote_repo,
¤t_workspace_id,
&directory,
vec![feature_file.clone()],
&Some(cloned_repo.clone()),
)
.await?;
let commit_body =
NewCommitBody::from_config(&UserConfig::get()?, "Added feature_only.txt");
repositories::remote_mode::commit(&cloned_repo, &commit_body).await?;
std::fs::remove_file(&feature_file)?;
assert!(!feature_file.exists());
repositories::remote_mode::checkout(&mut cloned_repo, &main_branch.name).await?;
assert!(hello_file.exists());
assert!(!feature_file.exists());
Ok(())
})
.await?;
Ok(remote_repo_copy)
})
.await
}
#[tokio::test]
async fn test_remote_mode_checkout_modified_file() -> Result<(), OxenError> {
test::run_remote_repo_test_bounding_box_csv_pushed(|_local_repo, remote_repo| async move {
let remote_repo_copy = 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.is_remote = true;
let mut cloned_repo = repositories::clone(&opts).await?;
assert!(cloned_repo.is_remote_mode());
let main_branch = repositories::branches::current_branch(&cloned_repo)?.unwrap();
let hello_file = cloned_repo.path.join("hello.txt");
let initial_content = "Hello";
util::fs::write_to_path(&hello_file, initial_content)?;
let workspace_id = cloned_repo.workspace_name.clone().unwrap();
let directory = ".".to_string();
api::client::workspaces::files::add(
&remote_repo,
&workspace_id,
&directory,
vec![hello_file.clone()],
&Some(cloned_repo.clone()),
)
.await?;
let commit_body =
NewCommitBody::from_config(&UserConfig::get()?, "Added hello.txt");
let _initial_commit =
repositories::remote_mode::commit(&cloned_repo, &commit_body).await?;
assert_eq!(util::fs::read_from_path(&hello_file)?, initial_content);
let branch_name = "feature";
repositories::remote_mode::create_checkout(&mut cloned_repo, branch_name).await?;
let modified_content = "World";
test::modify_txt_file(&hello_file, modified_content)?;
let current_workspace_id = cloned_repo.workspace_name.clone().unwrap();
api::client::workspaces::files::add(
&remote_repo,
¤t_workspace_id,
&directory,
vec![hello_file.clone()],
&Some(cloned_repo.clone()),
)
.await?;
let commit_body =
NewCommitBody::from_config(&UserConfig::get()?, "Changed file to world");
repositories::remote_mode::commit(&cloned_repo, &commit_body).await?;
assert_eq!(util::fs::read_from_path(&hello_file)?, modified_content);
repositories::remote_mode::checkout(&mut cloned_repo, &main_branch.name).await?;
assert_eq!(util::fs::read_from_path(&hello_file)?, initial_content);
repositories::remote_mode::checkout(&mut cloned_repo, branch_name).await?;
assert_eq!(util::fs::read_from_path(&hello_file)?, modified_content);
Ok(())
})
.await?;
Ok(remote_repo_copy)
})
.await
}
}