Skip to content

Commit 1092b9b

Browse files
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
1 parent b0f41e7 commit 1092b9b

File tree

2 files changed

+53
-4
lines changed

2 files changed

+53
-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).map_or(false, |m| m.is_dir());
1381+
let source_is_dir = fs::metadata(source).map_or(false, |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: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,46 @@ 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("src_dir");
151+
at.mkdir("src_dir/subdir");
152+
at.touch("src_dir/subdir/file1.txt");
153+
at.write("src_dir/subdir/file1.txt", "content1");
154+
at.touch("src_dir/subdir/file2.txt");
155+
at.write("src_dir/subdir/file2.txt", "content2");
156+
157+
// Source directory 2
158+
at.mkdir("src_dir2");
159+
at.mkdir("src_dir2/subdir");
160+
at.touch("src_dir2/subdir/file1.txt");
161+
at.write("src_dir2/subdir/file1.txt", "content3");
162+
163+
// Destination
164+
at.mkdir("dest");
165+
166+
// Perform merge copy
167+
ucmd.arg("-r")
168+
.arg("src_dir/subdir")
169+
.arg("src_dir2/subdir")
170+
.arg("dest")
171+
.succeeds();
172+
173+
// Verify directory exists
174+
assert!(at.dir_exists("dest/subdir"));
175+
176+
// file1.txt should be overwritten by src_dir2/subdir/file1.txt
177+
assert!(at.file_exists("dest/subdir/file1.txt"));
178+
assert_eq!(at.read("dest/subdir/file1.txt"), "content3");
179+
180+
// file2.txt should remain from first copy
181+
assert!(at.file_exists("dest/subdir/file2.txt"));
182+
assert_eq!(at.read("dest/subdir/file2.txt"), "content2");
183+
}
184+
145185
#[test]
146186
fn test_cp_duplicate_files_normalized_path() {
147187
let (at, mut ucmd) = at_and_ucmd!();

0 commit comments

Comments
 (0)