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

Issue:
feat: support `--tag` in addition to `--branch`
It's really difficult right now to expand a specific tagged version of a template.

We really should allow to use tagged versions!

Pull request description:
feat: support `--tag` in addition to `--branch`
Fixes #647

Patch:
diff --git a/src/app_config.rs b/src/app_config.rs
index 232ebcb..cfda68b 100644
--- a/src/app_config.rs
+++ b/src/app_config.rs
@@ -29,6 +29,7 @@ pub struct FavoriteConfig {
     pub description: Option<String>,
     pub git: Option<String>,
     pub branch: Option<String>,
+    pub tag: Option<String>,
     pub subfolder: Option<String>,
     pub path: Option<PathBuf>,
     pub values: Option<HashMap<String, toml::Value>>,
diff --git a/src/args.rs b/src/args.rs
index dab866d..7d1661d 100644
--- a/src/args.rs
+++ b/src/args.rs
@@ -136,9 +136,13 @@ pub struct TemplatePath {
     pub git: Option<String>,
 
     /// Branch to use when installing from git
-    #[clap(short, long)]
+    #[clap(short, long, conflicts_with = "tag")]
     pub branch: Option<String>,
 
+    /// Tag to use when installing from git
+    #[clap(short, long, conflicts_with = "branch")]
+    pub tag: Option<String>,
+
     /// Local path to copy the template from. Can not be specified together with --git.
     #[clap(short, long, group("SpecificPath"))]
     pub path: Option<String>,
@@ -170,6 +174,10 @@ impl TemplatePath {
         self.branch.as_ref()
     }
 
+    pub const fn tag(&self) -> Option<&(impl AsRef<str> + '_)> {
+        self.tag.as_ref()
+    }
+
     pub const fn path(&self) -> Option<&(impl AsRef<str> + '_)> {
         self.path.as_ref()
     }
diff --git a/src/git/mod.rs b/src/git/mod.rs
index f6591b9..beeffee 100644
--- a/src/git/mod.rs
+++ b/src/git/mod.rs
@@ -68,16 +68,16 @@ impl<'cb> RepoCloneBuilder<'cb> {
     }
 
     pub fn new_with(url: &str, branch: Option<&str>, identity_path: Option<&Path>) -> Result<Self> {
-        let mut builer = Self::new(url)?;
+        let mut builder = Self::new(url)?;
         if let Some(branch) = branch {
-            builer.set_branch(branch);
+            builder.set_branch(branch);
         }
 
         if let Some(identity_path) = identity_path {
-            builer.set_identity(identity_path);
+            builder.set_identity(identity_path);
         }
 
-        Ok(builer)
+        Ok(builder)
     }
 
     pub fn set_identity(&mut self, identity_path: &Path) {
diff --git a/src/git/utils.rs b/src/git/utils.rs
index 7ff46a5..d9410ec 100644
--- a/src/git/utils.rs
+++ b/src/git/utils.rs
@@ -31,6 +31,7 @@ pub fn home() -> Result<PathBuf> {
 pub fn clone_git_template_into_temp(
     git: &str,
     branch: Option<&str>,
+    tag: Option<&str>,
     identity: Option<&Path>,
 ) -> anyhow::Result<(TempDir, String)> {
     let git_clone_dir = tempfile::tempdir()?;
@@ -42,6 +43,15 @@ pub fn clone_git_template_into_temp(
         .context("Please check if the Git user / repository exists.")?;
     let branch = get_branch_name_repo(&repo)?;
 
+    if let Some(tag) = tag {
+        let (object, reference) = repo.revparse_ext(tag)?;
+        repo.checkout_tree(&object, None)?;
+        match reference {
+            Some(gref) => repo.set_head(gref.name().unwrap()),
+            None => repo.set_head_detached(object.id()),
+        }?;
+    }
+
     Ok((git_clone_dir, branch))
 }
 
diff --git a/src/lib.rs b/src/lib.rs
index b5fbb31..3f5e8c6 100755
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -173,8 +173,12 @@ fn get_source_template_into_temp(
     let branch: String;
     match template_location {
         TemplateLocation::Git(git) => {
-            let (temp_dir2, branch2) =
-                git::clone_git_template_into_temp(git.url(), git.branch(), git.identity())?;
+            let (temp_dir2, branch2) = git::clone_git_template_into_temp(
+                git.url(),
+                git.branch(),
+                git.tag(),
+                git.identity(),
+            )?;
             temp_dir = temp_dir2;
             branch = branch2;
         }
diff --git a/src/user_parsed_input.rs b/src/user_parsed_input.rs
index a8ace7e..d2ac91c 100644
--- a/src/user_parsed_input.rs
+++ b/src/user_parsed_input.rs
@@ -58,6 +58,7 @@ impl UserParsedInput {
             let git_user_in = GitUserInput::new(
                 git_url,
                 args.template_path.branch(),
+                args.template_path.tag(),
                 ssh_identity,
                 args.force_git_init,
             );
@@ -87,9 +88,15 @@ impl UserParsedInput {
                         .branch()
                         .map(|s| s.as_ref().to_owned())
                         .or_else(|| fav_cfg.branch.clone());
+                    let tag = args
+                        .template_path
+                        .tag()
+                        .map(|s| s.as_ref().to_owned())
+                        .or_else(|| fav_cfg.tag.clone());
                     let git_user_input = GitUserInput::new(
                         git_url,
                         branch.as_ref(),
+                        tag.as_ref(),
                         ssh_identity,
                         args.force_git_init,
                     );
@@ -138,6 +145,7 @@ impl UserParsedInput {
             let git_user_in = GitUserInput::new(
                 &fav_name,
                 args.template_path.branch(),
+                args.template_path.tag(),
                 ssh_identity,
                 args.force_git_init,
             );
@@ -217,24 +225,28 @@ pub fn local_path(fav: &str) -> Option<PathBuf> {
 pub struct GitUserInput {
     url: String,
     branch: Option<String>,
+    tag: Option<String>,
     identity: Option<PathBuf>,
     _force_init: bool,
 }
 
 impl GitUserInput {
-    fn new<T1, T2>(
+    fn new<T1, T2, T3>(
         url: &T1,
         branch: Option<&T2>,
+        tag: Option<&T3>,
         identity: Option<PathBuf>,
         force_init: bool,
     ) -> Self
     where
         T1: AsRef<str>,
         T2: AsRef<str>,
+        T3: AsRef<str>,
     {
         Self {
             url: url.as_ref().to_owned(),
             branch: branch.map(|s| s.as_ref().to_owned()),
+            tag: tag.map(|s| s.as_ref().to_owned()),
             identity,
             _force_init: force_init,
         }
@@ -248,6 +260,7 @@ impl GitUserInput {
         Self::new(
             url,
             args.template_path.branch(),
+            args.template_path.tag(),
             args.ssh_identity.clone(),
             args.force_git_init,
         )
@@ -261,6 +274,10 @@ impl GitUserInput {
         self.branch.as_deref()
     }
 
+    pub fn tag(&self) -> Option<&str> {
+        self.tag.as_deref()
+    }
+
     pub fn identity(&self) -> Option<&Path> {
         self.identity.as_deref()
     }


Test patch:
diff --git a/tests/integration/git.rs b/tests/integration/git.rs
index 1061f83..3723e0f 100644
--- a/tests/integration/git.rs
+++ b/tests/integration/git.rs
@@ -32,6 +32,29 @@ fn it_allows_a_git_branch_to_be_specified() {
         .contains("foobar-project"));
 }
 
+#[test]
+fn it_allows_a_git_tag_to_be_specified() {
+    let template = tmp_dir().init_default_template().tag("v1.0").build();
+    let dir = tmp_dir().build();
+
+    binary()
+        .arg("generate")
+        .arg("--tag")
+        .arg("v1.0")
+        .arg("--git")
+        .arg(template.path())
+        .arg("--name")
+        .arg("foobar-project")
+        .current_dir(&dir.path())
+        .assert()
+        .success()
+        .stdout(predicates::str::contains("Done!").from_utf8());
+
+    assert!(dir
+        .read("foobar-project/Cargo.toml")
+        .contains("foobar-project"));
+}
+
 #[test]
 fn it_removes_git_history() {
     let template = tmp_dir().init_default_template().build();
diff --git a/tests/integration/helpers/project_builder.rs b/tests/integration/helpers/project_builder.rs
index 1486236..bcd76df 100644
--- a/tests/integration/helpers/project_builder.rs
+++ b/tests/integration/helpers/project_builder.rs
@@ -11,6 +11,7 @@ pub struct ProjectBuilder {
     root: TempDir,
     git: bool,
     branch: Option<String>,
+    tag: Option<String>,
 }
 
 pub fn tmp_dir() -> ProjectBuilder {
@@ -20,6 +21,7 @@ pub fn tmp_dir() -> ProjectBuilder {
         root: tempdir().unwrap(),
         git: false,
         branch: None,
+        tag: None,
     }
 }
 
@@ -55,6 +57,11 @@ version = "0.1.0"
         self
     }
 
+    pub fn tag(mut self, tag: &str) -> Self {
+        self.tag = Some(tag.to_owned());
+        self
+    }
+
     pub fn add_submodule<I: Into<String>>(mut self, destination: I, path: I) -> Self {
         self.submodules.push((destination.into(), path.into()));
         self
@@ -163,6 +170,40 @@ version = "0.1.0"
                 .assert()
                 .success();
 
+            if let Some(ref tag) = self.tag {
+                Command::new("git")
+                    .arg("tag")
+                    .arg("-a")
+                    .arg(tag)
+                    .arg("-m")
+                    .arg(format!("our test tag {tag}"))
+                    .current_dir(&path)
+                    .assert()
+                    .success();
+
+                for &(ref file, _) in self.files.iter() {
+                    let path = path.join(file);
+                    fs::remove_file(&path).unwrap_or_else(|_| {
+                        panic!("couldn't remove file {path:?}, after commiting tag {tag}")
+                    });
+                }
+
+                Command::new("git")
+                    .arg("add")
+                    .arg("--all")
+                    .current_dir(&path)
+                    .assert()
+                    .success();
+
+                Command::new("git")
+                    .arg("commit")
+                    .arg("--message")
+                    .arg("dummy commit after tag")
+                    .current_dir(&path)
+                    .assert()
+                    .success();
+            }
+
             if self.branch.is_some() {
                 Command::new("git")
                     .arg("checkout")
diff --git a/tests/integration/library.rs b/tests/integration/library.rs
index d63e071..68a1195 100644
--- a/tests/integration/library.rs
+++ b/tests/integration/library.rs
@@ -12,6 +12,7 @@ fn it_allows_generate_call_with_public_args_and_returns_the_generated_path() {
             auto_path: None,
             git: Some(format!("{}", template.path().display())),
             branch: Some(String::from("main")),
+            tag: None,
             path: None,
             favorite: None,
             subfolder: None,