cp: allow directory merging when destination was just created (#9325)

* cp: allow directory merging when destination was just created

Previously, when copying to a destination that was created in the same
cp call, the operation would fail with "will not overwrite just-created"
for both files and directories. This change allows directories to be
merged (matching GNU cp behavior) while still preventing file
overwrites.

The fix checks if both the source and destination are directories
before allowing the merge. If either is a file, the original error
behavior is preserved to prevent accidental file overwrites.

Fixes the case where copying multiple directories to the same
destination path would incorrectly error instead of merging their
contents.

fixes: #9318

* test_cp: Update with the suggestion

Co-authored-by: Daniel Hofstetter <daniel.hofstetter@42dh.com>

---------

Co-authored-by: Daniel Hofstetter <daniel.hofstetter@42dh.com>
This commit is contained in:
Vikram Kangotra
2025-11-19 16:53:23 +01:00
committed by GitHub
co-authored by Daniel Hofstetter
parent 376fa6408b
commit 15d22c285e
2 changed files with 48 additions and 4 deletions
+13 -4
View File
@@ -1375,10 +1375,19 @@ pub fn copy(sources: &[PathBuf], target: &Path, options: &Options) -> CopyResult
{
// There is already a file and it isn't a symlink (managed in a different place)
if copied_destinations.contains(&dest) && options.backup != BackupMode::Numbered {
// If the target file was already created in this cp call, do not overwrite
return Err(CpError::Error(
translate!("cp-error-will-not-overwrite-just-created", "dest" => dest.quote(), "source" => source.quote()),
));
// If the target was already created in this cp call, check if it's a directory.
// Directories should be merged (GNU cp behavior), but files should not be overwritten.
let dest_is_dir = fs::metadata(&dest).is_ok_and(|m| m.is_dir());
let source_is_dir = fs::metadata(source).is_ok_and(|m| m.is_dir());
// Only prevent overwriting if both source and dest are files (not directories)
// Directories should be merged, which is handled by copy_directory
if !dest_is_dir || !source_is_dir {
// If the target file was already created in this cp call, do not overwrite
return Err(CpError::Error(
translate!("cp-error-will-not-overwrite-just-created", "dest" => dest.quote(), "source" => source.quote()),
));
}
}
}
+35
View File
@@ -142,6 +142,41 @@ fn test_cp_duplicate_folder() {
assert!(at.dir_exists(format!("{TEST_COPY_TO_FOLDER}/{TEST_COPY_FROM_FOLDER}").as_str()));
}
#[test]
fn test_cp_duplicate_directories_merge() {
let (at, mut ucmd) = at_and_ucmd!();
// Source directory 1
at.mkdir_all("src_dir/subdir");
at.write("src_dir/subdir/file1.txt", "content1");
at.write("src_dir/subdir/file2.txt", "content2");
// Source directory 2
at.mkdir_all("src_dir2/subdir");
at.write("src_dir2/subdir/file1.txt", "content3");
// Destination
at.mkdir("dest");
// Perform merge copy
ucmd.arg("-r")
.arg("src_dir/subdir")
.arg("src_dir2/subdir")
.arg("dest")
.succeeds();
// Verify directory exists
assert!(at.dir_exists("dest/subdir"));
// file1.txt should be overwritten by src_dir2/subdir/file1.txt
assert!(at.file_exists("dest/subdir/file1.txt"));
assert_eq!(at.read("dest/subdir/file1.txt"), "content3");
// file2.txt should remain from first copy
assert!(at.file_exists("dest/subdir/file2.txt"));
assert_eq!(at.read("dest/subdir/file2.txt"), "content2");
}
#[test]
fn test_cp_duplicate_files_normalized_path() {
let (at, mut ucmd) = at_and_ucmd!();