Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

merged_tree: don't add empty subtree to parent, remove canceling terms prior to resolving file-level conflict #2662

Merged
merged 2 commits into from
Dec 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed bugs

* Fixed another file conflict resolution issue where `jj status` would disagree
with the actual file content.
[#2654](https://github.com/martinvonz/jj/issues/2654)


## [0.11.0] - 2023-11-01

Expand Down
16 changes: 9 additions & 7 deletions lib/src/merged_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -537,16 +537,18 @@ fn merge_tree_values(
if let Some(trees) = values.to_tree_merge(store, path)? {
// If all sides are trees or missing, merge the trees recursively, treating
// missing trees as empty.
let empty_tree_id = store.empty_tree_id();
let merged_tree = merge_trees(&trees)?;
if merged_tree.as_resolved().map(|tree| tree.id()) == Some(store.empty_tree_id()) {
Ok(Merge::absent())
} else {
Ok(merged_tree.map(|tree| Some(TreeValue::Tree(tree.id().clone()))))
}
Ok(merged_tree
.map(|tree| (tree.id() != empty_tree_id).then(|| 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, &values)? {
// files as empty. The values may contain trees canceling each other (notably
// padded absent trees), so we need to simplify them first.
let simplified = values.clone().simplify();
// No fast path for simplified.is_resolved(). If it could be resolved, it would
// have been caught by values.resolve_trivial() above.
if let Some(resolved) = try_resolve_file_conflict(store, path, &simplified)? {
Ok(Merge::normal(resolved))
} else {
// Failed to merge the files, or the paths are not files
Expand Down
8 changes: 7 additions & 1 deletion lib/src/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,10 @@ fn merge_tree_value(
})
}

/// Resolves file-level conflict by merging content hunks.
///
/// The input `conflict` is supposed to be simplified. It shouldn't contain
/// non-file values that cancel each other.
pub fn try_resolve_file_conflict(
store: &Store,
filename: &RepoPath,
Expand Down Expand Up @@ -430,7 +434,9 @@ pub fn try_resolve_file_conflict(
}));
}

// Simplify the conflict for two reasons:
// While the input conflict should be simplified by caller, it might contain
// terms which only differ in executable bits. Simplify the conflict further
// for two reasons:
// 1. Avoid reading unchanged file contents
// 2. The simplified conflict can sometimes be resolved when the unsimplfied one
// cannot
Expand Down
49 changes: 49 additions & 0 deletions lib/tests/test_merged_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,24 @@ fn test_resolve_with_conflict() {
)
}

#[test]
fn test_resolve_with_conflict_containing_empty_subtree() {
let test_repo = TestRepo::init();
let repo = &test_repo.repo;

// Since "dir" in side2 is absent, the root tree should be empty as well.
// If it were added to the root tree, side2.id() would differ.
let conflict_path = RepoPath::from_internal_string("dir/file_conflict");
let base1 = create_single_tree(repo, &[(conflict_path, "base1")]);
let side1 = create_single_tree(repo, &[(conflict_path, "side1")]);
let side2 = create_single_tree(repo, &[]);

let original_tree = Merge::from_removes_adds(vec![base1], vec![side1, side2]);
let tree = MergedTree::new(original_tree.clone());
let resolved_tree = tree.resolve().unwrap();
assert_eq!(resolved_tree, original_tree);
}

#[test]
fn test_conflict_iterator() {
let test_repo = TestRepo::init();
Expand Down Expand Up @@ -1437,3 +1455,34 @@ fn test_merge_simplify_file_conflict() {
MergeResult::Conflict(_)
));
}

/// Like `test_merge_simplify_file_conflict()`, but some of the conflicts are
/// absent.
#[test]
fn test_merge_simplify_file_conflict_with_absent() {
let test_repo = TestRepo::init();
let repo = &test_repo.repo;

// conflict_path doesn't exist in parent and child2_left, and these two
// trees can't be canceled out at the root level. Still the file merge
// should succeed by eliminating absent entries.
let child2_path = RepoPath::from_internal_string("file_child2");
let conflict_path = RepoPath::from_internal_string("dir/file_conflict");
let child1 = create_single_tree(repo, &[(conflict_path, "1\n0\n")]);
let parent = create_single_tree(repo, &[]);
let child2_left = create_single_tree(repo, &[(child2_path, "")]);
let child2_base = create_single_tree(repo, &[(child2_path, ""), (conflict_path, "0\n")]);
let child2_right = create_single_tree(repo, &[(child2_path, ""), (conflict_path, "0\n2\n")]);
let child1_merged = MergedTree::resolved(child1);
let parent_merged = MergedTree::resolved(parent);
let child2_merged = MergedTree::new(Merge::from_removes_adds(
vec![child2_base],
vec![child2_left, child2_right],
));

let expected = create_single_tree(repo, &[(child2_path, ""), (conflict_path, "1\n0\n2\n")]);
let expected_merged = MergedTree::resolved(expected);

let merged = child1_merged.merge(&parent_merged, &child2_merged).unwrap();
assert_eq!(merged, expected_merged);
}