use std::path::{Path, PathBuf};
use crate::core::refs::with_ref_manager;
use crate::core::versions::MinOxenVersion;
use crate::error::OxenError;
use crate::model::{Branch, Commit, CommitEntry, LocalRepository};
use crate::repositories;
use crate::{core, util};
pub fn list(repo: &LocalRepository) -> Result<Vec<Branch>, OxenError> {
with_ref_manager(repo, |manager| manager.list_branches())
}
pub fn list_with_commits(repo: &LocalRepository) -> Result<Vec<(Branch, Commit)>, OxenError> {
with_ref_manager(repo, |manager| manager.list_branches_with_commits())
}
pub fn get_by_name(repo: &LocalRepository, name: &str) -> Result<Option<Branch>, OxenError> {
with_ref_manager(repo, |manager| manager.get_branch_by_name(name))
}
pub fn get_by_name_or_current(
repo: &LocalRepository,
branch_name: Option<impl AsRef<str>>,
) -> Result<Branch, OxenError> {
if let Some(branch_name) = branch_name {
let branch_name = branch_name.as_ref();
match repositories::branches::get_by_name(repo, branch_name)? {
Some(branch) => Ok(branch),
None => Err(OxenError::local_branch_not_found(branch_name)),
}
} else {
match repositories::branches::current_branch(repo)? {
Some(branch) => Ok(branch),
None => {
log::error!("get_by_name_or_current No current branch found");
Err(OxenError::must_be_on_valid_branch())
}
}
}
}
pub fn get_commit_id(repo: &LocalRepository, name: &str) -> Result<Option<String>, OxenError> {
with_ref_manager(repo, |manager| manager.get_commit_id_for_branch(name))
}
pub fn exists(repo: &LocalRepository, name: &str) -> Result<bool, OxenError> {
match get_by_name(repo, name)? {
Some(_) => Ok(true),
None => Ok(false),
}
}
pub fn current_branch(repo: &LocalRepository) -> Result<Option<Branch>, OxenError> {
with_ref_manager(repo, |manager| manager.get_current_branch())
}
pub fn create_from_head(
repo: &LocalRepository,
name: impl AsRef<str>,
) -> Result<Branch, OxenError> {
let name = name.as_ref();
let head_commit = repositories::commits::head_commit(repo)?;
with_ref_manager(repo, |manager| manager.create_branch(name, &head_commit.id))
}
pub fn create(
repo: &LocalRepository,
name: impl AsRef<str>,
commit_id: impl AsRef<str>,
) -> Result<Branch, OxenError> {
let name = name.as_ref();
let commit_id = commit_id.as_ref();
if repositories::commits::commit_id_exists(repo, commit_id)? {
with_ref_manager(repo, |manager| manager.create_branch(name, commit_id))
} else {
Err(OxenError::commit_id_does_not_exist(commit_id))
}
}
pub fn create_checkout(repo: &LocalRepository, name: impl AsRef<str>) -> Result<Branch, OxenError> {
let name = name.as_ref();
let name = util::fs::linux_path_str(name);
println!("Create and checkout branch: {name}");
let head_commit = repositories::commits::head_commit(repo)?;
with_ref_manager(repo, |manager| {
let branch = manager.create_branch(&name, &head_commit.id)?;
manager.set_head(name);
Ok(branch)
})
}
pub fn update(
repo: &LocalRepository,
name: impl AsRef<str>,
commit_id: impl AsRef<str>,
) -> Result<Branch, OxenError> {
let name = name.as_ref();
let commit_id = commit_id.as_ref();
with_ref_manager(repo, |manager| {
if let Some(branch) = manager.get_branch_by_name(name)? {
manager.set_branch_commit_id(name, commit_id)?;
Ok(branch)
} else {
create(repo, name, commit_id)
}
})
}
pub fn delete(repo: &LocalRepository, name: impl AsRef<str>) -> Result<Branch, OxenError> {
let name = name.as_ref();
if let Ok(Some(branch)) = current_branch(repo)
&& branch.name == name
{
let err = format!("Err: Cannot delete current checked out branch '{name}'");
return Err(OxenError::basic_str(err));
}
if branch_has_been_merged(repo, name)? {
with_ref_manager(repo, |manager| manager.delete_branch(name))
} else {
let err = format!(
"Err: The branch '{name}' is not fully merged.\nIf you are sure you want to delete it, run 'oxen branch -D {name}'."
);
Err(OxenError::basic_str(err))
}
}
pub fn force_delete(repo: &LocalRepository, name: impl AsRef<str>) -> Result<Branch, OxenError> {
let name = name.as_ref();
if let Ok(Some(branch)) = current_branch(repo)
&& branch.name == name
{
let err = format!("Err: Cannot delete current checked out branch '{name}'");
return Err(OxenError::basic_str(err));
}
with_ref_manager(repo, |manager| manager.delete_branch(name))
}
pub fn is_checked_out(repo: &LocalRepository, name: &str) -> bool {
if let Ok(Some(current_branch)) = with_ref_manager(repo, |manager| manager.get_current_branch())
{
if current_branch.name == name {
return true;
}
}
false
}
pub async fn checkout_branch_from_commit(
repo: &LocalRepository,
name: impl AsRef<str>,
from_commit: &Option<Commit>,
) -> Result<(), OxenError> {
let name = name.as_ref();
log::debug!("checkout_branch {name}");
match repo.min_version() {
MinOxenVersion::V0_10_0 => panic!("v0.10.0 no longer supported"),
_ => core::v_latest::branches::checkout(repo, name, from_commit).await,
}
}
pub async fn checkout_subtrees_to_commit(
repo: &LocalRepository,
to_commit: &Commit,
subtree_paths: &[PathBuf],
depth: i32,
) -> Result<(), OxenError> {
match repo.min_version() {
MinOxenVersion::V0_10_0 => {
panic!("checkout_subtree_from_commit not implemented for oxen v0.10.0")
}
_ => {
core::v_latest::branches::checkout_subtrees(repo, to_commit, subtree_paths, depth).await
}
}
}
pub async fn checkout_commit_from_commit(
repo: &LocalRepository,
commit: &Commit,
from_commit: &Option<Commit>,
) -> Result<(), OxenError> {
match repo.min_version() {
MinOxenVersion::V0_10_0 => panic!("v0.10.0 no longer supported"),
_ => core::v_latest::branches::checkout_commit(repo, commit, from_commit).await,
}
}
pub fn set_head(repo: &LocalRepository, value: impl AsRef<str>) -> Result<(), OxenError> {
log::debug!("set_head {}", value.as_ref());
with_ref_manager(repo, |manager| {
manager.set_head(value);
Ok(())
})
}
fn branch_has_been_merged(repo: &LocalRepository, name: &str) -> Result<bool, OxenError> {
with_ref_manager(repo, |manager| {
if let Some(branch_commit_id) = manager.get_commit_id_for_branch(name)? {
if let Some(commit_id) = manager.head_commit_id()? {
let history = repositories::commits::list_from(repo, &commit_id)?;
for commit in history.iter() {
if commit.id == branch_commit_id {
return Ok(true);
}
}
Ok(false)
} else {
Ok(false)
}
} else {
let err = format!("Err: The branch '{name}' does not exist.");
Err(OxenError::basic_str(err))
}
})
}
pub fn rename_current_branch(repo: &LocalRepository, new_name: &str) -> Result<(), OxenError> {
if let Ok(Some(branch)) = current_branch(repo) {
with_ref_manager(repo, |manager| {
manager.rename_branch(&branch.name, new_name)?;
manager.set_head(new_name);
Ok(())
})
} else {
log::error!("rename_current_branch No current branch found");
Err(OxenError::must_be_on_valid_branch())
}
}
pub fn list_entry_versions_on_branch(
local_repo: &LocalRepository,
branch_name: &str,
path: &Path,
) -> Result<Vec<(Commit, CommitEntry)>, OxenError> {
let branch = repositories::branches::get_by_name(local_repo, branch_name)?
.ok_or_else(|| OxenError::local_branch_not_found(branch_name))?;
log::debug!(
"get branch commits for branch {:?} -> {}",
branch.name,
branch.commit_id
);
match local_repo.min_version() {
MinOxenVersion::V0_10_0 => panic!("v0.10.0 no longer supported"),
_ => core::v_latest::branches::list_entry_versions_for_commit(
local_repo,
&branch.commit_id,
path,
),
}
}
pub async fn set_working_repo_to_commit(
repo: &LocalRepository,
commit: &Commit,
from_commit: &Option<Commit>,
) -> Result<(), OxenError> {
match repo.min_version() {
MinOxenVersion::V0_10_0 => {
panic!("set_working_repo_to_commit not implemented for oxen v0.10.0")
}
_ => core::v_latest::branches::set_working_repo_to_commit(repo, commit, from_commit).await,
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use crate::constants::DEFAULT_BRANCH_NAME;
use crate::core::refs::with_ref_manager;
use crate::error::OxenError;
use crate::{repositories, test, util};
#[tokio::test]
async fn test_list_branch_versions_main() -> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let dir_path = Path::new("test_dir");
let dir_repo_path = repo.path.join(dir_path);
util::fs::create_dir_all(dir_repo_path)?;
let file_path = dir_path.join(Path::new("test_file.txt"));
let file_repo_path = repo.path.join(&file_path);
util::fs::write_to_path(&file_repo_path, "test")?;
repositories::add(&repo, &repo.path).await?;
let commit_1 = repositories::commit(&repo, "adding test dir")?;
let file_path_2 = Path::new("test_file_2.txt");
let file_repo_path_2 = repo.path.join(file_path_2);
util::fs::write_to_path(&file_repo_path_2, "test")?;
repositories::add(&repo, &file_repo_path_2).await?;
let commit_2 = repositories::commit(&repo, "adding test file")?;
let file_path_3 = Path::new("test_file_3.txt");
let file_repo_path_3 = repo.path.join(file_path_3);
util::fs::write_to_path(file_repo_path_3, "test 3")?;
util::fs::write_to_path(&file_repo_path_2, "something different now")?;
util::fs::write_to_path(&file_repo_path, "something different now")?;
repositories::add(&repo, &repo.path).await?;
let commit_3 = repositories::commit(&repo, "adding test file 2")?;
let _branch = repositories::branches::get_by_name(&repo, DEFAULT_BRANCH_NAME)?.unwrap();
let file_versions =
repositories::branches::list_entry_versions_on_branch(&repo, "main", &file_path)?;
let file_2_versions =
repositories::branches::list_entry_versions_on_branch(&repo, "main", file_path_2)?;
let file_3_versions =
repositories::branches::list_entry_versions_on_branch(&repo, "main", file_path_3)?;
assert_eq!(file_versions.len(), 2);
assert_eq!(file_versions[0].0.id, commit_3.id);
assert_eq!(file_versions[1].0.id, commit_1.id);
println!("commit_1: {commit_1}");
println!("commit_2: {commit_2}");
println!("commit_3: {commit_3}");
for v in &file_2_versions {
println!("file_2_versions: {:?} -> {:?}", v.0, v.1);
}
assert_eq!(file_2_versions.len(), 2);
assert_eq!(file_2_versions[0].0.id, commit_3.id);
assert_eq!(file_2_versions[1].0.id, commit_2.id);
assert_eq!(file_3_versions.len(), 1);
assert_eq!(file_3_versions[0].0.id, commit_3.id);
Ok(())
})
.await
}
#[tokio::test]
async fn test_list_branch_versions_branch_off_main() -> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let dir_path = Path::new("test_dir");
util::fs::create_dir_all(repo.path.join(dir_path))?;
let file_path = dir_path.join(Path::new("test_file.txt"));
let file_repo_path = repo.path.join(&file_path);
util::fs::write_to_path(&file_repo_path, "test")?;
repositories::add(&repo, &repo.path).await?;
let commit_1 = repositories::commit(&repo, "adding test file")?;
util::fs::write_to_path(&file_repo_path, "something different now")?;
repositories::add(&repo, &repo.path).await?;
let commit_2 = repositories::commit(&repo, "adding test file 2")?;
let file_path_2 = Path::new("test_file_2.txt");
let file_repo_path_2 = repo.path.join(file_path_2);
util::fs::write_to_path(file_repo_path_2, "test")?;
repositories::add(&repo, &repo.path).await?;
let _commit_3 = repositories::commit(&repo, "adding test file 3")?;
repositories::branches::create_checkout(&repo, "test_branch")?;
util::fs::write_to_path(&file_repo_path, "something different now again")?;
repositories::add(&repo, &repo.path).await?;
let commit_4 = repositories::commit(&repo, "adding test file 4")?;
util::fs::write_to_path(&file_repo_path, "something different now again again")?;
repositories::add(&repo, &repo.path).await?;
let commit_5 = repositories::commit(&repo, "adding test file 5")?;
with_ref_manager(&repo, |manager| {
manager.set_head(DEFAULT_BRANCH_NAME);
Ok(())
})?;
util::fs::write_to_path(&file_repo_path, "something different now again again again")?;
repositories::add(&repo, &repo.path).await?;
let commit_6 = repositories::commit(&repo, "adding test file 6")?;
let _main = repositories::branches::get_by_name(&repo, DEFAULT_BRANCH_NAME)?.unwrap();
let _branch = repositories::branches::get_by_name(&repo, "test_branch")?.unwrap();
let main_versions =
repositories::branches::list_entry_versions_on_branch(&repo, "main", &file_path)?;
let branch_versions = repositories::branches::list_entry_versions_on_branch(
&repo,
"test_branch",
&file_path.to_path_buf(),
)?;
for v in &main_versions {
println!("main: {:?} -> {:?}", v.0, v.1);
}
for v in &branch_versions {
println!("branch: {:?} -> {:?}", v.0, v.1);
}
assert_eq!(main_versions.len(), 3);
assert_eq!(main_versions[0].0.id, commit_6.id);
assert_eq!(main_versions[1].0.id, commit_2.id);
assert_eq!(main_versions[2].0.id, commit_1.id);
assert_eq!(branch_versions.len(), 4);
assert_eq!(branch_versions[0].0.id, commit_5.id);
assert_eq!(branch_versions[1].0.id, commit_4.id);
assert_eq!(branch_versions[2].0.id, commit_2.id);
assert_eq!(branch_versions[3].0.id, commit_1.id);
Ok(())
})
.await
}
#[tokio::test]
async fn test_local_delete_branch() -> Result<(), OxenError> {
test::run_one_commit_local_repo_test_async(|repo| async move {
let og_branches = repositories::branches::list(&repo)?;
let og_branch = repositories::branches::current_branch(&repo)?.unwrap();
let branch_name = "my-branch";
repositories::branches::create_checkout(&repo, branch_name)?;
repositories::checkout(&repo, og_branch.name).await?;
repositories::branches::delete(&repo, branch_name)?;
let leftover_branches = repositories::branches::list(&repo)?;
assert_eq!(og_branches.len(), leftover_branches.len());
Ok(())
})
.await
}
}