diff --git a/lib/src/conflicts.rs b/lib/src/conflicts.rs index b0a84e7b2..a4940a67e 100644 --- a/lib/src/conflicts.rs +++ b/lib/src/conflicts.rs @@ -14,17 +14,20 @@ #![allow(missing_docs)] +use std::borrow::Borrow; use std::hash::Hash; use std::io::Write; +use std::sync::Arc; use itertools::Itertools; -use crate::backend::{BackendResult, FileId, ObjectId, TreeValue}; +use crate::backend::{BackendError, BackendResult, FileId, ObjectId, TreeId, TreeValue}; use crate::diff::{find_line_ranges, Diff, DiffHunk}; use crate::files::{ContentHunk, MergeResult}; use crate::merge::trivial_merge; use crate::repo_path::RepoPath; use crate::store::Store; +use crate::tree::Tree; use crate::{backend, files}; const CONFLICT_START_LINE: &[u8] = b"<<<<<<<\n"; @@ -366,6 +369,44 @@ impl Conflict> { } } +impl Conflict> +where + T: Borrow, +{ + /// If every non-`None` term of a `Conflict>` + /// is a `TreeValue::Tree`, this converts it to + /// a `Conflict`, with empty trees instead of + /// any `None` terms. Otherwise, returns `None`. + pub fn to_tree_conflict( + &self, + store: &Arc, + dir: &RepoPath, + ) -> Result>, BackendError> { + let tree_id_conflict = self.maybe_map(|term| match term { + None => Some(None), + Some(value) => { + if let TreeValue::Tree(id) = value.borrow() { + Some(Some(id)) + } else { + None + } + } + }); + if let Some(tree_id_conflict) = tree_id_conflict { + let get_tree = |id: &Option<&TreeId>| -> Result { + if let Some(id) = id { + store.get_tree(dir, id) + } else { + Ok(Tree::null(store.clone(), dir.clone())) + } + }; + Ok(Some(tree_id_conflict.try_map(get_tree)?)) + } else { + Ok(None) + } + } +} + fn describe_conflict_term(value: &TreeValue) -> String { match value { TreeValue::File { diff --git a/lib/src/merged_tree.rs b/lib/src/merged_tree.rs index 9e2357601..3201138ab 100644 --- a/lib/src/merged_tree.rs +++ b/lib/src/merged_tree.rs @@ -15,14 +15,16 @@ //! A lazily merged view of a set of trees. use std::cmp::max; +use std::sync::Arc; use itertools::Itertools; +use crate::backend; use crate::backend::TreeValue; use crate::conflicts::Conflict; use crate::repo_path::{RepoPath, RepoPathComponent}; use crate::store::Store; -use crate::tree::Tree; +use crate::tree::{try_resolve_file_conflict, Tree, TreeMergeError}; use crate::tree_builder::TreeBuilder; /// Presents a view of a merged set of trees. @@ -154,4 +156,104 @@ impl MergedTree { } } } + + /// Tries to resolve any conflicts, resolving any conflicts that can be + /// automatically resolved and leaving the rest unresolved. The returned + /// conflict will either be resolved or have the same number of sides as + /// the input. + pub fn resolve(&self) -> Result, TreeMergeError> { + match self { + MergedTree::Legacy(tree) => Ok(Conflict::resolved(tree.clone())), + MergedTree::Merge(conflict) => merge_trees(conflict), + } + } +} + +fn merge_trees(conflict: &Conflict) -> Result, TreeMergeError> { + if let Some(tree) = conflict.resolve_trivial() { + return Ok(Conflict::resolved(tree.clone())); + } + + let base_names = itertools::chain(conflict.removes(), conflict.adds()) + .map(|tree| tree.data().names()) + .kmerge() + .dedup(); + + let base_tree = &conflict.adds()[0]; + let store = base_tree.store(); + let dir = base_tree.dir(); + // Keep resolved entries in `new_tree` and conflicted entries in `conflicts` to + // start with. Then we'll create the full trees later, and only if there are + // any conflicts. + let mut new_tree = backend::Tree::default(); + let mut conflicts = vec![]; + for basename in base_names { + let path_conflict = conflict.map(|tree| tree.value(basename).cloned()); + let path_conflict = merge_tree_values(store, dir, path_conflict)?; + if let Some(value) = path_conflict.as_resolved() { + new_tree.set_or_remove(basename, value.clone()); + } else { + conflicts.push((basename, path_conflict)); + }; + } + if conflicts.is_empty() { + let new_tree_id = store.write_tree(dir, new_tree)?; + Ok(Conflict::resolved(new_tree_id)) + } else { + // For each side of the conflict, overwrite the entries in `new_tree` with the + // values from `conflicts`. Entries that are not in `conflicts` will remain + // unchanged and will be reused for each side. + let mut tree_removes = vec![]; + for i in 0..conflict.removes().len() { + for (basename, path_conflict) in &conflicts { + new_tree.set_or_remove(basename, path_conflict.removes()[i].clone()); + } + let tree = store.write_tree(dir, new_tree.clone())?; + tree_removes.push(tree); + } + let mut tree_adds = vec![]; + for i in 0..conflict.adds().len() { + for (basename, path_conflict) in &conflicts { + new_tree.set_or_remove(basename, path_conflict.adds()[i].clone()); + } + let tree = store.write_tree(dir, new_tree.clone())?; + tree_adds.push(tree); + } + + Ok(Conflict::new(tree_removes, tree_adds)) + } +} + +/// Tries to resolve a conflict between tree values. Returns +/// Ok(Conflict::resolved(Some(value))) if the conflict was resolved, and +/// Ok(Conflict::resolved(None)) if the path should be removed. Returns the +/// conflict unmodified if it cannot be resolved automatically. +fn merge_tree_values( + store: &Arc, + path: &RepoPath, + conflict: Conflict>, +) -> Result>, TreeMergeError> { + if let Some(resolved) = conflict.resolve_trivial() { + return Ok(Conflict::resolved(resolved.clone())); + } + + if let Some(tree_conflict) = conflict.to_tree_conflict(store, path)? { + // If all sides are trees or missing, merge the trees recursively, treating + // missing trees as empty. + let merged_tree = merge_trees(&tree_conflict)?; + if merged_tree.as_resolved().map(|tree| tree.id()) == Some(store.empty_tree_id()) { + Ok(Conflict::resolved(None)) + } else { + Ok(merged_tree.map(|tree| Some(TreeValue::Tree(tree.id().clone())))) + } + } else { + // Try to resolve file conflicts by merging the file contents. Treats missing + // files as empty. + if let Some(resolved) = try_resolve_file_conflict(store, path, &conflict)? { + Ok(Conflict::resolved(Some(resolved))) + } else { + // Failed to merge the files, or the paths are not files + Ok(conflict) + } + } } diff --git a/lib/src/tree.rs b/lib/src/tree.rs index 65e6be2b1..32bdd5efc 100644 --- a/lib/src/tree.rs +++ b/lib/src/tree.rs @@ -581,7 +581,7 @@ fn merge_tree_value( }) } -fn try_resolve_file_conflict( +pub fn try_resolve_file_conflict( store: &Store, filename: &RepoPath, conflict: &Conflict>, diff --git a/lib/tests/test_merged_tree.rs b/lib/tests/test_merged_tree.rs index 5598e7760..ff46981d6 100644 --- a/lib/tests/test_merged_tree.rs +++ b/lib/tests/test_merged_tree.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use itertools::Itertools; use jj_lib::backend::{FileId, TreeValue}; use jj_lib::conflicts::Conflict; use jj_lib::merged_tree::{MergedTree, MergedTreeValue}; @@ -178,3 +179,117 @@ fn test_from_legacy_tree() { MergedTreeValue::Resolved(tree.value(&dir1_basename)) ); } + +#[test] +fn test_resolve_success() { + let test_repo = TestRepo::init(true); + let repo = &test_repo.repo; + + let unchanged_path = RepoPath::from_internal_string("unchanged"); + let trivial_file_path = RepoPath::from_internal_string("trivial-file"); + let trivial_hunk_path = RepoPath::from_internal_string("trivial-hunk"); + let both_added_dir_path = RepoPath::from_internal_string("added-dir"); + let both_added_dir_file1_path = both_added_dir_path.join(&RepoPathComponent::from("file1")); + let both_added_dir_file2_path = both_added_dir_path.join(&RepoPathComponent::from("file2")); + let emptied_dir_path = RepoPath::from_internal_string("to-become-empty"); + let emptied_dir_file1_path = emptied_dir_path.join(&RepoPathComponent::from("file1")); + let emptied_dir_file2_path = emptied_dir_path.join(&RepoPathComponent::from("file2")); + let base1 = testutils::create_tree( + repo, + &[ + (&unchanged_path, "unchanged"), + (&trivial_file_path, "base1"), + (&trivial_hunk_path, "line1\nline2\nline3\n"), + (&emptied_dir_file1_path, "base1"), + (&emptied_dir_file2_path, "base1"), + ], + ); + let side1 = testutils::create_tree( + repo, + &[ + (&unchanged_path, "unchanged"), + (&trivial_file_path, "base1"), + (&trivial_hunk_path, "line1 side1\nline2\nline3\n"), + (&both_added_dir_file1_path, "side1"), + (&emptied_dir_file2_path, "base1"), + ], + ); + let side2 = testutils::create_tree( + repo, + &[ + (&unchanged_path, "unchanged"), + (&trivial_file_path, "side2"), + (&trivial_hunk_path, "line1\nline2\nline3 side2\n"), + (&both_added_dir_file2_path, "side2"), + (&emptied_dir_file1_path, "base1"), + ], + ); + let expected = testutils::create_tree( + repo, + &[ + (&unchanged_path, "unchanged"), + (&trivial_file_path, "side2"), + (&trivial_hunk_path, "line1 side1\nline2\nline3 side2\n"), + (&both_added_dir_file1_path, "side1"), + (&both_added_dir_file2_path, "side2"), + ], + ); + + let tree = MergedTree::new(Conflict::new(vec![base1], vec![side1, side2])); + let resolved = tree.resolve().unwrap(); + let resolved_tree = resolved.as_resolved().unwrap().clone(); + assert_eq!( + resolved_tree, + expected, + "actual entries: {:#?}, expected entries {:#?}", + resolved_tree.entries().collect_vec(), + expected.entries().collect_vec() + ); +} + +#[test] +fn test_resolve_root_becomes_empty() { + let test_repo = TestRepo::init(true); + let repo = &test_repo.repo; + let store = repo.store(); + + let path1 = RepoPath::from_internal_string("dir1/file"); + let path2 = RepoPath::from_internal_string("dir2/file"); + let base1 = testutils::create_tree(repo, &[(&path1, "base1"), (&path2, "base1")]); + let side1 = testutils::create_tree(repo, &[(&path2, "base1")]); + let side2 = testutils::create_tree(repo, &[(&path1, "base1")]); + + let tree = MergedTree::new(Conflict::new(vec![base1], vec![side1, side2])); + let resolved = tree.resolve().unwrap(); + assert_eq!(resolved.as_resolved().unwrap().id(), store.empty_tree_id()); +} + +#[test] +fn test_resolve_with_conflict() { + let test_repo = TestRepo::init(true); + let repo = &test_repo.repo; + + // The trivial conflict should be resolved but the non-trivial should not (and + // cannot) + let trivial_path = RepoPath::from_internal_string("dir1/trivial"); + let conflict_path = RepoPath::from_internal_string("dir2/file_conflict"); + let base1 = + testutils::create_tree(repo, &[(&trivial_path, "base1"), (&conflict_path, "base1")]); + let side1 = + testutils::create_tree(repo, &[(&trivial_path, "side1"), (&conflict_path, "side1")]); + let side2 = + testutils::create_tree(repo, &[(&trivial_path, "base1"), (&conflict_path, "side2")]); + let expected_base1 = + testutils::create_tree(repo, &[(&trivial_path, "side1"), (&conflict_path, "base1")]); + let expected_side1 = + testutils::create_tree(repo, &[(&trivial_path, "side1"), (&conflict_path, "side1")]); + let expected_side2 = + testutils::create_tree(repo, &[(&trivial_path, "side1"), (&conflict_path, "side2")]); + + let tree = MergedTree::new(Conflict::new(vec![base1], vec![side1, side2])); + let resolved_tree = tree.resolve().unwrap(); + assert_eq!( + resolved_tree, + Conflict::new(vec![expected_base1], vec![expected_side1, expected_side2]) + ) +}