use crate::api;
use crate::constants::MERGE_DIR;
use crate::core::db;
use crate::core::index::{
oxenignore, CommitEntryReader, CommitReader, CommitWriter, MergeConflictDBReader, RefReader,
RefWriter, Stager,
};
use crate::error::OxenError;
use crate::model::{Branch, Commit, CommitEntry, LocalRepository, MergeConflict};
use crate::util;
use rocksdb::DB;
use std::path::{Path, PathBuf};
use std::str;
use super::{merge_conflict_writer, restore};
pub fn db_path(repo: &LocalRepository) -> PathBuf {
util::fs::oxen_hidden_dir(&repo.path).join(Path::new(MERGE_DIR))
}
pub struct MergeCommits {
lca: Commit,
base: Commit,
merge: Commit,
}
impl MergeCommits {
pub fn is_fast_forward_merge(&self) -> bool {
self.lca.id == self.base.id
}
}
pub struct Merger {
repository: LocalRepository,
merge_db: DB,
}
impl Merger {
pub fn new(repo: &LocalRepository) -> Result<Merger, OxenError> {
let db_path = db_path(repo);
log::debug!("Merger::new() DB {:?}", db_path);
let opts = db::opts::default();
Ok(Merger {
repository: repo.to_owned(),
merge_db: DB::open(&opts, dunce::simplified(&db_path))?,
})
}
pub fn has_conflicts(
&self,
base_branch: &Branch,
merge_branch: &Branch,
) -> Result<bool, OxenError> {
let commit_reader = CommitReader::new(&self.repository)?;
let base_commit = Commit::from_branch(&commit_reader, base_branch)?;
let merge_commit = Commit::from_branch(&commit_reader, merge_branch)?;
Ok(!self.can_merge_commits(&commit_reader, &base_commit, &merge_commit)?)
}
pub fn can_merge_commits(
&self,
commit_reader: &CommitReader,
base_commit: &Commit,
merge_commit: &Commit,
) -> Result<bool, OxenError> {
let lca =
self.lowest_common_ancestor_from_commits(commit_reader, base_commit, merge_commit)?;
let merge_commits = MergeCommits {
lca,
base: base_commit.clone(),
merge: merge_commit.clone(),
};
if merge_commits.is_fast_forward_merge() {
return Ok(true);
}
let write_to_disk = false;
let conflicts = self.find_merge_conflicts(&merge_commits, write_to_disk)?;
Ok(conflicts.is_empty())
}
pub fn list_conflicts_between_branches(
&self,
commit_reader: &CommitReader,
base_branch: &Branch,
merge_branch: &Branch,
) -> Result<Vec<PathBuf>, OxenError> {
let base_commit = Commit::from_branch(commit_reader, base_branch)?;
let merge_commit = Commit::from_branch(commit_reader, merge_branch)?;
self.list_conflicts_between_commits(commit_reader, &base_commit, &merge_commit)
}
pub fn list_commits_between_branches(
&self,
reader: &CommitReader,
base_branch: &Branch,
head_branch: &Branch,
) -> Result<Vec<Commit>, OxenError> {
log::debug!(
"list_commits_between_branches() base: {:?} head: {:?}",
base_branch,
head_branch
);
let base_commit = Commit::from_branch(reader, base_branch)?;
let head_commit = Commit::from_branch(reader, head_branch)?;
let lca = self.lowest_common_ancestor_from_commits(reader, &base_commit, &head_commit)?;
reader.history_from_base_to_head(&lca.id, &head_commit.id)
}
pub fn list_commits_between_commits(
&self,
reader: &CommitReader,
base_commit: &Commit,
head_commit: &Commit,
) -> Result<Vec<Commit>, OxenError> {
log::debug!(
"list_commits_between_commits() base: {:?} head: {:?}",
base_commit,
head_commit
);
let lca = self.lowest_common_ancestor_from_commits(reader, base_commit, head_commit)?;
log::debug!(
"For commits {:?} -> {:?} found lca {:?}",
base_commit,
head_commit,
lca
);
log::debug!("Reading history from lca to head");
reader.history_from_base_to_head(&lca.id, &head_commit.id)
}
pub fn list_conflicts_between_commits(
&self,
commit_reader: &CommitReader,
base_commit: &Commit,
merge_commit: &Commit,
) -> Result<Vec<PathBuf>, OxenError> {
let lca =
self.lowest_common_ancestor_from_commits(commit_reader, base_commit, merge_commit)?;
let merge_commits = MergeCommits {
lca,
base: base_commit.clone(),
merge: merge_commit.clone(),
};
let write_to_disk = false;
let conflicts = self.find_merge_conflicts(&merge_commits, write_to_disk)?;
Ok(conflicts
.iter()
.map(|c| c.base_entry.path.to_owned())
.collect())
}
pub fn merge(&self, branch_name: impl AsRef<str>) -> Result<Option<Commit>, OxenError> {
let branch_name = branch_name.as_ref();
let commit_reader = CommitReader::new(&self.repository)?;
let merge_branch = api::local::branches::get_by_name(&self.repository, branch_name)?
.ok_or(OxenError::local_branch_not_found(branch_name))?;
let base_commit = commit_reader.head_commit()?;
let merge_commit = Commit::from_branch(&commit_reader, &merge_branch)?;
let lca =
self.lowest_common_ancestor_from_commits(&commit_reader, &base_commit, &merge_commit)?;
let merge_commits = MergeCommits {
lca,
base: base_commit,
merge: merge_commit,
};
self.merge_commits(&merge_commits)
}
pub fn merge_into_base(
&self,
merge_branch: &Branch,
base_branch: &Branch,
) -> Result<Option<Commit>, OxenError> {
println!(
"merge_into_base merge {} into {}",
merge_branch, base_branch
);
if merge_branch.commit_id == base_branch.commit_id {
return Ok(None);
}
let commit_reader = CommitReader::new(&self.repository)?;
let base_commit = Commit::from_branch(&commit_reader, base_branch)?;
let merge_commit = Commit::from_branch(&commit_reader, merge_branch)?;
let lca =
self.lowest_common_ancestor_from_commits(&commit_reader, &base_commit, &merge_commit)?;
let merge_commits = MergeCommits {
lca,
base: base_commit,
merge: merge_commit,
};
self.merge_commits(&merge_commits)
}
pub fn merge_commit_into_base(
&self,
merge_commit: &Commit,
base_commit: &Commit,
) -> Result<Option<Commit>, OxenError> {
let commit_reader = CommitReader::new(&self.repository)?;
let lca =
self.lowest_common_ancestor_from_commits(&commit_reader, base_commit, merge_commit)?;
let merge_commits = MergeCommits {
lca,
base: base_commit.to_owned(),
merge: merge_commit.to_owned(),
};
self.merge_commits(&merge_commits)
}
fn merge_commits(&self, merge_commits: &MergeCommits) -> Result<Option<Commit>, OxenError> {
println!(
"Updating {} -> {}",
merge_commits.base.id, merge_commits.merge.id
);
log::debug!(
"FOUND MERGE COMMITS:\nLCA: {} -> {}\nBASE: {} -> {}\nMerge: {} -> {}",
merge_commits.lca.id,
merge_commits.lca.message,
merge_commits.base.id,
merge_commits.base.message,
merge_commits.merge.id,
merge_commits.merge.message,
);
if merge_commits.is_fast_forward_merge() {
println!("Fast-forward");
let commit = self.fast_forward_merge(&merge_commits.base, &merge_commits.merge)?;
Ok(Some(commit))
} else {
log::debug!(
"Three way merge! {} -> {}",
merge_commits.base.id,
merge_commits.merge.id
);
let write_to_disk = true;
let conflicts = self.find_merge_conflicts(merge_commits, write_to_disk)?;
log::debug!("Got {} conflicts", conflicts.len());
if conflicts.is_empty() {
let commit = self.create_merge_commit(merge_commits)?;
Ok(Some(commit))
} else {
merge_conflict_writer::write_conflicts_to_disk(
&self.repository,
&self.merge_db,
&merge_commits.merge,
&merge_commits.base,
&conflicts,
)?;
Ok(None)
}
}
}
pub fn has_file(&self, path: &Path) -> Result<bool, OxenError> {
MergeConflictDBReader::has_file(&self.merge_db, path)
}
pub fn remove_conflict_path(&self, path: &Path) -> Result<(), OxenError> {
let path_str = path.to_str().unwrap();
let key = path_str.as_bytes();
self.merge_db.delete(key)?;
Ok(())
}
fn create_merge_commit(&self, merge_commits: &MergeCommits) -> Result<Commit, OxenError> {
let repo = &self.repository;
let stager = Stager::new(repo)?;
let commit = api::local::commits::head_commit(repo)?;
let reader = CommitEntryReader::new(repo, &commit)?;
let ignore = oxenignore::create(repo);
stager.add(&repo.path, &reader, &ignore)?;
let commit_msg = format!(
"Merge commit {} into {}",
merge_commits.merge.id, merge_commits.base.id
);
log::debug!("create_merge_commit {}", commit_msg);
let reader = CommitEntryReader::new_from_head(repo)?;
let status = stager.status(&reader)?;
let commit_writer = CommitWriter::new(repo)?;
let parent_ids: Vec<String> = vec![
merge_commits.base.id.to_owned(),
merge_commits.merge.id.to_owned(),
];
let commit = commit_writer.commit_with_parent_ids(&status, parent_ids, &commit_msg)?;
stager.unstage()?;
Ok(commit)
}
pub fn find_merge_commits<S: AsRef<str>>(
&self,
branch_name: S,
) -> Result<MergeCommits, OxenError> {
let branch_name = branch_name.as_ref();
let ref_reader = RefReader::new(&self.repository)?;
let head_commit_id = ref_reader
.head_commit_id()?
.ok_or_else(OxenError::head_not_found)?;
let merge_commit_id = ref_reader
.get_commit_id_for_branch(branch_name)?
.ok_or_else(|| OxenError::commit_db_corrupted(branch_name))?;
let commit_reader = CommitReader::new(&self.repository)?;
let base = commit_reader
.get_commit_by_id(&head_commit_id)?
.ok_or_else(|| OxenError::commit_db_corrupted(&head_commit_id))?;
let merge = commit_reader
.get_commit_by_id(&merge_commit_id)?
.ok_or_else(|| OxenError::commit_db_corrupted(&merge_commit_id))?;
let lca = self.lowest_common_ancestor_from_commits(&commit_reader, &base, &merge)?;
Ok(MergeCommits { lca, base, merge })
}
fn fast_forward_merge(
&self,
base_commit: &Commit,
merge_commit: &Commit,
) -> Result<Commit, OxenError> {
log::debug!("FF merge!");
let base_commit_entry_reader = CommitEntryReader::new(&self.repository, base_commit)?;
let merge_commit_entry_reader = CommitEntryReader::new(&self.repository, merge_commit)?;
let base_entries = base_commit_entry_reader.list_entries_set()?;
let merge_entries = merge_commit_entry_reader.list_entries_set()?;
for merge_entry in merge_entries.iter() {
log::debug!("Merge entry: {:?}", merge_entry.path);
if let Some(base_entry) = base_entries.get(merge_entry) {
if base_entry.hash != merge_entry.hash {
log::debug!("Merge entry has changed, restore: {:?}", merge_entry.path);
self.update_entry(merge_entry)?;
}
} else {
log::debug!("Merge entry is new, restore: {:?}", merge_entry.path);
self.update_entry(merge_entry)?;
}
}
for base_entry in base_entries.iter() {
log::debug!("Base entry: {:?}", base_entry.path);
if !merge_entries.contains(base_entry) {
log::debug!("Removing Base Entry: {:?}", base_entry.path);
let path = self.repository.path.join(&base_entry.path);
if path.exists() {
util::fs::remove_file(path)?;
}
}
}
let ref_writer = RefWriter::new(&self.repository)?;
ref_writer.set_head_commit_id(&merge_commit.id)?;
Ok(merge_commit.clone())
}
pub fn lowest_common_ancestor<S: AsRef<str>>(
&self,
branch_name: S,
) -> Result<Commit, OxenError> {
let branch_name = branch_name.as_ref();
let ref_reader = RefReader::new(&self.repository)?;
let base_commit_id = ref_reader
.head_commit_id()?
.ok_or_else(OxenError::head_not_found)?;
let merge_commit_id = ref_reader
.get_commit_id_for_branch(branch_name)?
.ok_or_else(|| OxenError::commit_db_corrupted(branch_name))?;
let commit_reader = CommitReader::new(&self.repository)?;
let base_commit = commit_reader
.get_commit_by_id(&base_commit_id)?
.ok_or_else(|| OxenError::commit_db_corrupted(&base_commit_id))?;
let merge_commit = commit_reader
.get_commit_by_id(&merge_commit_id)?
.ok_or_else(|| OxenError::commit_db_corrupted(&merge_commit_id))?;
self.lowest_common_ancestor_from_commits(&commit_reader, &base_commit, &merge_commit)
}
pub fn lowest_common_ancestor_from_commits(
&self,
commit_reader: &CommitReader,
base_commit: &Commit,
merge_commit: &Commit,
) -> Result<Commit, OxenError> {
let commit_depths_from_head = commit_reader.history_with_depth_from_commit(base_commit)?;
let commit_depths_from_merge =
commit_reader.history_with_depth_from_commit(merge_commit)?;
let mut min_depth = usize::MAX;
let mut lca: Commit = commit_depths_from_head.keys().next().unwrap().clone();
for (commit, _) in commit_depths_from_merge.iter() {
if let Some(depth) = commit_depths_from_head.get(commit) {
if depth < &min_depth {
min_depth = *depth;
log::debug!("setting new lca, {:?}", commit);
lca = commit.clone();
}
}
}
Ok(lca)
}
fn find_merge_conflicts(
&self,
merge_commits: &MergeCommits,
write_to_disk: bool,
) -> Result<Vec<MergeConflict>, OxenError> {
let mut conflicts: Vec<MergeConflict> = vec![];
let lca_entry_reader = CommitEntryReader::new(&self.repository, &merge_commits.lca)?;
let base_entry_reader = CommitEntryReader::new(&self.repository, &merge_commits.base)?;
let merge_entry_reader = CommitEntryReader::new(&self.repository, &merge_commits.merge)?;
let lca_entries = lca_entry_reader.list_entries_set()?;
let base_entries = base_entry_reader.list_entries_set()?;
let merge_entries = merge_entry_reader.list_entries_set()?;
log::debug!("lca_entries.len() {}", lca_entries.len());
log::debug!("base_entries.len() {}", base_entries.len());
log::debug!("merge_entries.len() {}", merge_entries.len());
for merge_entry in merge_entries.iter() {
if let Some(base_entry) = base_entries.get(merge_entry) {
if let Some(lca_entry) = lca_entries.get(merge_entry) {
if base_entry.hash == lca_entry.hash && write_to_disk {
self.update_entry(merge_entry)?;
}
if base_entry.hash != lca_entry.hash
&& lca_entry.hash != merge_entry.hash
&& base_entry.hash != merge_entry.hash
{
conflicts.push(MergeConflict {
lca_entry: lca_entry.to_owned(),
base_entry: base_entry.to_owned(),
merge_entry: merge_entry.to_owned(),
});
}
} else {
if base_entry.hash != merge_entry.hash {
conflicts.push(MergeConflict {
lca_entry: base_entry.to_owned(),
base_entry: base_entry.to_owned(),
merge_entry: merge_entry.to_owned(),
});
}
}
} else if write_to_disk {
self.update_entry(merge_entry)?;
}
}
log::debug!("three_way_merge conflicts.len() {}", conflicts.len());
Ok(conflicts)
}
fn update_entry(&self, merge_entry: &CommitEntry) -> Result<(), OxenError> {
restore::restore_file(
&self.repository,
&merge_entry.path,
&merge_entry.commit_id,
merge_entry,
)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::api;
use crate::command;
use crate::core::index::{CommitReader, MergeConflictReader, Merger};
use crate::error::OxenError;
use crate::model::{Commit, LocalRepository};
use crate::test;
use crate::util;
async fn populate_threeway_merge_repo(
repo: &LocalRepository,
merge_branch_name: &str,
) -> Result<Commit, OxenError> {
let a_branch = api::local::branches::current_branch(repo)?.unwrap();
let a_path = repo.path.join("a.txt");
util::fs::write_to_path(&a_path, "a")?;
command::add(repo, a_path)?;
let lca = command::commit(repo, "Committing a.txt file")?;
api::local::branches::create_checkout(repo, merge_branch_name)?;
let b_path = repo.path.join("b.txt");
util::fs::write_to_path(&b_path, "b")?;
command::add(repo, b_path)?;
command::commit(repo, "Committing b.txt file")?;
command::checkout(repo, &a_branch.name).await?;
let c_path = repo.path.join("c.txt");
util::fs::write_to_path(&c_path, "c")?;
command::add(repo, c_path)?;
command::commit(repo, "Committing c.txt file")?;
let d_path = repo.path.join("d.txt");
util::fs::write_to_path(&d_path, "d")?;
command::add(repo, d_path)?;
command::commit(repo, "Committing d.txt file")?;
command::checkout(repo, merge_branch_name).await?;
let e_path = repo.path.join("e.txt");
util::fs::write_to_path(&e_path, "e")?;
command::add(repo, e_path)?;
command::commit(repo, "Committing e.txt file")?;
command::checkout(repo, &a_branch.name).await?;
Ok(lca)
}
#[tokio::test]
async fn test_merge_one_commit_add_fast_forward() -> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let og_branch = api::local::branches::current_branch(&repo)?.unwrap();
let hello_file = repo.path.join("hello.txt");
util::fs::write_to_path(&hello_file, "Hello")?;
command::add(&repo, hello_file)?;
command::commit(&repo, "Adding hello file")?;
let branch_name = "add-world";
api::local::branches::create_checkout(&repo, branch_name)?;
let world_file = repo.path.join("world.txt");
util::fs::write_to_path(&world_file, "World")?;
command::add(&repo, &world_file)?;
command::commit(&repo, "Adding world file")?;
let merge_branch = api::local::branches::current_branch(&repo)?.unwrap();
let og_branch = command::checkout(&repo, &og_branch.name).await?.unwrap();
assert!(!world_file.exists());
let merger = Merger::new(&repo)?;
let commit = merger.merge_into_base(&merge_branch, &og_branch)?.unwrap();
assert!(world_file.exists());
let head_commit = api::local::commits::head_commit(&repo)?;
assert_eq!(head_commit.id, commit.id);
Ok(())
})
.await
}
#[tokio::test]
async fn test_merge_one_commit_remove_fast_forward() -> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let og_branch = api::local::branches::current_branch(&repo)?.unwrap();
let hello_file = repo.path.join("hello.txt");
util::fs::write_to_path(&hello_file, "Hello")?;
command::add(&repo, hello_file)?;
let world_file = repo.path.join("world.txt");
util::fs::write_to_path(&world_file, "World")?;
command::add(&repo, &world_file)?;
command::commit(&repo, "Adding hello & world files")?;
let branch_name = "remove-world";
let merge_branch = api::local::branches::create_checkout(&repo, branch_name)?;
let world_file = repo.path.join("world.txt");
util::fs::remove_file(&world_file)?;
command::add(&repo, &world_file)?;
command::commit(&repo, "Removing world file")?;
command::checkout(&repo, &og_branch.name).await?;
assert!(world_file.exists());
let merger = Merger::new(&repo)?;
merger.merge(&merge_branch.name)?.unwrap();
assert!(!world_file.exists());
Ok(())
})
.await
}
#[tokio::test]
async fn test_merge_one_commit_modified_fast_forward() -> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let og_branch = api::local::branches::current_branch(&repo)?.unwrap();
let hello_file = repo.path.join("hello.txt");
util::fs::write_to_path(&hello_file, "Hello")?;
command::add(&repo, hello_file)?;
let world_file = repo.path.join("world.txt");
let og_contents = "World";
util::fs::write_to_path(&world_file, og_contents)?;
command::add(&repo, &world_file)?;
command::commit(&repo, "Adding hello & world files")?;
let branch_name = "modify-world";
api::local::branches::create_checkout(&repo, branch_name)?;
let new_contents = "Around the world";
let world_file = test::modify_txt_file(world_file, new_contents)?;
command::add(&repo, &world_file)?;
command::commit(&repo, "Modifying world file")?;
command::checkout(&repo, &og_branch.name).await?;
let contents = util::fs::read_from_path(&world_file)?;
assert_eq!(contents, og_contents);
let merger = Merger::new(&repo)?;
merger.merge(branch_name)?.unwrap();
let contents = util::fs::read_from_path(&world_file)?;
assert_eq!(contents, new_contents);
Ok(())
})
.await
}
#[tokio::test]
async fn test_merge_is_three_way_merge() -> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let merge_branch_name = "B"; populate_threeway_merge_repo(&repo, merge_branch_name).await?;
let merger = Merger::new(&repo)?;
let merge_commits = merger.find_merge_commits(merge_branch_name)?;
let is_fast_forward = merge_commits.is_fast_forward_merge();
assert!(!is_fast_forward);
Ok(())
})
.await
}
#[tokio::test]
async fn test_merge_get_lowest_common_ancestor() -> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let merge_branch_name = "B"; let lca = populate_threeway_merge_repo(&repo, merge_branch_name).await?;
let merger = Merger::new(&repo)?;
let guess = merger.lowest_common_ancestor(merge_branch_name)?;
assert_eq!(lca.id, guess.id);
Ok(())
})
.await
}
#[tokio::test]
async fn test_merge_no_conflict_three_way_merge() -> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let merge_branch_name = "B";
populate_threeway_merge_repo(&repo, merge_branch_name).await?;
{
let merger = Merger::new(&repo)?;
let merge_commit = merger.merge(merge_branch_name)?.unwrap();
assert_eq!(merge_commit.parent_ids.len(), 2);
let file_prefixes = ["a", "b", "c", "d", "e"];
for prefix in file_prefixes.iter() {
let filename = format!("{prefix}.txt");
let filepath = repo.path.join(filename);
println!(
"test_merge_no_conflict_three_way_merge checking file exists {filepath:?}"
);
assert!(filepath.exists());
}
}
let commit_reader = CommitReader::new(&repo)?;
let post_merge_history = commit_reader.history_from_head()?;
assert_eq!(7, post_merge_history.len());
Ok(())
})
.await
}
#[tokio::test]
async fn test_merge_conflict_three_way_merge() -> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let a_branch = api::local::branches::current_branch(&repo)?.unwrap();
let a_path = repo.path.join("a.txt");
util::fs::write_to_path(&a_path, "a")?;
command::add(&repo, &a_path)?;
command::commit(&repo, "Committing a.txt file")?;
let merge_branch_name = "B";
api::local::branches::create_checkout(&repo, merge_branch_name)?;
let b_path = repo.path.join("b.txt");
util::fs::write_to_path(&b_path, "b")?;
command::add(&repo, &b_path)?;
test::modify_txt_file(&a_path, "a modified from branch")?;
command::add(&repo, &a_path)?;
command::commit(&repo, "Committing b.txt file")?;
command::checkout(&repo, &a_branch.name).await?;
let c_path = repo.path.join("c.txt");
util::fs::write_to_path(&c_path, "c")?;
command::add(&repo, &c_path)?;
test::modify_txt_file(&a_path, "a modified from main line")?;
command::add(&repo, &a_path)?;
command::commit(&repo, "Committing c.txt file")?;
let d_path = repo.path.join("d.txt");
util::fs::write_to_path(&d_path, "d")?;
command::add(&repo, &d_path)?;
command::commit(&repo, "Committing d.txt file")?;
command::checkout(&repo, merge_branch_name).await?;
let e_path = repo.path.join("e.txt");
util::fs::write_to_path(&e_path, "e")?;
command::add(&repo, &e_path)?;
command::commit(&repo, "Committing e.txt file")?;
command::checkout(&repo, &a_branch.name).await?;
{
let merger = Merger::new(&repo)?;
merger.merge(merge_branch_name)?;
}
let conflict_reader = MergeConflictReader::new(&repo)?;
let has_conflicts = conflict_reader.has_conflicts()?;
let conflicts = conflict_reader.list_conflicts()?;
assert!(has_conflicts);
assert_eq!(conflicts.len(), 1);
let local_a_path = util::fs::path_relative_to_dir(&a_path, &repo.path)?;
assert_eq!(conflicts[0].base_entry.path, local_a_path);
Ok(())
})
.await
}
#[tokio::test]
async fn test_merge_conflict_three_way_merge_post_merge_branch() -> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let og_branch = api::local::branches::current_branch(&repo)?.unwrap();
let labels_path = repo.path.join("labels.txt");
util::fs::write_to_path(&labels_path, "cat\ndog")?;
command::add(&repo, &labels_path)?;
command::commit(&repo, "Add initial labels.txt file with cat and dog")?;
let fish_branch_name = "add-fish-label";
api::local::branches::create_checkout(&repo, fish_branch_name)?;
let labels_path = test::modify_txt_file(labels_path, "cat\ndog\nfish")?;
command::add(&repo, &labels_path)?;
command::commit(&repo, "Adding fish to labels.txt file")?;
command::checkout(&repo, &og_branch.name).await?;
let human_branch_name = "add-human-label";
api::local::branches::create_checkout(&repo, human_branch_name)?;
let labels_path = test::modify_txt_file(labels_path, "cat\ndog\nhuman")?;
command::add(&repo, labels_path)?;
command::commit(&repo, "Adding human to labels.txt file")?;
command::checkout(&repo, &og_branch.name).await?;
{
let merger = Merger::new(&repo)?;
merger.merge(fish_branch_name)?;
}
command::checkout(&repo, &og_branch.name).await?;
{
let merger = Merger::new(&repo)?;
merger.merge(human_branch_name)?;
}
let conflict_reader = MergeConflictReader::new(&repo)?;
let has_conflicts = conflict_reader.has_conflicts()?;
let conflicts = conflict_reader.list_conflicts()?;
assert!(has_conflicts);
assert_eq!(conflicts.len(), 1);
Ok(())
})
.await
}
#[tokio::test]
async fn test_merger_has_merge_conflicts_without_merging() -> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let og_branch = api::local::branches::current_branch(&repo)?.unwrap();
let labels_path = repo.path.join("labels.txt");
util::fs::write_to_path(&labels_path, "cat\ndog")?;
command::add(&repo, &labels_path)?;
command::commit(&repo, "Add initial labels.txt file with cat and dog")?;
let fish_branch_name = "add-fish-label";
api::local::branches::create_checkout(&repo, fish_branch_name)?;
let labels_path = test::modify_txt_file(labels_path, "cat\ndog\nfish")?;
command::add(&repo, &labels_path)?;
command::commit(&repo, "Adding fish to labels.txt file")?;
command::checkout(&repo, &og_branch.name).await?;
let human_branch_name = "add-human-label";
api::local::branches::create_checkout(&repo, human_branch_name)?;
let labels_path = test::modify_txt_file(labels_path, "cat\ndog\nhuman")?;
command::add(&repo, labels_path)?;
command::commit(&repo, "Adding human to labels.txt file")?;
command::checkout(&repo, &og_branch.name).await?;
let merger = Merger::new(&repo)?;
let result = merger.merge(fish_branch_name)?;
assert!(result.is_some());
let base_branch = api::local::branches::get_by_name(&repo, &og_branch.name)?.unwrap();
let merge_branch =
api::local::branches::get_by_name(&repo, human_branch_name)?.unwrap();
let has_conflicts = merger.has_conflicts(&base_branch, &merge_branch)?;
assert!(has_conflicts);
Ok(())
})
.await
}
#[tokio::test]
async fn test_list_merge_conflicts_without_merging() -> Result<(), OxenError> {
test::run_empty_local_repo_test_async(|repo| async move {
let og_branch = api::local::branches::current_branch(&repo)?.unwrap();
let labels_path = repo.path.join("labels.txt");
util::fs::write_to_path(&labels_path, "cat\ndog")?;
command::add(&repo, &labels_path)?;
command::commit(&repo, "Add initial labels.txt file with cat and dog")?;
let fish_branch_name = "add-fish-label";
api::local::branches::create_checkout(&repo, fish_branch_name)?;
let labels_path = test::modify_txt_file(labels_path, "cat\ndog\nfish")?;
command::add(&repo, &labels_path)?;
command::commit(&repo, "Adding fish to labels.txt file")?;
command::checkout(&repo, &og_branch.name).await?;
let human_branch_name = "add-human-label";
api::local::branches::create_checkout(&repo, human_branch_name)?;
let labels_path = test::modify_txt_file(labels_path, "cat\ndog\nhuman")?;
command::add(&repo, labels_path)?;
let human_commit = command::commit(&repo, "Adding human to labels.txt file")?;
command::checkout(&repo, &og_branch.name).await?;
let merger = Merger::new(&repo)?;
let result_commit = merger.merge(fish_branch_name)?;
assert!(result_commit.is_some());
let commit_reader = CommitReader::new(&repo)?;
let base_commit = result_commit.unwrap();
let conflicts = merger.list_conflicts_between_commits(
&commit_reader,
&base_commit,
&human_commit,
)?;
assert_eq!(conflicts.len(), 1);
Ok(())
})
.await
}
}