Repository: cargo-generate/cargo-generate
Pull request #379 (2021-07-25 15:33:23)

Feature: specify subdir in repo as template
This change allows to specify a sub-folder inside a git repository for use as the template.

By doing so, it allows us to e.g. generate projects from examples inside a repo, or to have a repo that contains multiple templates.

Relates to #47 and #78 

This pull-request will also make #295 obsolete, as there is no need to specify folders as templates, and thus we avoid a lot of the problems specified in #295 and #291, allowing for easier implementation of #291 (and thus #211).

@sassman Sorry for the long timeout here - as it seems we get no feedback for #295, I suggest this one instead. I still like the idea of `--pick` for #291, but it kinda conflicted and this is my proposal to remedy that!

taurr: @sassman After the changes to 0.7.0 with SSH, I can't run tests neither locally nor here on Github - any ideas? 
sassman: Locally should work. What's the error message?

The github action right now failed because of access to a secret variable.. I will fix it ASAP 
taurr: test basics::ssh_remote::it_should_support_a_public_repo ... FAILED
test basics::ssh_remote::it_should_support_a_private_repo ... FAILED

failures:

---- basics::ssh_remote::it_should_support_a_public_repo stdout ----
thread 'basics::ssh_remote::it_should_support_a_public_repo' panicked at 'Unexpected failure.
code-1
stderr=```"Error: ⛔   Git Error: Failed to retrieve list of SSH authentication methods: Failed getting response; class=Ssh (23)\n"```
command=`"/home/johnny/projects/github/cargo-generate/target/debug/cargo-generate" "generate" "--git" "git@github.com:ashleygwilliams/wasm-pack-template.git" "--name" "foobar-project"`
code=1
stdout=```"🔧   Creating project called `foobar-project`...\n💡   Using private key: `$HOME/.ssh/id_rsa` for git-ssh checkout\n"```
stderr=```"Error: ⛔   Git Error: Failed to retrieve list of SSH authentication methods: Failed getting response; class=Ssh (23)\n"```
', /home/johnny/.cargo/registry/src/github.com-1ecc6299db9ec823/assert_cmd-1.0.7/src/assert.rs:155:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

---- basics::ssh_remote::it_should_support_a_private_repo stdout ----
thread 'basics::ssh_remote::it_should_support_a_private_repo' panicked at 'Unexpected failure.
code-1
stderr=```"Error: ⛔   Git Error: Failed to retrieve list of SSH authentication methods: Failed getting response; class=Ssh (23)\n"```
command=`"/home/johnny/projects/github/cargo-generate/target/debug/cargo-generate" "generate" "--git" "git@github.com:cargo-generate/wasm-pack-template.git" "--name" "foobar-project"`
code=1
stdout=```"🔧   Creating project called `foobar-project`...\n💡   Using private key: `$HOME/.ssh/id_rsa` for git-ssh checkout\n"```
stderr=```"Error: ⛔   Git Error: Failed to retrieve list of SSH authentication methods: Failed getting response; class=Ssh (23)\n"```
', /home/johnny/.cargo/registry/src/github.com-1ecc6299db9ec823/assert_cmd-1.0.7/src/assert.rs:155:13


failures:
    basics::ssh_remote::it_should_support_a_private_repo
    basics::ssh_remote::it_should_support_a_public_repo

test result: FAILED. 37 passed; 2 failed; 1 ignored; 0 measured; 0 filtered out; finished in 10.91s

error: test failed, to rerun pass '--test integration'
taurr: Famous words: It works on my machine(s) 😄 
taurr: Hmm, I probably shouldn't have force pushed my latest commit with the review changes - but I thought: hey @sassman is keen on having a clean git history, so I'll make it easy...

And now I don't know how to get Github to recognize I've made the needed changes 😦 
sassman: I recommend squashing at the end of the review when all things are settled.

Patch:
diff --git a/README.md b/README.md
index 99e2ce5..7ce3833 100644
--- a/README.md
+++ b/README.md
@@ -62,6 +62,16 @@ 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
 ```
 
+If the git repository contains multiple templates, the specific subfolder in the git repository may be specified like this:
+
+```sh
+cargo generate --git "https://github.com/githubusername/mytemplate.git <relative-template-path>"
+```
+
+> NOTE: The specified `relative-template-path` will be used as the actual template root, whether or not this is actually true!
+
+> NOTE: When using the `subfolder` feature, `cargo-generate` will search for
+
 ## git over ssh
 
 New in version [0.7.0] is the support for both public and private and ssh git remote urls.
@@ -82,7 +92,7 @@ cargo generate --git rustwasm/wasm-pack-template --name mywasm
 
 ## http(s) proxy
 
-New in version [0.7.0] is automatic proxy usage. So, if http(s)_PROXY env variables are provided, they 
+New in version [0.7.0] is automatic proxy usage. So, if http(s)\_PROXY env variables are provided, they 
 will be used for cloning a http(s) template repository. 
 
 ## Favorites
@@ -94,12 +104,13 @@ Each favorite template is specified in its own section, e.g.:
 
 ```toml
 [favorites.demo]
