Repository: cargo-generate/cargo-generate (rust)

Issue:
Feature request: Add feature to see if file exists in destination directory
I'd like to be able to use cargo generate's `--init` feature to configure a pre-existing project to use my crate. My crate requires a build script and config.toml to operate.

I want to be able to write these files to the destination if they don't yet exist, but if the user already has one or both of these files I don't want to overwrite them as this would destroy the user's configuration for their project.

At present I have to either use system commands or ask the user whether the files exist, otherwise cargo generate simply errors when trying to copy the file. We already have a way to check whether a file exists in the template folder, so it seems reasonable to also be able to check the destination folder too.

Pull request description:
Enhance file copy behavior: Implement skipping existing files and allow overwriting with a flag
Fixes #1363 
- Introduced a new function `safe_copy_skip_existing` to introduce logic for skipping files that already exist, unless the overwrite flag is set, for non liquid files only.
- Added a test in src/copy.rs to cover new behavior
  - Test to verify that files are copied if they don't exist.
  - Test to ensure that files are not overwritten without the flag set.
  - Test to allow overwriting files when the flag is set to true.
- Updated a test in `tests/integration/basic.rs` to include:
  - Sucess in case of existance of non liquid file of same name.

These changes enhance the usability of the file copy functionality, ensuring safe file handling in various scenarios.

Patch:
diff --git a/src/copy.rs b/src/copy.rs
index 9e46215..c927156 100644
--- a/src/copy.rs
+++ b/src/copy.rs
@@ -48,7 +48,8 @@ pub fn copy_files_recursively(
     Ok(())
 }
 
-/// move a file from src to dst, possibly overwriting existing files if overwrite is true
+/// move a file from src to dst, possibly overwriting existing files
+/// if overwrite is true skipping otherwise
 /// if the file has a .liquid suffix, the suffix will be removed in the destination, and overwritten if existing
 fn copy_file(src_path: &Path, dst: &Path, overwrite: bool) -> Result<()> {
     let filename = src_path.file_name().unwrap().to_string_lossy().to_string();
@@ -76,8 +77,9 @@ fn copy_file(src_path: &Path, dst: &Path, overwrite: bool) -> Result<()> {
         return Ok(());
     } else {
         // if the file doesn't have a .liquid suffix, just copy it
+        // and skip if flile with that name exists
         // possibly overwriting existing files if overwrite is true
-        safe_copy(src_path, &dst_path, overwrite)?;
+        safe_copy_skip_existing(src_path, &dst_path, overwrite)?;
     }
 
     Ok(())
@@ -104,6 +106,29 @@ fn safe_copy(src_path: &Path, dst_path: &Path, overwrite: bool) -> Result<()> {
     Ok(())
 }
 
+/// Does the same as `safe_copy`, but skips existing files if set to not overwriting.
+/// It does not error if the file already exists.
+fn safe_copy_skip_existing(src_path: &Path, dst_path: &Path, overwrite: bool) -> Result<()> {
+    if dst_path.exists() && !overwrite {
+        warn!(
+            "{} `{}` {}",
+            style("[Skipping] File already exists").bold().yellow(),
+            style(dst_path.display()).bold(),
+            style("and `--overwrite` was not passed")
+        );
+        return Ok(());
+    }
+
+    if dst_path.exists() && overwrite {
+        remove_file(dst_path)?;
+        copy(src_path, dst_path)?;
+    } else if !dst_path.exists() {
+        copy(src_path, dst_path)?;
+    }
+
+    Ok(())
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -128,6 +153,34 @@ mod tests {
         assert_eq!(std::fs::read_to_string(f2.as_path()).unwrap(), "A README");
     }
 
+    #[test]
+    fn test_overwriting_behavior2() {
+        let tmp1 = tempdir().unwrap();
+        let tmp2 = tempdir().unwrap();
+        let f1 = tmp1.path().join("README.md");
+        std::fs::write(&f1, "FIRST README").unwrap();
+        let f2 = tmp2.path().join("README.md");
+        std::fs::write(&f2, "SECOND README").unwrap();
+
+        assert!(
+            safe_copy_skip_existing(f1.as_path(), f2.as_path(), false).is_ok(),
+            "we do not allow overwriting if file with same name already exists without the flag set"
+        );
+        assert_eq!(
+            std::fs::read_to_string(f2.as_path()).unwrap(),
+            "SECOND README",
+            "the file should not be copied"
+        );
+        assert!(
+            safe_copy_skip_existing(f1.as_path(), f2.as_path(), true).is_ok(),
+            "we do allow overwriting if file with same name already exists without the flag set"
+        );
+        assert_eq!(
+            std::fs::read_to_string(f2.as_path()).unwrap(),
+            "FIRST README"
+        );
+    }
+
     #[test]
     fn test_special_liquid_file_handling() {
         let tmp = tempdir().unwrap();


Test patch:
diff --git a/tests/integration/basics.rs b/tests/integration/basics.rs
index 38a7917..cc780c8 100644
--- a/tests/integration/basics.rs
+++ b/tests/integration/basics.rs
@@ -296,7 +296,7 @@ fn it_can_generate_at_given_path() -> anyhow::Result<()> {
 }
 
 #[test]
-fn it_refuses_to_overwrite_files() -> anyhow::Result<()> {
+fn it_does_not_overwrite_existing_files() -> anyhow::Result<()> {
     let template = tempdir().init_default_template().build();
     let dir = tempdir().build();
     let _ = binary()
@@ -311,7 +311,7 @@ fn it_refuses_to_overwrite_files() -> anyhow::Result<()> {
         .flag_init()
         .current_dir(dir.path())
         .assert()
-        .failure();
+        .success();
     assert!(dir.read("Cargo.toml").contains("my-proj"));
     Ok(())
 }