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

Issue:
Add option to overwrite files in existing project
[The book has the following to say](https://cargo-generate.github.io/cargo-generate/usage.html#generating-into-current-dir) about this:

> `cargo-generate` will not allow any existing files to be overwritten and will fail to generate any files should there be any conflicts.

I actually think this would be a good feature to have, although the default choice of refusing to overwrite is good. Perhaps an `--overwrite` flag could be added to generate over the existing project anyway?

For an example use case, I have [this template](https://or.computer.surgery/charles/nix-rust-quickstart) that receives relatively frequent updates since Nix Flakes are still evolving. I feel like an `--overwrite` option would make it easier to propagate these changes and new best practices to existing projects generated from this template. Then one could do `cargo gen ... --overwrite && git restore -p .` instead of having to figure out how to apply all of the changes manually.

Pull request:
Add option to overwrite files in existing project
Fixes #569

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e9fc166..0db165a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,7 +18,7 @@
   If a selected template doesn't have a `cargo-generate.toml` file, but a parent one exists, any configured sub-templates will be ignored.
   
   Further implication is that sub-templates no longer needs to have a `cargo-generate.toml` file.
-- [Test for file existence from rhai scripts](https://github.com/cargo-generate/cargo-generate/pull/690).
+- [Test for file existance from rhai scripts](https://github.com/cargo-generate/cargo-generate/pull/690).
   Adds the `file::exists(path: &str)` method for use from hook scripts.
 - Conditionals and placeholders now supported in multiple levels.
   If a template sets up more placeholders conditionally, those placeholders are now checked/asked
@@ -26,6 +26,7 @@
 - [Set default VCS in the favorite or template config](https://github.com/cargo-generate/cargo-generate/issues/635)
 - [Add case filters to `liquid` and functions to `Rhai`. Enables case changing functionality from both `liquid` and `Rhai`](https://github.com/cargo-generate/cargo-generate/issues/638)
 - [Add placeholder `{{is_init}}`. Enables templates to adust if they are expanded with the `--init` arg for `cargo-generate`](https://github.com/cargo-generate/cargo-generate/issues/649)
+- [Add option to overwrite files in existing project](https://github.com/cargo-generate/cargo-generate/issues/569)
 
 ### Contributors
 - [taurr](https://github.com/taurr)
diff --git a/guide/src/favorites.md b/guide/src/favorites.md
index 0c1c3c2..4dd4c5e 100644
--- a/guide/src/favorites.md
+++ b/guide/src/favorites.md
@@ -15,6 +15,7 @@ branch = "<optional-branch>"
 subfolder = "<optional-subfolder>"
 vcs = "<optional: None|Git>"
 init = optional true|false
+overwrite = optional true|false
 ```
 
 Values may be overridden using the CLI arguments of the same names (e.g. `--subfolder` for the `subfolder` value).
@@ -22,6 +23,8 @@ Values may be overridden using the CLI arguments of the same names (e.g. `--subf
 **Note:** Specifying `init = true` has the effect of forcing the template to exhibit behaviour as if `--init` is specified on the
 commandline, as there is no counter-option!
 
+**Note:** Specifying `overwrite = true` has the effect of allowing the template to always overwrite files as there is no counter-option!
+
 When favorites are available, they can be generated simply by invoking:
 
 ```cli
diff --git a/src/app_config.rs b/src/app_config.rs
index 0a085e3..dd1f32a 100644
--- a/src/app_config.rs
+++ b/src/app_config.rs
@@ -35,6 +35,7 @@ pub struct FavoriteConfig {
     pub values: Option<HashMap<String, toml::Value>>,
     pub vcs: Option<Vcs>,
     pub init: Option<bool>,
+    pub overwrite: Option<bool>,
 }
 
 #[derive(Deserialize, Default)]
diff --git a/src/args.rs b/src/args.rs
index 40f7b8a..7940d7d 100644
--- a/src/args.rs
+++ b/src/args.rs
@@ -114,6 +114,10 @@ pub struct GenerateArgs {
     /// Use at your own risk and be sure to review the template code beforehand.
     #[clap(short, long, action)]
     pub allow_commands: bool,
+
+    /// Allow the template to overwrite existing files in the destination.
+    #[clap(short, long, action)]
+    pub overwrite: bool,
 }
 
 #[derive(Debug, Clone, Args)]
diff --git a/src/lib.rs b/src/lib.rs
index fa265f8..a8db9a7 100755
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -154,7 +154,7 @@ fn internal_generate(mut args: GenerateArgs) -> Result<PathBuf> {
         style(project_dir.display()).bold().yellow(),
         style("...").bold()
     );
-    copy_dir_all(&template_dir, &project_dir)?;
+    copy_dir_all(&template_dir, &project_dir, user_parsed_input.overwrite())?;
 
     let vcs = config
         .template
@@ -353,14 +353,18 @@ fn resolve_project_name(args: &GenerateArgs) -> Result<ProjectName> {
 
 fn copy_path_template_into_temp(src_path: &Path) -> Result<TempDir> {
     let path_clone_dir = tempfile::tempdir()?;
-    copy_dir_all(src_path, path_clone_dir.path())?;
+    copy_dir_all(src_path, path_clone_dir.path(), false)?;
     git::remove_history(path_clone_dir.path())?;
 
     Ok(path_clone_dir)
 }
 
-pub(crate) fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
-    fn check_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
+pub(crate) fn copy_dir_all(
+    src: impl AsRef<Path>,
+    dst: impl AsRef<Path>,
+    overwrite: bool,
+) -> Result<()> {
+    fn check_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>, overwrite: bool) -> Result<()> {
         if !dst.as_ref().exists() {
             return Ok(());
         }
@@ -372,18 +376,29 @@ pub(crate) fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Resu
 
             if entry_type.is_dir() {
                 let dst_path = dst.as_ref().join(filename);
-                check_dir_all(src_entry.path(), dst_path)?;
+                check_dir_all(src_entry.path(), dst_path, overwrite)?;
             } else if entry_type.is_file() {
                 let filename = filename.strip_suffix(".liquid").unwrap_or(&filename);
                 let dst_path = dst.as_ref().join(filename);
-                if dst_path.exists() {
-                    bail!(
-                        "{} {} {}",
-                        crate::emoji::WARN,
-                        style("File already exists:").bold().red(),
-                        style(dst_path.display()).bold().red(),
-                    )
-                }
+                match (dst_path.exists(), overwrite) {
+                    (true, false) => {
+                        bail!(
+                            "{} {} {}",
+                            crate::emoji::WARN,
+                            style("File already exists:").bold().red(),
+                            style(dst_path.display()).bold().red(),
+                        )
+                    }
+                    (true, true) => {
+                        eprintln!(
+                            "{} {} {}",
+                            emoji::WARN,
+                            style("Overwriting file:").bold().red(),
+                            style(dst_path.display()).bold().red(),
+                        );
+                    }
+                    _ => {}
+                };
             } else {
                 bail!(
                     "{} {}",
@@ -394,7 +409,7 @@ pub(crate) fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Resu
         }
         Ok(())
     }
-    fn copy_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
+    fn copy_all(src: impl AsRef<Path>, dst: impl AsRef<Path>, overwrite: bool) -> Result<()> {
         fs::create_dir_all(&dst)?;
         let git_file_name: OsString = ".git".into();
         for src_entry in fs::read_dir(src)? {
@@ -406,18 +421,21 @@ pub(crate) fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Resu
                 if git_file_name == src_entry.file_name() {
                     continue;
                 }
-                copy_dir_all(src_entry.path(), dst_path)?;
+                copy_dir_all(src_entry.path(), dst_path, overwrite)?;
             } else if entry_type.is_file() {
                 let filename = filename.strip_suffix(".liquid").unwrap_or(&filename);
                 let dst_path = dst.as_ref().join(filename);
+                if dst_path.exists() && overwrite {
+                    fs::remove_file(&dst_path)?;
+                }
                 fs::copy(src_entry.path(), dst_path)?;
             }
         }
         Ok(())
     }
 
-    check_dir_all(&src, &dst)?;
-    copy_all(src, dst)
+    check_dir_all(&src, &dst, overwrite)?;
+    copy_all(src, dst, overwrite)
 }
 
 fn locate_template_file(
diff --git a/src/user_parsed_input.rs b/src/user_parsed_input.rs
index 4600221..03c4e7e 100644
--- a/src/user_parsed_input.rs
+++ b/src/user_parsed_input.rs
@@ -24,6 +24,7 @@ pub struct UserParsedInput {
     template_values: HashMap<String, toml::Value>,
     vcs: Vcs,
     pub init: bool,
+    overwrite: bool,
     //TODO:
     // 1. This structure should be used instead of args
     // 2. This struct can contains internaly args and app_config to not confuse
@@ -37,6 +38,7 @@ impl UserParsedInput {
         default_values: HashMap<String, toml::Value>,
         vcs: Vcs,
         init: bool,
+        overwrite: bool,
     ) -> Self {
         Self {
             template_location: template_location.into(),
@@ -44,6 +46,7 @@ impl UserParsedInput {
             template_values: default_values,
             vcs,
             init,
+            overwrite,
         }
     }
 
@@ -77,6 +80,7 @@ impl UserParsedInput {
                 default_values,
                 args.vcs.unwrap_or(DEFAULT_VCS),
                 args.init,
+                args.overwrite,
             );
         }
 
@@ -88,6 +92,7 @@ impl UserParsedInput {
                 default_values,
                 args.vcs.unwrap_or(DEFAULT_VCS),
                 args.init,
+                args.overwrite,
             );
         }
 
@@ -133,11 +138,12 @@ impl UserParsedInput {
                     .map(|s| s.as_ref().to_owned())
                     .or_else(|| fav_cfg.subfolder.clone()),
                 default_values,
-                args.vcs
-                    .unwrap_or_else(|| fav_cfg.vcs.unwrap_or(DEFAULT_VCS)),
-                args.init
+                args.vcs.or(fav_cfg.vcs).unwrap_or(DEFAULT_VCS),
+                args.init.then(|| true).or(fav_cfg.init).unwrap_or_default(),
+                args.overwrite
                     .then(|| true)
-                    .unwrap_or_else(|| fav_cfg.init.unwrap_or(false)),
+                    .or(fav_cfg.overwrite)
+                    .unwrap_or_default(),
             );
         }
 
@@ -197,6 +203,7 @@ impl UserParsedInput {
             default_values,
             args.vcs.unwrap_or(DEFAULT_VCS),
             args.init,
+            args.overwrite,
         )
     }
 
@@ -223,6 +230,10 @@ impl UserParsedInput {
     pub const fn init(&self) -> bool {
         self.init
     }
+
+    pub const fn overwrite(&self) -> bool {
+        self.overwrite
+    }
 }
 
 // favorite can be in form with abbrevation what means that input is git repositoru


Test patch:
diff --git a/tests/integration/basics.rs b/tests/integration/basics.rs
index 62ea85e..5861400 100644
--- a/tests/integration/basics.rs
+++ b/tests/integration/basics.rs
@@ -462,6 +462,44 @@ version = "0.1.0"
     Ok(())
 }
 
+#[test]
+fn it_can_overwrite_files() -> anyhow::Result<()> {
+    let template = tmp_dir()
+        .file(
+            "Cargo.toml",
+            r#"[package]
+name = "{{project-name}}"
+description = "A wonderful project"
+version = "0.1.0"
+"#,
+        )
+        .init_git()
+        .build();
+    let dir = tmp_dir().build();
+    let _ = binary()
+        .arg("generate")
+        .arg("--git")
+        .arg(template.path())
+        .arg("--name")
+        .arg("my-proj")
+        .arg("--init")
+        .current_dir(&dir.path())
+        .status();
+    binary()
+        .arg("generate")
+        .arg("--git")
+        .arg(template.path())
+        .arg("--name")
+        .arg("overwritten-proj")
+        .arg("--init")
+        .arg("--overwrite")
+        .current_dir(&dir.path())
+        .assert()
+        .success();
+    assert!(dir.read("Cargo.toml").contains("overwritten-proj"));
+    Ok(())
+}
+
 #[test]
 fn it_allows_user_defined_projectname_when_passing_force_flag() {
     let template = tmp_dir()
diff --git a/tests/integration/library.rs b/tests/integration/library.rs
index e19f1f0..39e2bd2 100644
--- a/tests/integration/library.rs
+++ b/tests/integration/library.rs
@@ -35,6 +35,7 @@ fn it_allows_generate_call_with_public_args_and_returns_the_generated_path() {
         destination: Some(dir.clone()),
         force_git_init: false,
         allow_commands: false,
+        overwrite: false,
     };
 
     assert_eq!(