Repository: cocogitto/cocogitto
Pull request #261 (2023-02-11 13:05:15)

feat(bump): support annotated tags
Many projects use annotated commits for their releases. This commit extends `cog bump` with the `-A MSG`/`--annoted MSG` argument, which will create an annotated tag with the tag message `MSG`.

Much like the bump hooks, `{{version}}` and `{{latest}}` can be used in the provided tag message and will be replaced accordingly.

cocogitto-bot[bot]: :heavy_check_mark: 3f1a309 - Conventional commits check succeeded.
codesee-maps[bot]: <details>
<summary><b>👇 <a href="https://app.codesee.io/r/reviews?pr=261&src=https%3A%2F%2Fgithub.com%2Fcocogitto%2Fcocogitto">Click on the image</a> for a new way to code review</b>
</summary>

  - Make big changes easier — review code in small groups of related files
  - Know where to start — see the whole change at a glance
  - Take a code tour — explore the change with an interactive tour
  - Make comments and review — all fully sync’ed with github

    [Try it now!](https://app.codesee.io/r/reviews?pr=261&src=https%3A%2F%2Fgithub.com%2Fcocogitto%2Fcocogitto)
</details>

[![Review these changes using an interactive CodeSee Map](https://s3.us-east-2.amazonaws.com/maps.codesee.io/images/github/cocogitto/cocogitto/261/c6153c90/3f1a309eea3944b553916eb9897422c8d91bee70.svg)](https://app.codesee.io/r/reviews?pr=261&src=https%3A%2F%2Fgithub.com%2Fcocogitto%2Fcocogitto)

#### Legend
![CodeSee Map Legend](https://app.codesee.io/Legend_v2.png)
cocogitto-bot[bot]: :heavy_check_mark: 1a90a11 - Conventional commits check succeeded.
bitfehler: Sorry for the test failure, mixed up `Tag::create()` and `::create_tag()` in one branch :grimacing: 

Should be fixed now.
bitfehler: Hm, the test failure is now a failure to upload the code coverage report, so seems unrelated to my changes?
oknozor: Hey @bitfehler, thanks for the PR! This is definitely a welcome addition to cocogitto.  
I'll do a proper review during the week. I am busy right now. Sorry to keep you waiting a bit longer. 

> Hm, the test failure is now a failure to upload the code coverage report, so seems unrelated to my changes ?

Yes don't worry that something I need to fix but it won't be blocking this PR.




Patch:
diff --git a/src/bin/cog/main.rs b/src/bin/cog/main.rs
index eae1927..e11f510 100644
--- a/src/bin/cog/main.rs
+++ b/src/bin/cog/main.rs
@@ -240,6 +240,10 @@ enum Command {
         #[arg(long, value_parser = packages())]
         package: Option<String>,
 
+        /// Annotate tag with given message
+        #[arg(short = 'A', long)]
+        annotated: Option<String>,
+
         /// Dry-run: print the target version. No action taken
         #[arg(short, long)]
         dry_run: bool,
@@ -311,6 +315,7 @@ fn main() -> Result<()> {
             pre,
             hook_profile,
             package,
+            annotated,
             dry_run,
         } => {
             let mut cocogitto = CocoGitto::get()?;
@@ -336,6 +341,7 @@ fn main() -> Result<()> {
                             increment,
                             pre.as_deref(),
                             hook_profile.as_deref(),
+                            annotated,
                             dry_run,
                         )?
                     }
@@ -343,6 +349,7 @@ fn main() -> Result<()> {
                         increment,
                         pre.as_deref(),
                         hook_profile.as_deref(),
+                        annotated,
                         dry_run,
                     )?,
                 }
@@ -351,6 +358,7 @@ fn main() -> Result<()> {
                     increment,
                     pre.as_deref(),
                     hook_profile.as_deref(),
+                    annotated,
                     dry_run,
                 )?
             }
diff --git a/src/command/bump/monorepo.rs b/src/command/bump/monorepo.rs
index a24d859..1613da4 100644
--- a/src/command/bump/monorepo.rs
+++ b/src/command/bump/monorepo.rs
@@ -16,6 +16,7 @@ use colored::*;
 
 use log::info;
 use semver::Prerelease;
+use tera::Tera;
 
 use crate::conventional::error::BumpError;
 use crate::git::oid::OidOf;
@@ -41,13 +42,20 @@ impl CocoGitto {
         increment: IncrementCommand,
         pre_release: Option<&str>,
         hooks_config: Option<&str>,
+        annotated: Option<String>,
         dry_run: bool,
     ) -> Result<()> {
         match increment {
             IncrementCommand::Auto => {
-                self.create_monorepo_version_auto(pre_release, hooks_config, dry_run)
+                self.create_monorepo_version_auto(pre_release, hooks_config, annotated, dry_run)
             }
-            _ => self.create_monorepo_version_manual(increment, pre_release, hooks_config, dry_run),
+            _ => self.create_monorepo_version_manual(
+                increment,
+                pre_release,
+                hooks_config,
+                annotated,
+                dry_run,
+            ),
         }
     }
 
@@ -55,6 +63,7 @@ impl CocoGitto {
         &mut self,
         pre_release: Option<&str>,
         hooks_config: Option<&str>,
+        annotated: Option<String>,
         dry_run: bool,
     ) -> Result<()> {
         self.pre_bump_checks()?;
@@ -158,7 +167,15 @@ impl CocoGitto {
             self.repository.create_tag(&bump.new_version.prefixed_tag)?;
         }
 
-        self.repository.create_tag(&tag)?;
+        if let Some(msg_tmpl) = annotated {
+            let mut context = tera::Context::new();
+            context.insert("latest", &old.version.to_string());
+            context.insert("version", &tag.version.to_string());
+            let msg = Tera::one_off(&msg_tmpl, &context, false)?;
+            self.repository.create_annotated_tag(&tag, &msg)?;
+        } else {
+            self.repository.create_tag(&tag)?;
+        }
 
         // Run per package post hooks
         for bump in bumps {
@@ -194,6 +211,7 @@ impl CocoGitto {
         increment: IncrementCommand,
         pre_release: Option<&str>,
         hooks_config: Option<&str>,
+        annotated: Option<String>,
         dry_run: bool,
     ) -> Result<()> {
         self.pre_bump_checks()?;
@@ -266,7 +284,15 @@ impl CocoGitto {
             sign,
         )?;
 
-        self.repository.create_tag(&tag)?;
+        if let Some(msg_tmpl) = annotated {
+            let mut context = tera::Context::new();
+            context.insert("latest", &old.version.to_string());
+            context.insert("version", &tag.version.to_string());
+            let msg = Tera::one_off(&msg_tmpl, &context, false)?;
+            self.repository.create_annotated_tag(&tag, &msg)?;
+        } else {
+            self.repository.create_tag(&tag)?;
+        }
 
         // Run global post hooks
         self.run_hooks(
diff --git a/src/command/bump/package.rs b/src/command/bump/package.rs
index 79ccbc6..3b6a386 100644
--- a/src/command/bump/package.rs
+++ b/src/command/bump/package.rs
@@ -10,6 +10,7 @@ use anyhow::Result;
 use colored::*;
 use log::info;
 use semver::Prerelease;
+use tera::Tera;
 
 impl CocoGitto {
     pub fn create_package_version(
@@ -18,6 +19,7 @@ impl CocoGitto {
         increment: IncrementCommand,
         pre_release: Option<&str>,
         hooks_config: Option<&str>,
+        annotated: Option<String>,
         dry_run: bool,
     ) -> Result<()> {
         self.pre_bump_checks()?;
@@ -76,7 +78,15 @@ impl CocoGitto {
         self.repository
             .commit(&format!("chore(version): {}", tag), sign)?;
 
-        self.repository.create_tag(&tag)?;
+        if let Some(msg_tmpl) = annotated {
+            let mut context = tera::Context::new();
+            context.insert("latest", &current_tag.version.to_string());
+            context.insert("version", &tag.version.to_string());
+            let msg = Tera::one_off(&msg_tmpl, &context, false)?;
+            self.repository.create_annotated_tag(&tag, &msg)?;
+        } else {
+            self.repository.create_tag(&tag)?;
+        }
 
         self.run_hooks(
             HookType::PostBump,
diff --git a/src/command/bump/standard.rs b/src/command/bump/standard.rs
index 9039aa7..ad18d9f 100644
--- a/src/command/bump/standard.rs
+++ b/src/command/bump/standard.rs
@@ -10,6 +10,7 @@ use anyhow::Result;
 use colored::*;
 use log::info;
 use semver::Prerelease;
+use tera::Tera;
 
 impl CocoGitto {
     pub fn create_version(
@@ -17,6 +18,7 @@ impl CocoGitto {
         increment: IncrementCommand,
         pre_release: Option<&str>,
         hooks_config: Option<&str>,
+        annotated: Option<String>,
         dry_run: bool,
     ) -> Result<()> {
         self.pre_bump_checks()?;
@@ -70,7 +72,15 @@ impl CocoGitto {
             sign,
         )?;
 
-        self.repository.create_tag(&tag)?;
+        if let Some(msg_tmpl) = annotated {
+            let mut context = tera::Context::new();
+            context.insert("latest", &current_tag.version.to_string());
+            context.insert("version", &tag.version.to_string());
+            let msg = Tera::one_off(&msg_tmpl, &context, false)?;
+            self.repository.create_annotated_tag(&tag, &msg)?;
+        } else {
+            self.repository.create_tag(&tag)?;
+        }
 
         self.run_hooks(
             HookType::PostBump,
diff --git a/src/git/tag.rs b/src/git/tag.rs
index aceee0d..fe11f3d 100644
--- a/src/git/tag.rs
+++ b/src/git/tag.rs
@@ -38,6 +38,20 @@ impl Repository {
             .map_err(Git2Error::from)
     }
 
+    pub(crate) fn create_annotated_tag(&self, tag: &Tag, msg: &str) -> Result<(), Git2Error> {
+        if self.get_diff(true).is_some() {
+            let statuses = self.get_statuses()?;
+            return Err(Git2Error::ChangesNeedToBeCommitted(statuses));
+        }
+
+        let head = self.get_head_commit().unwrap();
+        let sig = self.0.signature()?;
+        self.0
+            .tag(&tag.to_string(), &head.into_object(), &sig, msg, false)
+            .map(|_| ())
+            .map_err(Git2Error::from)
+    }
+
     /// Get the latest tag, will ignore package tag if on a monorepo
     pub(crate) fn get_latest_tag(&self) -> Result<Tag, TagError> {
         let tags: Vec<Tag> = self.all_tags()?;


Test patch:
diff --git a/tests/helpers.rs b/tests/helpers.rs
index b76a46a..52446f4 100644
--- a/tests/helpers.rs
+++ b/tests/helpers.rs
@@ -123,6 +123,13 @@ pub fn assert_latest_tag(tag: &str) -> Result<()> {
     Ok(())
 }
 
+pub fn assert_tag_is_annotated(tag: &str) -> Result<()> {
+    let objtype = run_fun!(git for-each-ref --format="%(objecttype)" refs/tags/$tag)?;
+    let objtype: Vec<&str> = objtype.split('\n').collect();
+    assert_that!(objtype.first()).is_some().is_equal_to(&"tag");
+    Ok(())
+}
+
 /// Git log showing only the HEAD commit, this can be used to make assertion on the last commit
 pub fn git_log_head() -> Result<String> {
     run_fun!(git log -1 --pretty=%B).map_err(|e| anyhow!(e))
diff --git a/tests/lib_tests/bump.rs b/tests/lib_tests/bump.rs
index 8ea55d6..88fd66e 100644
--- a/tests/lib_tests/bump.rs
+++ b/tests/lib_tests/bump.rs
@@ -19,7 +19,7 @@ fn bump_ok() -> Result<()> {
     let mut cocogitto = CocoGitto::get()?;
 
     // Act
-    let result = cocogitto.create_version(IncrementCommand::Auto, None, None, false);
+    let result = cocogitto.create_version(IncrementCommand::Auto, None, None, None, false);
 
     // Assert
     assert_that!(result).is_ok();
@@ -27,6 +27,33 @@ fn bump_ok() -> Result<()> {
     Ok(())
 }
 
+#[sealed_test]
+fn annotated_bump_ok() -> Result<()> {
+    // Arrange
+    git_init()?;
+    git_commit("chore: first commit")?;
+    git_commit("feat: add a feature commit")?;
+    git_tag("1.0.0")?;
+    git_commit("feat: add another feature commit")?;
+
+    let mut cocogitto = CocoGitto::get()?;
+
+    // Act
+    let result = cocogitto.create_version(
+        IncrementCommand::Auto,
+        None,
+        None,
+        Some(String::from("Release version {{version}}")),
+        false,
+    );
+
+    // Assert
+    assert_that!(result).is_ok();
+    assert_latest_tag("1.1.0")?;
+    assert_tag_is_annotated("1.1.0")?;
+    Ok(())
+}
+
 #[sealed_test]
 fn monorepo_bump_ok() -> Result<()> {
     // Arrange
@@ -39,7 +66,7 @@ fn monorepo_bump_ok() -> Result<()> {
     let mut cocogitto = CocoGitto::get()?;
 
     // Act
-    let result = cocogitto.create_monorepo_version(IncrementCommand::Auto, None, None, false);
+    let result = cocogitto.create_monorepo_version(IncrementCommand::Auto, None, None, None, false);
 
     // Assert
     assert_that!(result).is_ok();
@@ -63,7 +90,8 @@ fn monorepo_bump_manual_ok() -> Result<()> {
     let mut cocogitto = CocoGitto::get()?;
 
     // Act
-    let result = cocogitto.create_monorepo_version(IncrementCommand::Major, None, None, false);
+    let result =
+        cocogitto.create_monorepo_version(IncrementCommand::Major, None, None, None, false);
 
     // Assert
     assert_that!(result).is_ok();
@@ -84,7 +112,7 @@ fn monorepo_with_tag_prefix_bump_ok() -> Result<()> {
     let mut cocogitto = CocoGitto::get()?;
 
     // Act
-    let result = cocogitto.create_monorepo_version(IncrementCommand::Auto, None, None, false);
+    let result = cocogitto.create_monorepo_version(IncrementCommand::Auto, None, None, None, false);
 
     // Assert
     assert_that!(result).is_ok();
@@ -110,6 +138,7 @@ fn package_bump_ok() -> Result<()> {
         IncrementCommand::AutoPackage("one".to_string()),
         None,
         None,
+        None,
         false,
     );
 
@@ -130,7 +159,7 @@ fn should_fallback_to_0_0_0_when_there_is_no_tag() -> Result<()> {
     let mut cocogitto = CocoGitto::get()?;
 
     // Act
-    let result = cocogitto.create_version(IncrementCommand::Auto, None, None, false);
+    let result = cocogitto.create_version(IncrementCommand::Auto, None, None, None, false);
 
     // Assert
     assert_that!(result).is_ok();
@@ -183,7 +212,7 @@ fn bump_with_whitelisted_branch_ok() -> Result<()> {
     let mut cocogitto = CocoGitto::get()?;
 
     // Act
-    let result = cocogitto.create_version(IncrementCommand::Auto, None, None, false);
+    let result = cocogitto.create_version(IncrementCommand::Auto, None, None, None, false);
 
     // Assert
     assert_that!(result).is_ok();
@@ -208,7 +237,7 @@ fn bump_with_whitelisted_branch_fails() -> Result<()> {
     let mut cocogitto = CocoGitto::get()?;
 
     // Act
-    let result = cocogitto.create_version(IncrementCommand::Auto, None, None, false);
+    let result = cocogitto.create_version(IncrementCommand::Auto, None, None, None, false);
 
     // Assert
     assert_that!(result.unwrap_err().to_string()).is_equal_to(
@@ -237,7 +266,7 @@ fn bump_with_whitelisted_branch_pattern_ok() -> Result<()> {
     let mut cocogitto = CocoGitto::get()?;
 
     // Act
-    let result = cocogitto.create_version(IncrementCommand::Auto, None, None, false);
+    let result = cocogitto.create_version(IncrementCommand::Auto, None, None, None, false);
 
     // Assert
     assert_that!(result).is_ok();
@@ -262,7 +291,7 @@ fn bump_with_whitelisted_branch_pattern_err() -> Result<()> {
     let mut cocogitto = CocoGitto::get()?;
 
     // Act
-    let result = cocogitto.create_version(IncrementCommand::Auto, None, None, false);
+    let result = cocogitto.create_version(IncrementCommand::Auto, None, None, None, false);
 
     // Assert
     assert_that!(result).is_err();