From 15d22c285e672fa9dae6784c6f3d8becd9fb10d0 Mon Sep 17 00:00:00 2001 From: Vikram Kangotra <61800198+vikram-kangotra@users.noreply.github.com> Date: Wed, 19 Nov 2025 21:23:23 +0530 Subject: [PATCH] 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 --------- Co-authored-by: Daniel Hofstetter --- src/uu/cp/src/cp.rs | 17 +++++++++++++---- tests/by-util/test_cp.rs | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index c6c8789e2..9ef767d05 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -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()), + )); + } } } diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 3c5b3242e..c6f0d1c77 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -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!();