Repository: cargo-generate/cargo-generate
Pull request #414 (2021-08-15 13:33:35)

Add `init` like behavior.
Relates to #193

sassman: could you pls rebase this branche?
sassman: Sorry, needs rebasing again
taurr: @sassman PRs are rebased and ready.

Patch:
diff --git a/README.md b/README.md
index fb60138..aaee9e6 100644
--- a/README.md
+++ b/README.md
@@ -62,6 +62,8 @@ You can also pass the name of your project to the tool using the `--name` or `-n
 cargo generate --git https://github.com/githubusername/mytemplate.git --name myproject
 ```
 
+#### Templates in subfolders
+
 If the git repository contains multiple templates, the specific subfolder in the git repository may be specified like this:
 
 ```sh
@@ -72,6 +74,16 @@ cargo generate --git https://github.com/githubusername/mytemplate.git <relative-
 
 > NOTE: When using the `subfolder` feature, `cargo-generate` will search for the `cargo-generate.toml` file in the subfolder first, traversing back towards the template root in case it is not found.
 
+#### Generating into current dir
+
+If the user wants to generate a template straight into the current folder, without creating a subfolder for the contents and without attempting to initialize a `.git` repo or similar, the `--init` flag can be used.
+
+```sh
+cargo generate --init --git https://github.com/githubusername/mytemplate.git
+```
+
+> NOTE: `cargo-generate` will not allow any existing files to be overwritten and will fail to generate any files should there be any conflicts.
+
 ## git over ssh
 
 New in version [0.7.0] is the support for both public and private and ssh git remote urls.
diff --git a/src/args.rs b/src/args.rs
index 570aaf1..a87173d 100644
--- a/src/args.rs
+++ b/src/args.rs
@@ -101,6 +101,10 @@ pub struct Args {
     /// Define a value for use during template expansion
     #[structopt(long, short, number_of_values = 1)]
     pub define: Vec<String>,
+
+    /// Generate the template directly into the current dir. No subfolder will be created and no vcs is initialized.
+    #[structopt(long)]
+    pub init: bool,
 }
 
 #[derive(Debug, StructOpt, Clone, Copy)]
diff --git a/src/lib.rs b/src/lib.rs
index 0c76343..ece9f5b 100755
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -125,7 +125,7 @@ pub fn generate(mut args: Args) -> Result<()> {
     let default_values = resolve_favorite_args_and_default_values(&app_config, &mut args)?;
 
     let project_name = resolve_project_name(&args)?;
-    let project_dir = resolve_project_dir(&project_name, args.force)?;
+    let project_dir = resolve_project_dir(&project_name, &args)?;
 
     let (template_base_dir, template_folder, branch) = prepare_local_template(&args)?;
 
@@ -137,10 +137,9 @@ pub fn generate(mut args: Args) -> Result<()> {
     let template_values = resolve_template_values(default_values, &args)?;
 
     println!(
-        "{} {} `{}`{}",
+        "{} {} {}",
         emoji::WRENCH,
-        style("Creating project called").bold(),
-        style(project_dir.display()).bold().yellow(),
+        style("Generating template").bold(),
         style("...").bold()
     );
 
@@ -151,8 +150,19 @@ pub fn generate(mut args: Args) -> Result<()> {
         template_config,
         &args,
     )?;
+
+    println!(
+        "{} {} `{}`{}",
+        emoji::WRENCH,
+        style("Moving generated files into:").bold(),
+        style(project_dir.display()).bold().yellow(),
+        style("...").bold()
+    );
     copy_dir_all(&template_folder, &project_dir)?;
-    args.vcs.initialize(&project_dir, branch)?;
+
+    if !args.init {
+        args.vcs.initialize(&project_dir, branch)?;
+    }
 
     println!(
         "{} {} {} {}",
@@ -310,23 +320,54 @@ fn clone_git_template_into_temp(args: &Args) -> Result<(TempDir, String)> {
 }
 
 pub(crate) fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
-    fs::create_dir_all(&dst)?;
-    for entry in fs::read_dir(src)? {
-        let entry = entry?;
-        let ty = entry.file_type()?;
-        if ty.is_dir() {
-            copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
-        } else if ty.is_file() {
-            fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
-        } else {
-            bail!(
-                "{} {}",
-                crate::emoji::WARN,
-                style("Symbolic links not supported").bold().red(),
-            )
+    fn check_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
+        if !dst.as_ref().exists() {
+            return Ok(());
+        }
+
+        for src_entry in fs::read_dir(src)? {
+            let src_entry = src_entry?;
+            let dst_path = dst.as_ref().join(src_entry.file_name());
+            let entry_type = src_entry.file_type()?;
+
+            if entry_type.is_dir() {
+                check_dir_all(src_entry.path(), dst_path)?;
+            } else if entry_type.is_file() {
+                if dst_path.exists() {
+                    bail!(
+                        "{} {} {}",
+                        crate::emoji::WARN,
+                        style("File already exists:").bold().red(),
+                        style(dst_path.display()).bold().red(),
+                    )
+                }
+            } else {
+                bail!(
+                    "{} {}",
+                    crate::emoji::WARN,
+                    style("Symbolic links not supported").bold().red(),
+                )
+            }
         }
+        Ok(())
     }
-    Ok(())
+    fn copy_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
+        fs::create_dir_all(&dst)?;
+        for src_entry in fs::read_dir(src)? {
+            let src_entry = src_entry?;
+            let dst_path = dst.as_ref().join(src_entry.file_name());
+            let entry_type = src_entry.file_type()?;
+            if entry_type.is_dir() {
+                copy_dir_all(src_entry.path(), dst_path)?;
+            } else if entry_type.is_file() {
+                fs::copy(src_entry.path(), dst_path)?;
+            }
+        }
+        Ok(())
+    }
+
+    check_dir_all(&src, &dst)?;
+    copy_all(src, dst)
 }
 
 fn locate_template_file<T>(
@@ -356,8 +397,13 @@ where
     }
 }
 
-fn resolve_project_dir(name: &ProjectName, force: bool) -> Result<PathBuf> {
-    let dir_name = if force {
+fn resolve_project_dir(name: &ProjectName, args: &Args) -> Result<PathBuf> {
+    if args.init {
+        let cwd = env::current_dir()?;
+        return Ok(cwd);
+    }
+
+    let dir_name = if args.force {
         name.raw()
     } else {
         rename_warning(name);


Test patch:
diff --git a/tests/integration/basics.rs b/tests/integration/basics.rs
index 260974f..d6a05cb 100644
--- a/tests/integration/basics.rs
+++ b/tests/integration/basics.rs
@@ -347,6 +347,73 @@ version = "0.1.0"
     Ok(())
 }
 
+#[test]
+fn it_can_generate_into_cwd() -> 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();
+    binary()
+        .arg("generate")
+        .arg("--git")
+        .arg(template.path())
+        .arg("--name")
+        .arg("my-proj")
+        .arg("--init")
+        .current_dir(&dir.path())
+        .assert()
+        .success()
+        .stdout(predicates::str::contains("Done!").from_utf8());
+    assert!(dir.read("Cargo.toml").contains("my-proj"));
+    assert!(!dir.path().join(".git").exists());
+    Ok(())
+}
+
+#[test]
+fn it_refuses_to_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")
+        .current_dir(&dir.path())
+        .assert()
+        .failure();
+    assert!(dir.read("Cargo.toml").contains("my-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 c013114..c2177c2 100644
--- a/tests/integration/library.rs
+++ b/tests/integration/library.rs
@@ -35,6 +35,7 @@ version = "0.1.0"
         lib: false,
         ssh_identity: None,
         define: vec![],
+        init: false,
     };
     // need to cd to the dir as we aren't running in the cargo shell.
     assert!(std::env::set_current_dir(&dir.root).is_ok());