-description = "Demo template for cargo-generate"
+description = "<optional description, visible with --list-favorites>"
 git = "https://github.com/ashleygwilliams/wasm-pack-template"
-branch = "master"
+branch = "<optional-branch>"
+subfolder = "<optional-subfolder>"
 ```
 
-Both `branch` and `description` are optional, and the branch may be overridden by specifying `--branch <branch>` on the command line.
+Values may be overridden using the CLI arguments of the same names (e.g. `--subfolder` for the `subfolder` value).
 
 When favorites are available, they can be generated simply by invoking:
 
@@ -110,7 +121,7 @@ cargo gen <favorite>
 or slightly more involved:
 
 ```cli
-cargo generate demo --branch master --name expanded_demo
+cargo generate demo --branch mybranch --name expanded_demo --subfolder myfolder
 ```
 
 > NOTE: when `<favorite>` is not defined in the config file, it is interpreted as a git repo like as if `--git <favorite>`
@@ -295,6 +306,8 @@ include = ["Cargo.toml"]
 exclude = ["*.c"]
 ```
 
+The `cargo-generate.toml` file should be placed in the root of the template. If using the `subfolder` feature, the root is the `subfolder` inside the repository, though `cargo-generate` will look for the file in all parent folders until it reaches the repository root.
+
 ## Cargo gen - alias
 
 `cargo gen` requires an [cargo alias](https://doc.rust-lang.org/cargo/reference/config.html) 
@@ -323,4 +336,4 @@ conditions.
 If you want to contribute to `cargo-generate`, please read our [CONTRIBUTING notes].
 
 [CONTRIBUTING notes]: CONTRIBUTING.md
-[0.7.0]: https://github.com/cargo-generate/cargo-generate/releases/tag/v0.7.0
\ No newline at end of file
+[0.7.0]: https://github.com/cargo-generate/cargo-generate/releases/tag/v0.7.0
diff --git a/src/app_config.rs b/src/app_config.rs
index 1a83a40..9c55c08 100644
--- a/src/app_config.rs
+++ b/src/app_config.rs
@@ -18,6 +18,7 @@ pub(crate) struct FavoriteConfig {
     pub description: Option<String>,
     pub git: Option<String>,
     pub branch: Option<String>,
+    pub subfolder: Option<String>,
 }
 
 impl Default for AppConfig {
diff --git a/src/config.rs b/src/config.rs
index 256d3e3..57bd792 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -51,15 +51,21 @@ impl TryFrom<String> for Config {
 }
 
 impl Config {
-    pub(crate) fn new<P: AsRef<Path>>(path: P) -> Result<Option<Self>> {
-        match fs::read_to_string(path) {
-            Ok(contents) => Config::try_from(contents)
-                .map(Option::from)
-                .map_err(|e| e.into()),
-            Err(e) => match e.kind() {
-                ErrorKind::NotFound => Ok(None),
-                _ => anyhow::bail!(e),
+    pub(crate) fn from_path<P>(path: &Option<P>) -> Result<Option<Self>>
+    where
+        P: AsRef<Path>,
+    {
+        match path {
+            Some(path) => match fs::read_to_string(path) {
+                Ok(contents) => Config::try_from(contents)
+                    .map(Option::from)
+                    .map_err(|e| e.into()),
+                Err(e) => match e.kind() {
+                    ErrorKind::NotFound => Ok(None),
+                    _ => anyhow::bail!(e),
+                },
             },
+            None => Ok(None),
         }
     }
 }
@@ -89,7 +95,7 @@ mod tests {
         )
         .unwrap();
 
-        let config = Config::new(&config_path).unwrap().unwrap();
+        let config = Config::from_path(&Some(config_path)).unwrap().unwrap();
 
         assert_eq!(
             config.template,
diff --git a/src/favorites.rs b/src/favorites.rs
index 50c40b3..654e1fb 100644
--- a/src/favorites.rs
+++ b/src/favorites.rs
@@ -35,8 +35,9 @@ pub(crate) fn list_favorites(args: &Args) -> Result<()> {
     Ok(())
 }
 
-pub(crate) fn resolve_favorite(args: &mut Args) -> Result<()> {
+pub(crate) fn resolve_favorite_args(args: &mut Args) -> Result<()> {
     if args.git.is_some() {
+        args.subfolder = args.favorite.take();
         return Ok(());
     }
 
@@ -47,7 +48,8 @@ pub(crate) fn resolve_favorite(args: &mut Args) -> Result<()> {
 
     let app_config_path = app_config_path(&args.config)?;
     let app_config = AppConfig::from_path(app_config_path.as_path())?;
-    let favorite = app_config
+
+    let (git, branch, subfolder) = app_config
         .favorites
         .get(favorite_name.as_str())
         .map_or_else(
@@ -56,17 +58,27 @@ pub(crate) fn resolve_favorite(args: &mut Args) -> Result<()> {
                     "Favorite {} not found in config, using it as a git repo url",
                     style(&favorite_name).bold()
                 );
-                (Some(favorite_name.clone()), args.branch.as_ref().cloned())
+                (
+                    Some(favorite_name.clone()),
+                    args.branch.as_ref().cloned(),
+                    args.subfolder.clone(),
+                )
             },
             |f| {
                 (
                     f.git.clone(),
                     args.branch.as_ref().or_else(|| f.branch.as_ref()).cloned(),
+                    args.subfolder
+                        .as_ref()
+                        .or_else(|| f.subfolder.as_ref())
+                        .cloned(),
                 )
             },
         );
-    args.git = favorite.0;
-    args.branch = favorite.1;
+
+    args.git = git;
+    args.branch = branch;
+    args.subfolder = subfolder;
 
     Ok(())
 }
diff --git a/src/lib.rs b/src/lib.rs
index 72614e0..9c9c42d 100755
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -92,17 +92,19 @@ mod project_variables;
 mod template;
 mod template_variables;
 
-use anyhow::{Context, Result};
+use anyhow::{anyhow, bail, Context, Result};
 use config::{Config, ConfigValues, CONFIG_FILE_NAME};
 use console::style;
-use favorites::{list_favorites, resolve_favorite};
+use favorites::{list_favorites, resolve_favorite_args};
 use std::{
+    borrow::Borrow,
     collections::HashMap,
     env, fs,
     path::{Path, PathBuf},
     str::FromStr,
 };
 use structopt::StructOpt;
+use tempfile::TempDir;
 
 use crate::git::GitConfig;
 use crate::template_variables::{CrateType, ProjectName};
@@ -120,15 +122,22 @@ pub struct Args {
     #[structopt(long)]
     pub list_favorites: bool,
 
-    /// Generate a favorite template as defined in the config
+    /// Generate a favorite template as defined in the config. In case the favorite is undefined,
+    /// use in place of the `--git` option, otherwise specifies the subfolder
+    #[structopt(name = "favorite|git|subfolder")]
     pub favorite: Option<String>,
 
+    /// Specifies a subfolder within the template repository to be used as the actual template.
+    #[structopt()]
+    pub subfolder: Option<String>,
+
     /// Git repository to clone template from. Can be a URL (like
     /// `https://github.com/rust-cli/cli-template`), a path (relative or absolute), or an
     /// `owner/repo` abbreviated GitHub URL (like `rust-cli/cli-template`).
+    ///
     /// Note that cargo generate will first attempt to interpret the `owner/repo` form as a
     /// relative path and only try a GitHub URL if the local path doesn't exist.
-    #[structopt(short, long)]
+    #[structopt(short, long, conflicts_with = "subfolder")]
     pub git: Option<String>,
 
     /// Branch to use when installing from git
@@ -195,7 +204,7 @@ impl FromStr for Vcs {
         match s.to_uppercase().as_str() {
             "NONE" => Ok(Vcs::None),
             "GIT" => Ok(Vcs::Git),
-            _ => Err(anyhow::anyhow!("Must be one of 'git' or 'none'")),
+            _ => Err(anyhow!("Must be one of 'git' or 'none'")),
         }
     }
 }
@@ -204,72 +213,191 @@ pub fn generate(mut args: Args) -> Result<()> {
     if args.list_favorites {
         return list_favorites(&args);
     }
-    resolve_favorite(&mut args)?;
 
-    let name = match args.name {
-        Some(ref n) => ProjectName::new(n),
-        None if !args.silent => ProjectName::new(interactive::name()?),
-        None => anyhow::bail!(
+    resolve_favorite_args(&mut args)?;
+
+    let project_name = resolve_project_name(&args)?;
+    let project_dir = create_project_dir(&project_name, args.force)?;
+
+    let (template_base_dir, branch) = clone_git_template_into_temp(&args)?;
+    let template_folder = resolve_template_dir(&template_base_dir, &args)?;
+
+    let template_values = args
+        .template_values_file
+        .as_ref()
+        .map(|p| Path::new(p))
+        .map_or(Ok(Default::default()), |path| get_config_file_values(&path))?;
+
+    let template_config = Config::from_path(
+        &locate_template_file(&CONFIG_FILE_NAME, &template_base_dir, &args.subfolder).ok(),
+    )?;
+
+    expand_template(
+        &project_name,
+        &template_folder,
+        &template_values,
+        template_config,
+        &args,
+    )?;
+
+    copy_dir_all(&template_folder, &project_dir)?;
+
+    initialize_vcs(args, &project_dir, branch)?;
+
+    println!(
+        "{} {} {} {}",
+        emoji::SPARKLE,
+        style("Done!").bold().green(),
+        style("New project created").bold(),
+        style(&project_dir.display()).underlined()
+    );
+    Ok(())
+}
+
+fn initialize_vcs(args: Args, project_dir: &Path, branch: String) -> Result<()> {
+    match args.vcs {
+        Vcs::None => {}
+        Vcs::Git => {
+            git::init(project_dir, &branch)?;
+        }
+    };
+    Ok(())
+}
+
+fn resolve_project_name(args: &Args) -> Result<ProjectName> {
+    match args.name {
+        Some(ref n) => Ok(ProjectName::new(n)),
+        None if !args.silent => Ok(ProjectName::new(interactive::name()?)),
+        None => Err(anyhow!(
             "{} {} {}",
             emoji::ERROR,
             style("Project Name Error:").bold().red(),
-            style("Option `--silent` provided, but project name was not set. Please use `--project-name`.")
+            style("Option `--silent` provided, but project name was not set. Please use `--name`.")
                 .bold()
                 .red(),
-        ),
-    };
+        )),
+    }
+}
 
-    create_git(args, &name)?;
-    Ok(())
+fn resolve_template_dir(template_base_dir: &TempDir, args: &Args) -> Result<PathBuf> {
+    match &args.subfolder {
+        Some(subfolder) => {
+            let template_base_dir = fs::canonicalize(template_base_dir.path())?;
+            let template_dir = fs::canonicalize(template_base_dir.join(subfolder))?;
+
+            if !template_dir.starts_with(&template_base_dir) {
+                return Err(anyhow!(
+                    "{} {} {}",
+                    emoji::ERROR,
+                    style("Subfolder Error:").bold().red(),
+                    style("Invalid subfolder. Must be part of the template folder structure.")
+                        .bold()
+                        .red(),
+                ));
+            }
+            if !template_dir.is_dir() {
+                return Err(anyhow!(
+                    "{} {} {}",
+                    emoji::ERROR,
+                    style("Subfolder Error:").bold().red(),
+                    style("The specified subfolder must be a valid folder.")
+                        .bold()
+                        .red(),
+                ));
+            }
+
+            println!(
+                "{} {} `{}`{}",
+                emoji::WRENCH,
+                style("Using template subfolder").bold(),
+                style(subfolder).bold().yellow(),
+                style("...").bold()
+            );
+            Ok(template_dir)
+        }
+        None => Ok(template_base_dir.path().to_owned()),
+    }
 }
 
-fn create_git(args: Args, name: &ProjectName) -> Result<()> {
-    let force = args.force;
-    let template_values = args
-        .template_values_file
-        .as_ref()
-        .map(|p| Path::new(p))
-        .map_or(Ok(Default::default()), |path| get_config_file_values(path))?;
+fn clone_git_template_into_temp(args: &Args) -> Result<(TempDir, String)> {
+    let git_clone_dir = tempfile::tempdir()?;
+
     let remote = args
         .git
         .clone()
         .with_context(|| "Missing option git, or a favorite")?;
+
     let git_config = GitConfig::new_abbr(
         remote.into(),
         args.branch.to_owned(),
         args.ssh_identity.clone(),
     )?;
 
-    if let Some(dir) = &create_project_dir(&name, force) {
-        match git::create(dir, git_config) {
-            Ok(branch) => {
-                progress(name, &template_values, dir, &branch, &args)?;
-            }
-            Err(e) => anyhow::bail!(
-                "{} {} {}",
-                emoji::ERROR,
-                style("Git Error:").bold().red(),
-                style(e).bold().red(),
-            ),
-        };
-    } else {
-        anyhow::bail!(
-            "{} {}",
+    let branch = git::create(&git_clone_dir.path(), git_config).map_err(|e| {
+        anyhow!(
+            "{} {} {}",
             emoji::ERROR,
-            style("Target directory already exists, aborting!")
-                .bold()
-                .red(),
-        );
+            style("Git Error:").bold().red(),
+            style(e).bold().red(),
+        )
+    })?;
+
+    Ok((git_clone_dir, branch))
+}
+
+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(),
+            )
+        }
     }
     Ok(())
 }
 
+fn locate_template_file<T>(
+    name: &str,
+    template_folder: T,
+    subfolder: &Option<String>,
+) -> Result<PathBuf>
+where
+    T: AsRef<Path>,
+{
+    let template_folder = template_folder.as_ref().to_path_buf();
+    let mut search_folder = subfolder
+        .as_ref()
+        .map_or_else(|| template_folder.to_owned(), |s| template_folder.join(s));
+    loop {
+        let file_path = search_folder.join(name.borrow());
+        if file_path.exists() {
+            return Ok(file_path);
+        }
+        if search_folder == template_folder {
+            bail!("File not found within template");
+        }
+        search_folder = search_folder
+            .parent()
+            .ok_or_else(|| anyhow!("Reached root folder"))?
+            .to_path_buf();
+    }
+}
+
 fn get_config_file_values(path: &Path) -> Result<HashMap<String, toml::Value>> {
     match fs::read_to_string(path) {
         Ok(ref contents) => toml::from_str::<ConfigValues>(contents)
             .map(|v| v.values)
             .map_err(|e| e.into()),
-        Err(e) => anyhow::bail!(
+        Err(e) => bail!(
             "{} {} {}",
             emoji::ERROR,
             style("Values File Error:").bold().red(),
@@ -278,7 +406,7 @@ fn get_config_file_values(path: &Path) -> Result<HashMap<String, toml::Value>> {
     }
 }
 
-fn create_project_dir(name: &ProjectName, force: bool) -> Option<PathBuf> {
+fn create_project_dir(name: &ProjectName, force: bool) -> Result<PathBuf> {
     let dir_name = if force {
         name.raw()
     } else {
@@ -296,25 +424,28 @@ fn create_project_dir(name: &ProjectName, force: bool) -> Option<PathBuf> {
         style(&dir_name).bold().yellow(),
         style("...").bold()
     );
-
     if project_dir.exists() {
-        None
+        Err(anyhow!(
+            "{} {}",
+            emoji::ERROR,
+            style("Target directory already exists, aborting!")
+                .bold()
+                .red()
+        ))
     } else {
-        Some(project_dir)
+        Ok(project_dir)
     }
 }
 
-fn progress(
+fn expand_template(
     name: &ProjectName,
-    template_values: &HashMap<String, toml::Value>,
     dir: &Path,
-    branch: &str,
+    template_values: &HashMap<String, toml::Value>,
+    template_config: Option<Config>,
     args: &Args,
 ) -> Result<()> {
     let crate_type: CrateType = args.into();
     let template = template::substitute(name, &crate_type, template_values, args.force)?;
-    let config_path = dir.join(CONFIG_FILE_NAME);
-    let template_config = Config::new(config_path)?;
     let template = match template_config.as_ref() {
         None => Ok(template),
         Some(config) => {
@@ -336,29 +467,9 @@ fn progress(
 
     pbar.join().unwrap();
 
-    match args.vcs {
-        Vcs::None => {}
-        Vcs::Git => {
-            git::init(dir, branch)?;
-        }
-    }
-
-    gen_success(dir);
-
     Ok(())
 }
 
-fn gen_success(dir: &Path) {
-    let dir_string = dir.to_str().unwrap_or("");
-    println!(
-        "{} {} {} {}",
-        emoji::SPARKLE,
-        style("Done!").bold().green(),
-        style("New project created").bold(),
-        style(dir_string).underlined()
-    );
-}
-
 fn rename_warning(name: &ProjectName) {
     if !name.is_crate_name() {
         info!(


Test patch:
diff --git a/tests/integration/favorites.rs b/tests/integration/favorites.rs
index d4cc314..8a1c0a5 100644
--- a/tests/integration/favorites.rs
+++ b/tests/integration/favorites.rs
@@ -51,16 +51,16 @@ branch = "{branch}"
 }
 
 #[test]
-fn it_survives_an_empty_config() {
-    let empty_config = tmp_dir().build();
-    let empty_config_path = empty_config.path().join("cargo-generate");
+fn favorite_with_git_becomes_subfolder() {
+    let favorite_template = create_template("favorite-template");
     let git_template = create_template("git-template");
+    let (_config, config_path) = create_favorite_config("test", &favorite_template);
     let dir = tmp_dir().build();
 
     binary()
         .arg("generate")
         .arg("--config")
-        .arg(empty_config_path)
+        .arg(config_path)
         .arg("--name")
         .arg("foobar-project")
         .arg("--git")
@@ -68,40 +68,93 @@ fn it_survives_an_empty_config() {
         .arg("test")
         .current_dir(&dir.path())
         .assert()
-        .success()
-        .stdout(predicates::str::contains("Done!").from_utf8());
-
-    assert!(dir
-        .read("foobar-project/Cargo.toml")
-        .contains(r#"description = "git-template""#));
+        .failure();
 }
 
 #[test]
-fn it_can_use_favorites() {
-    let favorite_template = create_template("favorite-template");
-    let (_config, config_path) = create_favorite_config("test", &favorite_template);
+fn favorite_subfolder_must_be_valid() {
+    let template = tmp_dir()
+        .file("Cargo.toml", "")
+        .file(
+            "inner/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("--config")
-        .arg(config_path)
-        .arg("--name")
-        .arg("favorite-project")
-        .arg("test")
+        .arg("-n")
+        .arg("outer")
+        .arg(template.path())
+        .arg("Cargo.toml")
+        .current_dir(&dir.path())
+        .assert()
+        .failure()
+        .stderr(predicates::str::contains("must be a valid folder").from_utf8());
+
+    binary()
+        .arg("generate")
+        .arg("-n")
+        .arg("outer")
+        .arg(template.path())
+        .arg("non-existant")
+        .current_dir(&dir.path())
+        .assert()
+        .failure(); // Error text is OS specific
+
+    binary()
+        .arg("generate")
+        .arg("-n")
+        .arg("outer")
+        .arg(template.path())
+        .arg(dir.path().parent().unwrap())
+        .current_dir(&dir.path())
+        .assert()
+        .failure()
+        .stderr(predicates::str::contains("Invalid subfolder.").from_utf8());
+}
+
+#[test]
+fn favorite_with_subfolder() -> anyhow::Result<()> {
+    let template = tmp_dir()
+        .file("Cargo.toml", "")
+        .file(
+            "inner/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("-n")
+        .arg("outer")
+        .arg(template.path())
+        .arg("inner")
         .current_dir(&dir.path())
         .assert()
         .success()
         .stdout(predicates::str::contains("Done!").from_utf8());
 
-    assert!(dir
-        .read("favorite-project/Cargo.toml")
-        .contains(r#"description = "favorite-template""#));
+    assert!(dir.read("outer/Cargo.toml").contains("outer"));
+    Ok(())
 }
 
 #[test]
-fn git_specification_overrides_favorite() {
-    let git_template = create_template("git-template");
+fn it_can_use_favorites() {
     let favorite_template = create_template("favorite-template");
     let (_config, config_path) = create_favorite_config("test", &favorite_template);
     let dir = tmp_dir().build();
@@ -112,8 +165,6 @@ fn git_specification_overrides_favorite() {
         .arg(config_path)
         .arg("--name")
         .arg("favorite-project")
-        .arg("--git")
-        .arg(git_template.path())
         .arg("test")
         .current_dir(&dir.path())
         .assert()
@@ -122,7 +173,7 @@ fn git_specification_overrides_favorite() {
 
     assert!(dir
         .read("favorite-project/Cargo.toml")
-        .contains(r#"description = "git-template""#));
+        .contains(r#"description = "favorite-template""#));
 }
 
 #[test]
diff --git a/tests/integration/library.rs b/tests/integration/library.rs
index 7938c40..0973960 100644
--- a/tests/integration/library.rs
+++ b/tests/integration/library.rs
@@ -20,6 +20,7 @@ version = "0.1.0"
     let args_exposed: Args = Args {
         git: Some(format!("{}", template.path().display())),
         branch: Some(String::from("main")),
+        subfolder: None,
         name: Some(String::from("foobar_project")),
         force: true,
         vcs: Vcs::Git,