Skip to content

Commit 91ffb2a

Browse files
committed
merge-tools builtin: only delete files from tree which used to exist
This adds a manual regression test for the scenario in issue #5189. As of #6411, this no longer results in a panic. However, the test still fails and rightfully so: When following the reproduction for #5189 and not selecting any change in `jj split`, the split-off (first) commit will still record the deletion of `folder/.keep` but not the creation of the file `folder`. scm-record yields a selected change with `FileMode::Absent` in one of two cases: - when an existing file is deleted and this change is selected, - when a new file is created and this change is not selected. From the information provided by `File::get_selected_contents()` alone, it is not possible to distinguish these two cases. In the first case, the tree definitely needs to change to reflect the deletion, so it was marked for deletion. In the second case, that deletion marker ("tombstone") usually was just ignored because no such file existed. However, when the tombstone happens to coindice with a deleted directory and a new file has been created in its place, but neither change is selected, then the tombstone had the effect of deleting the directory from the tree. This is now fixed by only marking the file for deletion if a corresponding file mode change to `Absent` has been "checked". Otherwise, if the file mode change for the creation of a new file is not "checked", the file can be merely skipped.
1 parent 9c165d6 commit 91ffb2a

File tree

2 files changed

+103
-3
lines changed

2 files changed

+103
-3
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8282
* `jj` will no longer warn about deprecated paths on macOS if the configured
8383
XDG directory is the deprecated one (~/Library/Application Support).
8484

85+
* The builtin diff editor now correctly handles splitting changes where a file is
86+
replaced by a directory of the same name.
87+
[#5189](https://github.com/jj-vcs/jj/issues/5189)
88+
8589
### Packaging changes
8690

8791
* Due to the removal of the `libgit2` code path, packagers should

cli/src/merge_tools/builtin.rs

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,15 @@ fn apply_changes(
420420
);
421421
// TODO: Write files concurrently
422422
for (path, file) in changed_files.into_iter().zip(files) {
423+
let file_mode_change_selected = file
424+
.sections
425+
.iter()
426+
.find_map(|sec| match sec {
427+
scm_record::Section::FileMode { is_checked, .. } => Some(*is_checked),
428+
_ => None,
429+
})
430+
.unwrap_or(false);
431+
423432
let (
424433
scm_record::SelectedChanges {
425434
contents,
@@ -428,10 +437,16 @@ fn apply_changes(
428437
_unselected,
429438
) = file.get_selected_contents();
430439

431-
// If a file was not present in the selected changes (i.e. split out of a
432-
// change, deleted, etc.) remove it from the tree.
433440
if file_mode == scm_record::FileMode::Absent {
434-
tree_builder.set_or_remove(path, Merge::absent());
441+
// The file is not present in the selected changes.
442+
// Either a file mode change was selected to delete an existing file, so we
443+
// should remove it from the tree,
444+
if file_mode_change_selected {
445+
tree_builder.set_or_remove(path, Merge::absent());
446+
}
447+
// or the file's creation has been split out of the change, in which case we
448+
// don't need to change the tree.
449+
// In either case, we're done with this file afterwards.
435450
continue;
436451
}
437452

@@ -639,7 +654,9 @@ mod tests {
639654
use jj_lib::matchers::EverythingMatcher;
640655
use jj_lib::merge::MergedTreeValue;
641656
use jj_lib::repo::Repo as _;
657+
use testutils::dump_tree;
642658
use testutils::repo_path;
659+
use testutils::repo_path_component;
643660
use testutils::TestRepo;
644661

645662
use super::*;
@@ -1253,6 +1270,85 @@ mod tests {
12531270
);
12541271
}
12551272

1273+
#[test]
1274+
fn test_edit_diff_builtin_replace_directory_with_file() {
1275+
let test_repo = TestRepo::init();
1276+
let store = test_repo.repo.store();
1277+
1278+
let folder_path = repo_path("folder");
1279+
let file_in_folder_path = folder_path.join(repo_path_component("file_in_folder"));
1280+
let left_tree = testutils::create_tree_with(&test_repo.repo, |builder| {
1281+
builder.file(&file_in_folder_path, vec![]);
1282+
});
1283+
let right_tree = testutils::create_tree_with(&test_repo.repo, |builder| {
1284+
builder.file(folder_path, vec![]);
1285+
});
1286+
1287+
let (changed_files, files) = make_diff(store, &left_tree, &right_tree);
1288+
insta::assert_debug_snapshot!(changed_files, @r#"
1289+
[
1290+
"folder/file_in_folder",
1291+
"folder",
1292+
]
1293+
"#);
1294+
insta::with_settings!({filters => vec![(r"\\\\", "/")]}, {
1295+
insta::assert_debug_snapshot!(files, @r#"
1296+
[
1297+
File {
1298+
old_path: None,
1299+
path: "folder/file_in_folder",
1300+
file_mode: Unix(
1301+
33188,
1302+
),
1303+
sections: [
1304+
FileMode {
1305+
is_checked: false,
1306+
mode: Absent,
1307+
},
1308+
],
1309+
},
1310+
File {
1311+
old_path: None,
1312+
path: "folder",
1313+
file_mode: Absent,
1314+
sections: [
1315+
FileMode {
1316+
is_checked: false,
1317+
mode: Unix(
1318+
33188,
1319+
),
1320+
},
1321+
],
1322+
},
1323+
]
1324+
"#);
1325+
});
1326+
let no_changes_tree_id = apply_diff(store, &left_tree, &right_tree, &changed_files, &files);
1327+
let no_changes_tree = store.get_root_tree(&no_changes_tree_id).unwrap();
1328+
assert_eq!(
1329+
left_tree.id(),
1330+
no_changes_tree.id(),
1331+
"no-changes tree was different:\nexpected tree:\n{}\nactual tree:\n{}",
1332+
dump_tree(store, &left_tree.id()),
1333+
dump_tree(store, &no_changes_tree.id()),
1334+
);
1335+
1336+
let mut files = files;
1337+
for file in &mut files {
1338+
file.toggle_all();
1339+
}
1340+
let all_changes_tree_id =
1341+
apply_diff(store, &left_tree, &right_tree, &changed_files, &files);
1342+
let all_changes_tree = store.get_root_tree(&all_changes_tree_id).unwrap();
1343+
assert_eq!(
1344+
right_tree.id(),
1345+
all_changes_tree.id(),
1346+
"all-changes tree was different:\nexpected tree:\n{}\nactual tree:\n{}",
1347+
dump_tree(store, &right_tree.id()),
1348+
dump_tree(store, &all_changes_tree.id()),
1349+
);
1350+
}
1351+
12561352
#[test]
12571353
fn test_make_merge_sections() {
12581354
let test_repo = TestRepo::init();

0 commit comments

Comments
 (0)