Skip to content

Commit aebf681

Browse files
committed
merge-tools builtin: update mode of files with unchanged contents
If an empty file or a binary file has its file mode (specifically the executable bit) changed but its contents remain the same, scm-record reports its contents as `Unchanged`. By not doing anything for unchanged _contents_, a potentially selected file mode change was lost. For text file, scm_record 0.8.0 currently always reports `SelectedContents::Text`, even if the file hasn't changed at all (or only its executable bit has). I've created arxanas/scm-record#104 upstream to track this bug. This unexpected behavior caused file mode-only changes to be handled correctly but it unnecessarily rewrote the file object, where writing the tree would have sufficed. Thus, fixing the upstream issue prior to this commit would exacerbate the issue by also affecting text files. This commit now addresses file mode changes by updating the `TreeValue` without rewriting the files or generating new file IDs. It is sufficient to do so only if a file mode change has been selected, reusing the same `file_mode_change_selected` condition introduced by the previous commit.
1 parent f115662 commit aebf681

File tree

1 file changed

+230
-5
lines changed

1 file changed

+230
-5
lines changed

cli/src/merge_tools/builtin.rs

Lines changed: 230 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ fn apply_diff_builtin(
388388
&mut tree_builder,
389389
changed_files,
390390
files,
391+
|path| left_tree.path_value(path),
391392
|path| right_tree.path_value(path),
392393
|path, contents, executable| {
393394
let old_value = left_tree.path_value(path)?;
@@ -422,6 +423,7 @@ fn apply_changes(
422423
tree_builder: &mut MergedTreeBuilder,
423424
changed_files: Vec<RepoPathBuf>,
424425
files: &[scm_record::File],
426+
select_left: impl Fn(&RepoPath) -> BackendResult<MergedTreeValue>,
425427
select_right: impl Fn(&RepoPath) -> BackendResult<MergedTreeValue>,
426428
write_file: impl Fn(&RepoPath, &[u8], bool) -> BackendResult<MergedTreeValue>,
427429
) -> BackendResult<()> {
@@ -449,7 +451,7 @@ fn apply_changes(
449451
_unselected,
450452
) = file.get_selected_contents();
451453

452-
if file_mode == scm_record::FileMode::Absent {
454+
if file_mode == mode::ABSENT {
453455
// The file is not present in the selected changes.
454456
// Either a file mode change was selected to delete an existing file, so we
455457
// should remove it from the tree,
@@ -462,9 +464,16 @@ fn apply_changes(
462464
continue;
463465
}
464466

467+
let executable = file_mode == mode::EXECUTABLE;
465468
match contents {
466469
scm_record::SelectedContents::Unchanged => {
467-
// Do nothing.
470+
if file_mode_change_selected {
471+
// File contents haven't changed, but file mode needs to be updated on the tree.
472+
let value = override_file_executable_bit(select_left(&path)?, executable);
473+
tree_builder.set_or_remove(path, value);
474+
} else {
475+
// Neither file mode, nor contents changed => Do nothing.
476+
}
468477
}
469478
scm_record::SelectedContents::Binary {
470479
old_description: _,
@@ -474,7 +483,6 @@ fn apply_changes(
474483
tree_builder.set_or_remove(path, value);
475484
}
476485
scm_record::SelectedContents::Text { contents } => {
477-
let executable = file_mode == mode::EXECUTABLE;
478486
let value = write_file(&path, contents.as_bytes(), executable)?;
479487
tree_builder.set_or_remove(path, value);
480488
}
@@ -483,6 +491,19 @@ fn apply_changes(
483491
Ok(())
484492
}
485493

494+
fn override_file_executable_bit(
495+
mut merged_tree_value: MergedTreeValue,
496+
new_executable_bit: bool,
497+
) -> MergedTreeValue {
498+
for tree_value in merged_tree_value.iter_mut().flatten() {
499+
let TreeValue::File { executable, .. } = tree_value else {
500+
panic!("incompatible update: expected a TreeValue::File, got {tree_value:?}");
501+
};
502+
*executable = new_executable_bit;
503+
}
504+
merged_tree_value
505+
}
506+
486507
pub fn edit_diff_builtin(
487508
left_tree: &MergedTree,
488509
right_tree: &MergedTree,
@@ -648,8 +669,11 @@ pub fn edit_merge_builtin(
648669
.map(|file| file.repo_path.clone())
649670
.collect_vec(),
650671
&state.files,
651-
// TODO: It doesn't make sense to select new value from the source tree.
652-
// Perhaps, "their" tree value should be extracted from a conflict?
672+
|path| tree.path_value(path),
673+
// FIXME: It doesn't make sense to select a new value from the source tree.
674+
// Presently, `select_right` is never actually called, since it is used to select binary
675+
// sections, but `make_merge_file` does not produce `Binary` sections for conflicted files.
676+
// This needs to be revisited when the UI becomes capable of representing binary conflicts.
653677
|path| tree.path_value(path),
654678
|path, contents, executable| {
655679
let id = store.write_file(path, &mut &contents[..]).block_on()?;
@@ -952,6 +976,207 @@ mod tests {
952976
);
953977
}
954978

979+
#[test]
980+
fn test_edit_diff_builtin_empty_file_mode_change() {
981+
let test_repo = TestRepo::init();
982+
let store = test_repo.repo.store();
983+
984+
let empty_file_path = repo_path("empty_file");
985+
let left_tree = testutils::create_tree_with(&test_repo.repo, |builder| {
986+
builder.file(empty_file_path, vec![]).executable(false);
987+
});
988+
let right_tree = testutils::create_tree_with(&test_repo.repo, |builder| {
989+
builder.file(empty_file_path, vec![]).executable(true);
990+
});
991+
992+
let (changed_files, files) = make_diff(store, &left_tree, &right_tree);
993+
insta::assert_debug_snapshot!(changed_files, @r#"
994+
[
995+
"empty_file",
996+
]
997+
"#);
998+
insta::assert_debug_snapshot!(files, @r#"
999+
[
1000+
File {
1001+
old_path: None,
1002+
path: "empty_file",
1003+
file_mode: Unix(
1004+
33188,
1005+
),
1006+
sections: [
1007+
FileMode {
1008+
is_checked: false,
1009+
mode: Unix(
1010+
33261,
1011+
),
1012+
},
1013+
],
1014+
},
1015+
]
1016+
"#);
1017+
let no_changes_tree_id = apply_diff(store, &left_tree, &right_tree, &changed_files, &files);
1018+
let no_changes_tree = store.get_root_tree(&no_changes_tree_id).unwrap();
1019+
assert_eq!(
1020+
left_tree.id(),
1021+
no_changes_tree.id(),
1022+
"no-changes tree was different:\nexpected tree:\n{}\nactual tree:\n{}",
1023+
dump_tree(store, &left_tree.id()),
1024+
dump_tree(store, &no_changes_tree.id()),
1025+
);
1026+
1027+
let mut files = files;
1028+
for file in &mut files {
1029+
file.toggle_all();
1030+
}
1031+
let all_changes_tree_id =
1032+
apply_diff(store, &left_tree, &right_tree, &changed_files, &files);
1033+
let all_changes_tree = store.get_root_tree(&all_changes_tree_id).unwrap();
1034+
assert_eq!(
1035+
right_tree.id(),
1036+
all_changes_tree.id(),
1037+
"all-changes tree was different:\nexpected tree:\n{}\nactual tree:\n{}",
1038+
dump_tree(store, &right_tree.id()),
1039+
dump_tree(store, &all_changes_tree.id()),
1040+
);
1041+
}
1042+
1043+
#[test]
1044+
fn test_edit_diff_builtin_text_file_mode_change() {
1045+
let test_repo = TestRepo::init();
1046+
let store = test_repo.repo.store();
1047+
1048+
let text_file_path = repo_path("text_file");
1049+
let left_tree = testutils::create_tree_with(&test_repo.repo, |builder| {
1050+
builder.file(text_file_path, "text").executable(false);
1051+
});
1052+
let right_tree = testutils::create_tree_with(&test_repo.repo, |builder| {
1053+
builder.file(text_file_path, "text").executable(true);
1054+
});
1055+
1056+
let (changed_files, files) = make_diff(store, &left_tree, &right_tree);
1057+
insta::assert_debug_snapshot!(changed_files, @r#"
1058+
[
1059+
"text_file",
1060+
]
1061+
"#);
1062+
insta::assert_debug_snapshot!(files, @r#"
1063+
[
1064+
File {
1065+
old_path: None,
1066+
path: "text_file",
1067+
file_mode: Unix(
1068+
33188,
1069+
),
1070+
sections: [
1071+
FileMode {
1072+
is_checked: false,
1073+
mode: Unix(
1074+
33261,
1075+
),
1076+
},
1077+
Unchanged {
1078+
lines: [
1079+
"text",
1080+
],
1081+
},
1082+
],
1083+
},
1084+
]
1085+
"#);
1086+
let no_changes_tree_id = apply_diff(store, &left_tree, &right_tree, &changed_files, &files);
1087+
let no_changes_tree = store.get_root_tree(&no_changes_tree_id).unwrap();
1088+
assert_eq!(
1089+
left_tree.id(),
1090+
no_changes_tree.id(),
1091+
"no-changes tree was different:\nexpected tree:\n{}\nactual tree:\n{}",
1092+
dump_tree(store, &left_tree.id()),
1093+
dump_tree(store, &no_changes_tree.id()),
1094+
);
1095+
1096+
let mut files = files;
1097+
for file in &mut files {
1098+
file.toggle_all();
1099+
}
1100+
let all_changes_tree_id =
1101+
apply_diff(store, &left_tree, &right_tree, &changed_files, &files);
1102+
let all_changes_tree = store.get_root_tree(&all_changes_tree_id).unwrap();
1103+
assert_eq!(
1104+
right_tree.id(),
1105+
all_changes_tree.id(),
1106+
"all-changes tree was different:\nexpected tree:\n{}\nactual tree:\n{}",
1107+
dump_tree(store, &right_tree.id()),
1108+
dump_tree(store, &all_changes_tree.id()),
1109+
);
1110+
}
1111+
1112+
#[test]
1113+
fn test_edit_diff_builtin_binary_file_mode_change() {
1114+
let test_repo = TestRepo::init();
1115+
let store = test_repo.repo.store();
1116+
1117+
let binary_file_path = repo_path("binary_file");
1118+
let left_tree = testutils::create_tree_with(&test_repo.repo, |builder| {
1119+
builder
1120+
.file(binary_file_path, vec![0xff, 0x00])
1121+
.executable(false);
1122+
});
1123+
let right_tree = testutils::create_tree_with(&test_repo.repo, |builder| {
1124+
builder
1125+
.file(binary_file_path, vec![0xff, 0x00])
1126+
.executable(true);
1127+
});
1128+
1129+
let (changed_files, files) = make_diff(store, &left_tree, &right_tree);
1130+
insta::assert_debug_snapshot!(changed_files, @r#"
1131+
[
1132+
"binary_file",
1133+
]
1134+
"#);
1135+
insta::assert_debug_snapshot!(files, @r#"
1136+
[
1137+
File {
1138+
old_path: None,
1139+
path: "binary_file",
1140+
file_mode: Unix(
1141+
33188,
1142+
),
1143+
sections: [
1144+
FileMode {
1145+
is_checked: false,
1146+
mode: Unix(
1147+
33261,
1148+
),
1149+
},
1150+
],
1151+
},
1152+
]
1153+
"#);
1154+
let no_changes_tree_id = apply_diff(store, &left_tree, &right_tree, &changed_files, &files);
1155+
let no_changes_tree = store.get_root_tree(&no_changes_tree_id).unwrap();
1156+
assert_eq!(
1157+
left_tree.id(),
1158+
no_changes_tree.id(),
1159+
"no-changes tree was different:\nexpected tree:\n{}\nactual tree:\n{}",
1160+
dump_tree(store, &left_tree.id()),
1161+
dump_tree(store, &no_changes_tree.id()),
1162+
);
1163+
1164+
let mut files = files;
1165+
for file in &mut files {
1166+
file.toggle_all();
1167+
}
1168+
let all_changes_tree_id =
1169+
apply_diff(store, &left_tree, &right_tree, &changed_files, &files);
1170+
let all_changes_tree = store.get_root_tree(&all_changes_tree_id).unwrap();
1171+
assert_eq!(
1172+
right_tree.id(),
1173+
all_changes_tree.id(),
1174+
"all-changes tree was different:\nexpected tree:\n{}\nactual tree:\n{}",
1175+
dump_tree(store, &right_tree.id()),
1176+
dump_tree(store, &all_changes_tree.id()),
1177+
);
1178+
}
1179+
9551180
#[test]
9561181
fn test_edit_diff_builtin_delete_file() {
9571182
let test_repo = TestRepo::init();

0 commit comments

Comments
 (0)