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

local_working_copy: when all sides of a conflict are executable, mate… #3580

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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ to avoid letting the user edit the immutable one.

* `jj config list` now properly escapes TOML keys (#1322).

* Files with conflicts are now checked out as executable if all sides of the
conflict are executable.
gulbanana marked this conversation as resolved.
Show resolved Hide resolved

## [0.17.1] - 2024-05-07

### Fixed bugs
Expand Down
24 changes: 17 additions & 7 deletions lib/src/local_working_copy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

#![allow(missing_docs)]
#![allow(clippy::let_unit_value)]

use std::any::Any;
use std::collections::HashSet;
Expand Down Expand Up @@ -109,7 +110,6 @@ impl FileState {
let executable = {
// Windows doesn't support executable bit.
let _ = executable;
()
};
FileState {
file_type: FileType::Normal { executable },
Expand Down Expand Up @@ -1152,10 +1152,15 @@ impl TreeState {
.block_on()?;
match new_file_ids.into_resolved() {
Ok(file_id) => {
// On Windows, we preserve the executable bit from the merged trees.
#[cfg(windows)]
let executable = {
let () = executable; // use the variable
false
if let Some(merge) = current_tree_values.to_executable_merge() {
merge.resolve_trivial().copied().unwrap_or_default()
} else {
false
}
};
Ok(Merge::normal(TreeValue::File {
id: file_id.unwrap(),
Expand Down Expand Up @@ -1224,6 +1229,7 @@ impl TreeState {
&self,
disk_path: &Path,
conflict_data: Vec<u8>,
executable: bool,
gulbanana marked this conversation as resolved.
Show resolved Hide resolved
) -> Result<FileState, CheckoutError> {
let mut file = OpenOptions::new()
.write(true)
Expand All @@ -1239,12 +1245,11 @@ impl TreeState {
err: err.into(),
})?;
let size = conflict_data.len() as u64;
// TODO: Set the executable bit correctly (when possible) and preserve that on
// Windows like we do with the executable bit for regular files.
gulbanana marked this conversation as resolved.
Show resolved Hide resolved
self.set_executable(disk_path, executable)?;
let metadata = file
.metadata()
.map_err(|err| checkout_error_for_stat_error(err, disk_path))?;
Ok(FileState::for_file(false, size, &metadata))
Ok(FileState::for_file(executable, size, &metadata))
}

#[cfg_attr(windows, allow(unused_variables))]
Expand Down Expand Up @@ -1393,8 +1398,13 @@ impl TreeState {
MaterializedTreeValue::Tree(_) => {
panic!("unexpected tree entry in diff at {path:?}");
}
MaterializedTreeValue::Conflict { id: _, contents } => {
self.write_conflict(&disk_path, contents)?
MaterializedTreeValue::Conflict { id, contents } => {
let executable = if let Some(merge) = id.to_executable_merge() {
merge.resolve_trivial().copied().unwrap_or_default()
} else {
false
};
gulbanana marked this conversation as resolved.
Show resolved Hide resolved
self.write_conflict(&disk_path, contents, executable)?
}
};
changed_file_states.push((path, file_state));
Expand Down
10 changes: 10 additions & 0 deletions lib/src/merge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,16 @@ impl MergedTreeValue {
})
}

/// If this merge contains only files or absent entries, returns a merge of
/// the files' executable bits.
pub fn to_executable_merge(&self) -> Option<Merge<bool>> {
self.maybe_map(|term| match term {
None => Some(false),
Some(TreeValue::File { id: _, executable }) => Some(*executable),
_ => None,
})
}

/// Creates a new merge with the file ids from the given merge. In other
/// words, only the executable bits from `self` will be preserved.
pub fn with_new_file_ids(&self, file_ids: &Merge<Option<FileId>>) -> Self {
Expand Down
40 changes: 39 additions & 1 deletion lib/tests/test_local_working_copy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ fn test_checkout_file_transitions(backend: TestRepoBackend) {
// Executable, but same content as Normal, to test transition where only the bit changed
ExecutableNormalContent,
Conflict,
// Same content as Executable, to test that transition preserves the executable bit
ConflictedExecutableContent,
Symlink,
Tree,
GitSubmodule,
Expand All @@ -110,7 +112,8 @@ fn test_checkout_file_transitions(backend: TestRepoBackend) {
})
}
Kind::Executable => {
let id = testutils::write_file(store, path, "executable file contents");
let id: jj_lib::backend::FileId =
testutils::write_file(store, path, "executable file contents");
Merge::normal(TreeValue::File {
id,
executable: true,
Expand Down Expand Up @@ -144,6 +147,29 @@ fn test_checkout_file_transitions(backend: TestRepoBackend) {
],
)
}
Kind::ConflictedExecutableContent => {
let base_file_id = testutils::write_file(store, path, "executable file contents");
let left_file_id =
testutils::write_file(store, path, "left executable file contents");
let right_file_id =
testutils::write_file(store, path, "right executable file contents");
Merge::from_removes_adds(
vec![Some(TreeValue::File {
id: base_file_id,
executable: true,
})],
vec![
Some(TreeValue::File {
id: left_file_id,
executable: true,
}),
Some(TreeValue::File {
id: right_file_id,
executable: true,
}),
],
)
}
Kind::Symlink => {
let id = store.write_symlink(path, "target").unwrap();
Merge::normal(TreeValue::Symlink(id))
Expand Down Expand Up @@ -174,6 +200,7 @@ fn test_checkout_file_transitions(backend: TestRepoBackend) {
Kind::Executable,
Kind::ExecutableNormalContent,
Kind::Conflict,
Kind::ConflictedExecutableContent,
Kind::Tree,
];
kinds.push(Kind::Symlink);
Expand Down Expand Up @@ -246,6 +273,17 @@ fn test_checkout_file_transitions(backend: TestRepoBackend) {
"{path:?} should not be executable"
);
}
Kind::ConflictedExecutableContent => {
assert!(maybe_metadata.is_ok(), "{path:?} should exist");
let metadata = maybe_metadata.unwrap();
assert!(metadata.is_file(), "{path:?} should be a file");
#[cfg(unix)]
assert_ne!(
metadata.permissions().mode() & 0o111,
0,
"{path:?} should be executable"
);
}
Kind::Symlink => {
assert!(maybe_metadata.is_ok(), "{path:?} should exist");
let metadata = maybe_metadata.unwrap();
Expand Down