liboxen 0.9.9-alpha

Oxen is a fast, unstructured data version control, to help version datasets, written in Rust.
//! Interact with the remote repository to get information about mergability of branches
//!

use crate::api;
use crate::api::remote::client;
use crate::error::OxenError;
use crate::model::RemoteRepository;
use crate::view::merge::{Mergeable, MergeableResponse};

/// Can check the mergability of base into head
/// base or head are strings that can be branch names or commit ids
pub async fn mergability(
    remote_repo: &RemoteRepository,
    base: &str,
    head: &str,
) -> Result<Mergeable, OxenError> {
    let uri = format!("/merge/{base}..{head}");
    let url = api::endpoint::url_from_repo(remote_repo, &uri)?;
    log::debug!("url: {url}");

    let client = client::new_for_url(&url)?;
    match client.get(&url).send().await {
        Ok(res) => {
            let body = client::parse_json_body(&url, res).await?;
            log::debug!("got body: {}", body);
            let response: Result<MergeableResponse, serde_json::Error> =
                serde_json::from_str(&body);
            match response {
                Ok(val) => Ok(val.mergeable),
                Err(err) => Err(OxenError::basic_str(format!(
                    "api::remote::merger::mergability error parsing response from {url}\n\nErr {err:?} \n\n{body}"
                ))),
            }
        }
        Err(err) => {
            let err =
                format!("api::remote::merger::mergability Request failed: {url}\nErr {err:?}");
            Err(OxenError::basic_str(err))
        }
    }
}

#[cfg(test)]
mod tests {

    use crate::constants::DEFAULT_REMOTE_NAME;
    use crate::error::OxenError;
    use crate::test;
    use crate::{api, command};

    #[tokio::test]
    async fn test_remote_merger_no_commits() -> Result<(), OxenError> {
        test::run_training_data_fully_sync_remote(|local_repo, remote_repo| async move {
            let base = "main";
            let head = "add-data";

            command::create_checkout(&local_repo, head)?;
            command::push_remote_branch(&local_repo, DEFAULT_REMOTE_NAME, head).await?;

            let mergability = api::remote::merger::mergability(&remote_repo, base, head).await?;

            assert!(mergability.is_mergeable);
            assert_eq!(mergability.commits.len(), 0);

            Ok(remote_repo)
        })
        .await
    }

    #[tokio::test]
    async fn test_remote_merger_base_is_ahead() -> Result<(), OxenError> {
        test::run_training_data_fully_sync_remote(|local_repo, remote_repo| async move {
            let base = "main";
            let head = "add-data";

            command::create_checkout(&local_repo, head)?;
            command::push_remote_branch(&local_repo, DEFAULT_REMOTE_NAME, head).await?;

            // Checkout main and add a file to be ahead
            command::checkout(&local_repo, base).await?;
            let path = local_repo.path.join("file_1.txt");
            test::write_txt_file_to_path(&path, "hello")?;
            command::add(&local_repo, &path)?;
            command::commit(&local_repo, "adding file 1")?;
            command::push_remote_branch(&local_repo, DEFAULT_REMOTE_NAME, base).await?;

            let mergability = api::remote::merger::mergability(&remote_repo, base, head).await?;

            assert!(mergability.is_mergeable);
            assert_eq!(mergability.commits.len(), 0);

            Ok(remote_repo)
        })
        .await
    }

    #[tokio::test]
    async fn test_remote_merger_mergable_multiple_commits() -> Result<(), OxenError> {
        test::run_training_data_fully_sync_remote(|local_repo, remote_repo| async move {
            let base = "main";
            let head = "add-data";

            command::create_checkout(&local_repo, head)?;
            command::push_remote_branch(&local_repo, DEFAULT_REMOTE_NAME, head).await?;

            // Modify README.md
            let path = local_repo.path.join("README.md");
            test::write_txt_file_to_path(&path, "I am the README now")?;
            command::add(&local_repo, &path)?;

            // Commit twice
            let path = local_repo.path.join("file_1.txt");
            test::write_txt_file_to_path(&path, "hello")?;
            command::add(&local_repo, &path)?;
            command::commit(&local_repo, "adding file 1")?;

            let path = local_repo.path.join("file_2.txt");
            test::write_txt_file_to_path(&path, "world")?;
            command::add(&local_repo, &path)?;
            command::commit(&local_repo, "adding file 2")?;

            // Push commits
            command::push_remote_branch(&local_repo, DEFAULT_REMOTE_NAME, head).await?;

            let mergability = api::remote::merger::mergability(&remote_repo, base, head).await?;

            assert!(mergability.is_mergeable);
            assert_eq!(mergability.commits.len(), 2);
            assert_eq!(mergability.conflicts.len(), 0);

            Ok(remote_repo)
        })
        .await
    }

    #[tokio::test]
    async fn test_remote_merger_multiple_commits_conflict_head_is_ahead() -> Result<(), OxenError> {
        test::run_training_data_fully_sync_remote(|local_repo, remote_repo| async move {
            let base = "main";
            let head = "add-data";

            command::create_checkout(&local_repo, head)?;
            command::push_remote_branch(&local_repo, DEFAULT_REMOTE_NAME, head).await?;

            // Modify README.md to have a conflict
            let path = local_repo.path.join("README.md");
            test::write_txt_file_to_path(&path, "I am the README now")?;
            command::add(&local_repo, &path)?;

            // Commit twice
            let path = local_repo.path.join("file_1.txt");
            test::write_txt_file_to_path(&path, "hello")?;
            command::add(&local_repo, &path)?;
            command::commit(&local_repo, "adding file 1")?;

            let path = local_repo.path.join("file_2.txt");
            test::write_txt_file_to_path(&path, "world")?;
            command::add(&local_repo, &path)?;
            command::commit(&local_repo, "adding file 2")?;

            // Push commits
            command::push_remote_branch(&local_repo, DEFAULT_REMOTE_NAME, head).await?;

            // Checkout main and modify README.md to have a conflict
            command::checkout(&local_repo, base).await?;
            let path = local_repo.path.join("README.md");
            test::write_txt_file_to_path(&path, "I am on main conflicting the README")?;
            command::add(&local_repo, &path)?;
            command::commit(&local_repo, "modifying readme on main")?;
            command::push_remote_branch(&local_repo, DEFAULT_REMOTE_NAME, base).await?;

            let mergability = api::remote::merger::mergability(&remote_repo, base, head).await?;

            assert!(!mergability.is_mergeable);
            assert_eq!(mergability.commits.len(), 2);
            assert_eq!(mergability.conflicts.len(), 1);

            Ok(remote_repo)
        })
        .await
    }
}