Skip to content

Commit 15d22c2

Browse files
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 <[email protected]> --------- Co-authored-by: Daniel Hofstetter <[email protected]>
1 parent 376fa64 commit 15d22c2

File tree

2 files changed

+48
-4
lines changed

2 files changed

+48
-4
lines changed

src/uu/cp/src/cp.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1375,10 +1375,19 @@ pub fn copy(sources: &[PathBuf], target: &Path, options: &Options) -> CopyResult
13751375
{
13761376
// There is already a file and it isn't a symlink (managed in a different place)
13771377
if copied_destinations.contains(&dest) && options.backup != BackupMode::Numbered {
1378-
// If the target file was already created in this cp call, do not overwrite
1379-
return Err(CpError::Error(
1380-
translate!("cp-error-will-not-overwrite-just-created", "dest" => dest.quote(), "source" => source.quote()),
1381-
));
1378+
// If the target was already created in this cp call, check if it's a directory.
1379+
// Directories should be merged (GNU cp behavior), but files should not be overwritten.
1380+
let dest_is_dir = fs::metadata(&dest).is_ok_and(|m| m.is_dir());
1381+
let source_is_dir = fs::metadata(source).is_ok_and(|m| m.is_dir());
1382+
1383+
// Only prevent overwriting if both source and dest are files (not directories)
1384+
// Directories should be merged, which is handled by copy_directory
1385+
if !dest_is_dir || !source_is_dir {
1386+
// If the target file was already created in this cp call, do not overwrite
1387+
return Err(CpError::Error(
1388+
translate!("cp-error-will-not-overwrite-just-created", "dest" => dest.quote(), "source" => source.quote()),
1389+
));
1390+
}
13821391
}
13831392
}
13841393

tests/by-util/test_cp.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,41 @@ fn test_cp_duplicate_folder() {
142142
assert!(at.dir_exists(format!("{TEST_COPY_TO_FOLDER}/{TEST_COPY_FROM_FOLDER}").as_str()));
143143
}
144144

145+
#[test]
146+
fn test_cp_duplicate_directories_merge() {
147+
let (at, mut ucmd) = at_and_ucmd!();
148+
149+
// Source directory 1
150+
at.mkdir_all("src_dir/subdir");
151+
at.write("src_dir/subdir/file1.txt", "content1");
152+
at.write("src_dir/subdir/file2.txt", "content2");
153+
154+
// Source directory 2
155+
at.mkdir_all("src_dir2/subdir");
156+
at.write("src_dir2/subdir/file1.txt", "content3");
157+
158+
// Destination
159+
at.mkdir("dest");
160+
161+
// Perform merge copy
162+
ucmd.arg("-r")
163+
.arg("src_dir/subdir")
164+
.arg("src_dir2/subdir")
165+
.arg("dest")
166+
.succeeds();
167+
168+
// Verify directory exists
169+
assert!(at.dir_exists("dest/subdir"));
170+
171+
// file1.txt should be overwritten by src_dir2/subdir/file1.txt
172+
assert!(at.file_exists("dest/subdir/file1.txt"));
173+
assert_eq!(at.read("dest/subdir/file1.txt"), "content3");
174+
175+
// file2.txt should remain from first copy
176+
assert!(at.file_exists("dest/subdir/file2.txt"));
177+
assert_eq!(at.read("dest/subdir/file2.txt"), "content2");
178+
}
179+
145180
#[test]
146181
fn test_cp_duplicate_files_normalized_path() {
147182
let (at, mut ucmd) = at_and_ucmd!();

0 commit comments

Comments
 (0)