Repository: cocogitto/cocogitto
Pull request #274 (2023-03-24 14:43:27)

feat(bump): option to append skip-ci pattern for bump commit

codecov[bot]: ## [Codecov](https://codecov.io/gh/cocogitto/cocogitto/pull/274?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cocogitto) Report
> Merging [#274](https://codecov.io/gh/cocogitto/cocogitto/pull/274?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cocogitto) (b47a368) into [main](https://codecov.io/gh/cocogitto/cocogitto/commit/af08a7e86c714fcdd0977d283d4887f3e98b6aa2?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cocogitto) (af08a7e) will **decrease** coverage by `0.02%`.
> The diff coverage is `82.97%`.

```diff
@@            Coverage Diff             @@
##             main     #274      +/-   ##
==========================================
- Coverage   85.68%   85.67%   -0.02%     
==========================================
  Files          43       43              
  Lines        5918     5955      +37     
==========================================
+ Hits         5071     5102      +31     
- Misses        847      853       +6     
```


| [Impacted Files](https://codecov.io/gh/cocogitto/cocogitto/pull/274?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cocogitto) | Coverage Δ | |
|---|---|---|
| [src/bin/cog/main.rs](https://codecov.io/gh/cocogitto/cocogitto/pull/274?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cocogitto#diff-c3JjL2Jpbi9jb2cvbWFpbi5ycw==) | `46.82% <66.66%> (+0.56%)` | :arrow_up: |
| [src/command/bump/monorepo.rs](https://codecov.io/gh/cocogitto/cocogitto/pull/274?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cocogitto#diff-c3JjL2NvbW1hbmQvYnVtcC9tb25vcmVwby5ycw==) | `92.24% <81.25%> (-0.86%)` | :arrow_down: |
| [src/command/bump/package.rs](https://codecov.io/gh/cocogitto/cocogitto/pull/274?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cocogitto#diff-c3JjL2NvbW1hbmQvYnVtcC9wYWNrYWdlLnJz) | `91.42% <100.00%> (+0.25%)` | :arrow_up: |
| [src/command/bump/standard.rs](https://codecov.io/gh/cocogitto/cocogitto/pull/274?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cocogitto#diff-c3JjL2NvbW1hbmQvYnVtcC9zdGFuZGFyZC5ycw==) | `100.00% <100.00%> (ø)` | |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cocogitto)

cocogitto-bot[bot]: :heavy_check_mark: 4cce641 - Conventional commits check succeeded.
cocogitto-bot[bot]: :heavy_check_mark: 529a866...0c8101f - Conventional commits check succeeded.
cocogitto-bot[bot]: :heavy_check_mark: 529a866...c04aa81 - Conventional commits check succeeded.
cocogitto-bot[bot]: :heavy_check_mark: d1c600e - Conventional commits check succeeded.
cocogitto-bot[bot]: :x: Found 1 compliant commit and 1 non-compliant commits in d1c600ebc50a8cae2f43d6fa41fe04953e89ae15.

Commit 6c86f291bb272e64c03b1e9b4379e5111831465c by @oknozor  is not conform to the conventional commit specification :
- **message:** `Merge branch 'main' into feat/skip-ci`
- **cause:**
    ```
    Missing commit type separator `:`
	
	Caused by:
	     --> 1:6
	      |
	    1 | Merge branch 'main' into feat/skip-ci
	      |      ^---
	      |
	      = expected scope or type_separator
    ```


Wassim-AB: Hey @oknozor, I think your merged main into my branch by mistake rather than rebasing it :) 
cocogitto-bot[bot]: :heavy_check_mark: d1c600e...f06f605 - Conventional commits check succeeded.
cocogitto-bot[bot]: :heavy_check_mark: 042dca5 - Conventional commits check succeeded.
Wassim-AB: @oknozor Done ;)
Wassim-AB: @oknozor Do you have any idea about the error ?
oknozor: Some time coverage upload fails, this should not make the CI fail. don't worry though this is not blocking. 
cocogitto-bot[bot]: :heavy_check_mark: 0d0c2e3 - Conventional commits check succeeded.
oknozor: Hey @Wassim-AB I think we should also be able to override `cog.toml` settings using the cli flag `--skip-ci` as mentioned in #226.
If clippy is not happy you can add an exception using the `#[allow(..)]` macro for this feature. 
Wassim-AB: Hey @oknozor, no problem, will be working on it today ;)
cocogitto-bot[bot]: :x: Found 2 compliant commit and 1 non-compliant commits in aa0cb051301b937cd9606317612e44f64320afe8.

Commit 0a923ec7ed5806bb81cddb34ae7dc9c60310b325 by @Wassim-AB  is not conform to the conventional commit specification :
- **message:** `Merge branch 'cocogitto:main' into main`
- **cause:**
    ```
    Missing commit type separator `:`
	
	Caused by:
	     --> 1:6
	      |
	    1 | Merge branch 'cocogitto:main' into main
	      |      ^---
	      |
	      = expected scope or type_separator
    ```


cocogitto-bot[bot]: :heavy_check_mark: fea0ece - Conventional commits check succeeded.
Wassim-AB: Hey @oknozor, I'm done, I've also renamed the option "skip_ci" instead of "skip_ci_pattern" within the cog.toml, let me know if you're ok with it :)

Patch:
diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
index 37ef7e1..9ad643a 100644
--- a/.github/workflows/CI.yml
+++ b/.github/workflows/CI.yml
@@ -24,7 +24,7 @@ jobs:
           git config --global user.name github-actions
 
       - name: Conventional commit check
-        uses: cocogitto/cocogitto-action@v3
+        uses: cocogitto/cocogitto-action@v3.5
         with:
           git-user: 'github-actions'
           git-user-email: 'github-actions@github.com'
diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml
index aa9563c..66db949 100644
--- a/.github/workflows/Release.yml
+++ b/.github/workflows/Release.yml
@@ -32,7 +32,7 @@ jobs:
 
       - name: SemVer release
         id: release
-        uses: cocogitto/cocogitto-action@v3
+        uses: cocogitto/cocogitto-action@v3.5
         with:
           check-latest-tag-only: true
           release: true
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e69c137..99def35 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -37,6 +37,10 @@ In addition, our CI pipeline uses the following formatter and linter:
 
   Use `cargo clippy` instead of `cargo build` to spot lints before committing your changes.
 
+- **git-hooks**
+  Cocogitto provides sharable git-hooks, if you want to save the hassle of manually running lints and formatters before
+  each commit you can simply run `cog install-hook --all`.
+
 ### Error handling
 * `unwrap()` usage should be avoided, if needed use `expect("why")` instead.
 * For convenience, errors in the [cog](src/bin/cog.rs) CLI and the public API are wrapped with the 
diff --git a/README.md b/README.md
index 58ac998..15f7024 100644
--- a/README.md
+++ b/README.md
@@ -144,7 +144,7 @@ cog commit feat "add awesome feature"
 git commit -m "feat: add awesome feature"
 ```
 
-See [User guide -> Conventional commits](https://docs.cocogitto.io/coco_guide/#conventional-commits).
+See [User guide -> Conventional commits](https://docs.cocogitto.io/guide/#conventional-commits).
 
 ## Auto-bumps
 
@@ -222,7 +222,7 @@ Output :
 - add integration test - (bae629c) - *oknozor*
 ```
 
-See [User guide -> Changelogs](https://docs.cocogitto.io/cog_guide/#changelogs).
+See [User guide -> Changelogs](https://docs.cocogitto.io/guide/#changelogs).
 
 ## GitHub integration
 
diff --git a/cog.toml b/cog.toml
index 6b0b779..39ce4f5 100644
--- a/cog.toml
+++ b/cog.toml
@@ -1,4 +1,7 @@
+# A list of glob patterns describing branches on which semver bump are allowed
 branch_whitelist = ["main"]
+
+# Ignore merge commits from conventional commit checks
 ignore_merge_commits = true
 
 # A list of command to run BEFORE creating a version.
@@ -31,6 +34,25 @@ post_bump_hooks = [
     "echo 'we are done'"
 ]
 
+# Shareable git-hooks, can be installed using `cog install-hook --all`
+# or alternatively `cog install-hooks {commit_hook_type}
+[git_hooks.commit-msg]
+script = """#!/bin/sh
+set -e
+
+echo "[Check current commit message]"
+cog verify --file $1
+
+echo "[Conventional commit check]"
+cog check
+
+echo "[Format code]"
+cargo fmt -v --all --check
+
+echo "[Lints]"
+cargo clippy
+"""
+
 # An optional list of additional allowed commit type
 # `cog commit {commit_type}` commit command will be generated at runtime
 [commit_types]
@@ -42,6 +64,7 @@ template = "remote"
 remote = "github.com"
 repository = "cocogitto"
 owner = "cocogitto"
+
 # Optional contributor list
 # intended to map git signature to remote username
 # and generate changelog links to their remote profiles
@@ -63,5 +86,6 @@ authors = [
     { signature = "Luca Trevisani", username = "lucatrv" },
     { signature = "Racci", username = "DaRacci" },
     { signature = "Conrad Hoffmann", username = "bitfehler" },
-    { signature = "andre161292", username = "andre161292" }
+    { signature = "andre161292", username = "andre161292" },
+    { signature = "Sanchith Hegde", username = "SanchithHegde" }
 ]
diff --git a/src/bin/cog/main.rs b/src/bin/cog/main.rs
index f919ead..4d2f541 100644
--- a/src/bin/cog/main.rs
+++ b/src/bin/cog/main.rs
@@ -3,6 +3,7 @@ mod mangen;
 
 use std::fs;
 use std::path::PathBuf;
+use std::str::FromStr;
 
 use cocogitto::conventional::changelog::template::{RemoteContext, Template};
 use cocogitto::conventional::commit as conv_commit;
@@ -115,7 +116,7 @@ impl ValueEnum for Shell {
 #[derive(Parser)]
 #[command(
     version,
-    name = "Cog",
+    name = "cog",
     author = "Paul D. <paul.delafosse@protonmail.com>"
 )]
 struct Cli {
@@ -280,6 +281,10 @@ enum Command {
         /// Dry-run: print the target version. No action taken
         #[arg(short, long)]
         dry_run: bool,
+
+        /// Specify a "Skip CI" string to append to the end of the commit
+        #[arg(long = "skip-ci")]
+        skip_ci: Option<String>,
     },
 
     /// Install cog config files
@@ -357,6 +362,7 @@ fn main() -> Result<()> {
             package,
             annotated,
             dry_run,
+            skip_ci,
         } => {
             let mut cocogitto = CocoGitto::get()?;
             let is_monorepo = !SETTINGS.packages.is_empty();
@@ -391,6 +397,7 @@ fn main() -> Result<()> {
                             hook_profile.as_deref(),
                             annotated,
                             dry_run,
+                            skip_ci,
                         )?
                     }
                     None => cocogitto.create_monorepo_version(
@@ -399,6 +406,7 @@ fn main() -> Result<()> {
                         hook_profile.as_deref(),
                         annotated,
                         dry_run,
+                        skip_ci,
                     )?,
                 }
             } else {
@@ -408,6 +416,7 @@ fn main() -> Result<()> {
                     hook_profile.as_deref(),
                     annotated,
                     dry_run,
+                    skip_ci,
                 )?
             }
         }
@@ -523,7 +532,10 @@ fn main() -> Result<()> {
                 Template::default()
             };
 
-            let pattern = pattern.as_deref().map(RevspecPattern::from);
+            let pattern = pattern
+                .as_deref()
+                .map(RevspecPattern::from_str)
+                .transpose()?;
 
             let result = match at {
                 Some(at) => cocogitto.get_changelog_at_tag(&at, template)?,
diff --git a/src/command/bump/monorepo.rs b/src/command/bump/monorepo.rs
index 4ca58c8..90e5ae9 100644
--- a/src/command/bump/monorepo.rs
+++ b/src/command/bump/monorepo.rs
@@ -45,16 +45,28 @@ impl CocoGitto {
         hooks_config: Option<&str>,
         annotated: Option<String>,
         dry_run: bool,
+        skip_ci: Option<String>,
     ) -> Result<()> {
         match increment {
             IncrementCommand::Auto => {
                 if SETTINGS.generate_mono_repository_global_tag {
-                    self.create_monorepo_version_auto(pre_release, hooks_config, annotated, dry_run)
+                    self.create_monorepo_version_auto(
+                        pre_release,
+                        hooks_config,
+                        annotated,
+                        dry_run,
+                        skip_ci,
+                    )
                 } else {
                     if annotated.is_some() {
                         warn!("--annotated flag is not supported for package bumps without a global tag");
                     }
-                    self.create_all_package_version_auto(pre_release, hooks_config, dry_run)
+                    self.create_all_package_version_auto(
+                        pre_release,
+                        hooks_config,
+                        dry_run,
+                        skip_ci,
+                    )
                 }
             }
             _ => self.create_monorepo_version_manual(
@@ -63,6 +75,7 @@ impl CocoGitto {
                 hooks_config,
                 annotated,
                 dry_run,
+                skip_ci,
             ),
         }
     }
@@ -72,6 +85,7 @@ impl CocoGitto {
         pre_release: Option<&str>,
         hooks_config: Option<&str>,
         dry_run: bool,
+        skip_ci: Option<String>,
     ) -> Result<()> {
         self.pre_bump_checks()?;
         // Get package bumps
@@ -96,8 +110,13 @@ impl CocoGitto {
         self.bump_packages(pre_release, hooks_config, &bumps)?;
 
         let sign = self.repository.gpg_sign();
-        self.repository
-            .commit("chore(version): bump packages", sign)?;
+
+        let skip_ci_pattern = skip_ci.unwrap_or(SETTINGS.skip_ci.clone().unwrap_or_default());
+
+        self.repository.commit(
+            &format!("chore(version): bump packages {}", skip_ci_pattern),
+            sign,
+        )?;
 
         for bump in &bumps {
             self.repository.create_tag(&bump.new_version.prefixed_tag)?;
@@ -131,6 +150,7 @@ impl CocoGitto {
         hooks_config: Option<&str>,
         annotated: Option<String>,
         dry_run: bool,
+        skip_ci: Option<String>,
     ) -> Result<()> {
         self.pre_bump_checks()?;
         // Get package bumps
@@ -227,8 +247,14 @@ impl CocoGitto {
         self.bump_packages(pre_release, hooks_config, &bumps)?;
 
         let sign = self.repository.gpg_sign();
+
+        let skip_ci_pattern = skip_ci.unwrap_or(SETTINGS.skip_ci.clone().unwrap_or_default());
+
         self.repository.commit(
-            &format!("chore(version): {}", next_version.prefixed_tag),
+            &format!(
+                "chore(version): {} {}",
+                next_version.prefixed_tag, skip_ci_pattern
+            ),
             sign,
         )?;
 
@@ -279,6 +305,7 @@ impl CocoGitto {
         hooks_config: Option<&str>,
         annotated: Option<String>,
         dry_run: bool,
+        skip_ci: Option<String>,
     ) -> Result<()> {
         self.pre_bump_checks()?;
         // Get package bumps
@@ -343,8 +370,13 @@ impl CocoGitto {
         self.unwrap_or_stash_and_exit(&tag, hook_result);
 
         let sign = self.repository.gpg_sign();
+        let skip_ci_pattern = skip_ci.unwrap_or(SETTINGS.skip_ci.clone().unwrap_or_default());
+
         self.repository.commit(
-            &format!("chore(version): {}", next_version.prefixed_tag),
+            &format!(
+                "chore(version): {} {}",
+                next_version.prefixed_tag, skip_ci_pattern
+            ),
             sign,
         )?;
 
diff --git a/src/command/bump/package.rs b/src/command/bump/package.rs
index b204907..6791051 100644
--- a/src/command/bump/package.rs
+++ b/src/command/bump/package.rs
@@ -15,6 +15,7 @@ use semver::Prerelease;
 use tera::Tera;
 
 impl CocoGitto {
+    #[allow(clippy::too_many_arguments)]
     pub fn create_package_version(
         &mut self,
         (package_name, package): (&str, &MonoRepoPackage),
@@ -23,6 +24,7 @@ impl CocoGitto {
         hooks_config: Option<&str>,
         annotated: Option<String>,
         dry_run: bool,
+        skip_ci: Option<String>,
     ) -> Result<()> {
         self.pre_bump_checks()?;
 
@@ -82,8 +84,11 @@ impl CocoGitto {
         self.unwrap_or_stash_and_exit(&tag, hook_result);
 
         let sign = self.repository.gpg_sign();
+
+        let skip_ci_pattern = skip_ci.unwrap_or(SETTINGS.skip_ci.clone().unwrap_or_default());
+
         self.repository
-            .commit(&format!("chore(version): {tag}"), sign)?;
+            .commit(&format!("chore(version): {tag} {}", skip_ci_pattern), sign)?;
 
         if let Some(msg_tmpl) = annotated {
             let mut context = tera::Context::new();
diff --git a/src/command/bump/standard.rs b/src/command/bump/standard.rs
index 6427c59..4e8d17f 100644
--- a/src/command/bump/standard.rs
+++ b/src/command/bump/standard.rs
@@ -21,6 +21,7 @@ impl CocoGitto {
         hooks_config: Option<&str>,
         annotated: Option<String>,
         dry_run: bool,
+        skip_ci: Option<String>,
     ) -> Result<()> {
         self.pre_bump_checks()?;
 
@@ -70,8 +71,13 @@ impl CocoGitto {
 
         let sign = self.repository.gpg_sign();
 
+        let skip_ci_pattern = skip_ci.unwrap_or(SETTINGS.skip_ci.clone().unwrap_or_default());
+
         self.repository.commit(
-            &format!("chore(version): {}", next_version.prefixed_tag),
+            &format!(
+                "chore(version): {} {}",
+                next_version.prefixed_tag, skip_ci_pattern
+            ),
             sign,
         )?;
 
diff --git a/src/command/changelog.rs b/src/command/changelog.rs
index a72fb11..21679c0 100644
--- a/src/command/changelog.rs
+++ b/src/command/changelog.rs
@@ -4,6 +4,7 @@ use crate::git::revspec::RevspecPattern;
 use crate::CocoGitto;
 use anyhow::anyhow;
 use anyhow::Result;
+use std::str::FromStr;
 
 impl CocoGitto {
     /// ## Get a changelog between two oids
@@ -27,7 +28,7 @@ impl CocoGitto {
 
     pub fn get_changelog_at_tag(&self, tag: &str, template: Template) -> Result<String> {
         let pattern = format!("..{tag}");
-        let pattern = RevspecPattern::from(pattern.as_str());
+        let pattern = RevspecPattern::from_str(pattern.as_str())?;
         let changelog = self.get_changelog(pattern, false)?;
 
         changelog
diff --git a/src/command/check.rs b/src/command/check.rs
index 9fa30d5..a293730 100644
--- a/src/command/check.rs
+++ b/src/command/check.rs
@@ -6,6 +6,7 @@ use anyhow::anyhow;
 use anyhow::Result;
 use colored::*;
 use log::info;
+use std::str::FromStr;
 
 impl CocoGitto {
     pub fn check(
@@ -16,7 +17,7 @@ impl CocoGitto {
     ) -> Result<()> {
         let commit_range = if let Some(range) = range {
             self.repository
-                .get_commit_range(&RevspecPattern::from(range.as_str()))?
+                .get_commit_range(&RevspecPattern::from_str(range.as_str())?)?
         } else if check_from_latest_tag {
             self.repository
                 .get_commit_range(&RevspecPattern::default())?
diff --git a/src/conventional/bump.rs b/src/conventional/bump.rs
index 1808c82..504d901 100644
--- a/src/conventional/bump.rs
+++ b/src/conventional/bump.rs
@@ -5,6 +5,7 @@ use conventional_commit_parser::commit::CommitType;
 use git2::Commit as Git2Commit;
 use once_cell::sync::Lazy;
 use semver::{BuildMetadata, Prerelease, Version};
+use std::str::FromStr;
 
 static FILTER_MERGE_COMMITS: Lazy<fn(&&git2::Commit) -> bool> = Lazy::new(|| {
     |commit| {
@@ -146,7 +147,7 @@ impl Tag {
             .map(|oid| format!("{oid}.."))
             .unwrap_or_else(|| "..".to_string());
         let pattern = pattern.as_str();
-        let pattern = RevspecPattern::from(pattern);
+        let pattern = RevspecPattern::from_str(pattern)?;
         let commits = repository.get_commit_range(&pattern)?;
 
         let commits: Vec<&Git2Commit> = commits
@@ -189,7 +190,7 @@ impl Tag {
             .map(|oid| format!("{oid}.."))
             .unwrap_or_else(|| "..".to_string());
         let pattern = pattern.as_str();
-        let pattern = RevspecPattern::from(pattern);
+        let pattern = RevspecPattern::from_str(pattern)?;
         let commits = repository.get_commit_range_for_package(&pattern, package)?;
         let commits: Vec<&Git2Commit> = commits
             .commits
@@ -229,7 +230,7 @@ impl Tag {
             .map(|oid| format!("{oid}.."))
             .unwrap_or_else(|| "..".to_string());
         let pattern = pattern.as_str();
-        let pattern = RevspecPattern::from(pattern);
+        let pattern = RevspecPattern::from_str(pattern)?;
         let commits = repository.get_commit_range_for_monorepo_global(&pattern)?;
 
         let commits: Vec<&Git2Commit> = commits
diff --git a/src/git/error.rs b/src/git/error.rs
index 6e871ff..c47b6e6 100644
--- a/src/git/error.rs
+++ b/src/git/error.rs
@@ -26,6 +26,7 @@ pub enum Git2Error {
     CommitterNotFound,
     TagError(TagError),
     GitHookNonZeroExit(i32),
+    InvalidCommitRangePattern(String),
 }
 
 #[derive(Debug)]
@@ -122,6 +123,9 @@ impl Display for Git2Error {
             Git2Error::GitHookNonZeroExit(status) => {
                 writeln!(f, "commit hook failed with exit code {status}")
             }
+            Git2Error::InvalidCommitRangePattern(pattern) => {
+                writeln!(f, "invalid commit range pattern: `{pattern}`")
+            }
         }?;
 
         match self {
diff --git a/src/git/monorepo.rs b/src/git/monorepo.rs
index 0180a97..804c448 100644
--- a/src/git/monorepo.rs
+++ b/src/git/monorepo.rs
@@ -27,6 +27,7 @@ mod test {
     use indoc::formatdoc;
     use sealed_test::prelude::*;
     use speculoos::prelude::*;
+    use std::str::FromStr;
 
     #[sealed_test]
     fn get_repo_packages() -> Result<()> {
@@ -67,7 +68,8 @@ mod test {
         )?;
 
         // Act
-        let range = repo.get_commit_range_for_package(&RevspecPattern::from("..HEAD"), "two")?;
+        let range =
+            repo.get_commit_range_for_package(&RevspecPattern::from_str("..HEAD")?, "two")?;
 
         // Assert
         assert_that!(range)
diff --git a/src/git/revspec.rs b/src/git/revspec.rs
index 92b6eb9..88f3616 100644
--- a/src/git/revspec.rs
+++ b/src/git/revspec.rs
@@ -1,7 +1,8 @@
 use std::fmt;
 use std::fmt::Formatter;
+use std::str::FromStr;
 
-use git2::{Commit, ErrorCode, Oid};
+use git2::{Commit, DescribeFormatOptions, DescribeOptions, ErrorCode, Oid};
 
 use crate::conventional::changelog::release::Release;
 use crate::git::error::Git2Error;
@@ -31,27 +32,27 @@ impl fmt::Display for RevspecPattern {
     }
 }
 
-impl From<&str> for RevspecPattern {
-    fn from(value: &str) -> Self {
-        if !value.contains("..") {
-            panic!("Invalid commit range pattern: '{value}'");
-        }
+impl FromStr for RevspecPattern {
+    type Err = Git2Error;
 
-        let split = value.split("..").collect::<Vec<&str>>();
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if let Some((from, to)) = s.split_once("..") {
+            let from = if from.is_empty() {
+                None
+            } else {
+                Some(from.to_string())
+            };
 
-        let from = if split[0].is_empty() {
-            None
-        } else {
-            Some(split[0].to_string())
-        };
+            let to = if to.is_empty() {
+                None
+            } else {
+                Some(to.to_string())
+            };
 
-        let to = if split[1].is_empty() {
-            None
+            Ok(RevspecPattern { from, to })
         } else {
-            Some(split[1].to_string())
-        };
-
-        RevspecPattern { from, to }
+            Err(Git2Error::InvalidCommitRangePattern(s.to_string()))
+        }
     }
 }
 
@@ -131,7 +132,7 @@ impl Repository {
         target: &Oid,
     ) -> Result<Release<'a>, Git2Error> {
         let pattern = format!("..{}", release.from);
-        let pattern = RevspecPattern::from(pattern.as_str());
+        let pattern = RevspecPattern::from_str(pattern.as_str())?;
         let range = self.get_commit_range(&pattern)?;
 
         let target_in_range = range.commits.iter().any(|commit| commit.id() == *target);
@@ -346,46 +347,12 @@ impl Repository {
     fn get_latest_tag_starting_from(&self, starting_point: Oid) -> Result<Tag, Git2Error> {
         let starting_point = self.0.find_commit(starting_point)?;
         let starting_point = starting_point.parent(0)?;
-        let first_commit = self.get_first_commit()?;
-        let mut revwalk = self.0.revwalk()?;
-        let range = format!("{}..{}", first_commit, starting_point.id());
-
-        revwalk.push_range(&range)?;
-        let mut range = vec![];
-        for oid in revwalk {
-            range.push(oid?);
-        }
-
-        let mut tags = vec![];
-        self.0
-            .tag_foreach(|mut oid, name| {
-                let name = String::from_utf8_lossy(name);
-                let name = name.as_ref().strip_prefix("refs/tags/").unwrap();
-
-                // If this is an annotated tag, find the first parent commit
-                if self.0.revparse_single(name).unwrap().as_commit().is_none() {
-                    if let Some(commit) = self
-                        .0
-                        .revparse_single([name, "^{}"].concat().as_str())
-                        .unwrap()
-                        .as_commit()
-                    {
-                        oid = commit.id();
-                    }
-                };
-
-                if range.contains(&oid) {
-                    if let Ok(tag) = Tag::from_str(name, Some(oid)) {
-                        tags.push(tag);
-                    };
-                };
-                true
-            })
-            .expect("Unable to walk tags");
-
-        let latest_tag: Option<Tag> = tags.into_iter().max();
-
-        latest_tag.ok_or(Git2Error::NoTagFound)
+        let describe = starting_point
+            .as_object()
+            .describe(DescribeOptions::new().describe_tags())?;
+        let tag_name = describe.format(Some(DescribeFormatOptions::new().abbreviated_size(0)))?;
+        let tag_oid = self.0.refname_to_id(&format!("refs/tags/{tag_name}")).ok();
+        Tag::from_str(&tag_name, tag_oid).map_err(|_| Git2Error::NoTagFound)
     }
 }
 
@@ -399,6 +366,7 @@ mod test {
     use speculoos::prelude::*;
     use std::collections::HashMap;
     use std::path::PathBuf;
+    use std::str::FromStr;
 
     use crate::git::oid::OidOf;
     use crate::git::repository::Repository;
@@ -409,42 +377,49 @@ mod test {
     const COCOGITTO_REPOSITORY: &str = env!("CARGO_MANIFEST_DIR");
 
     #[test]
-    fn convert_str_to_pattern_to() {
-        let pattern = RevspecPattern::from("..1.0.0");
+    fn convert_str_to_pattern_to() -> Result<()> {
+        let pattern = RevspecPattern::from_str("..1.0.0")?;
 
         assert_that!(pattern.from).is_none();
         assert_that!(pattern.to)
             .is_some()
             .is_equal_to("1.0.0".to_string());
+        Ok(())
     }
 
     #[test]
-    fn convert_str_to_pattern_from() {
-        let pattern = RevspecPattern::from("1.0.0..");
+    fn convert_str_to_pattern_from() -> Result<()> {
+        let pattern = RevspecPattern::from_str("1.0.0..")?;
 
         assert_that!(pattern.from)
             .is_some()
             .is_equal_to("1.0.0".to_string());
-        assert_that!(pattern.to).is_none()
+        assert_that!(pattern.to).is_none();
+        Ok(())
     }
 
     #[test]
-    fn convert_empty_pattern() {
-        let pattern = RevspecPattern::from("..");
+    fn convert_empty_pattern() -> Result<()> {
+        let pattern = RevspecPattern::from_str("..")?;
 
         assert_that!(pattern.from).is_none();
-        assert_that!(pattern.to).is_none()
+        assert_that!(pattern.to).is_none();
+        Ok(())
     }
 
     #[test]
-    #[should_panic(expected = "Invalid commit range pattern: '1.0.0'")]
-    fn panic_invalid_pattern() {
-        let _ = RevspecPattern::from("1.0.0");
+    fn error_invalid_pattern() {
+        let result = RevspecPattern::from_str("1.0.0");
+        assert!(result.is_err());
+        assert_eq!(
+            result.unwrap_err().to_string(),
+            "invalid commit range pattern: `1.0.0`\n"
+        );
     }
 
     #[test]
-    fn convert_full_pattern() {
-        let pattern = RevspecPattern::from("1.0.0..2.0.0");
+    fn convert_full_pattern() -> Result<()> {
+        let pattern = RevspecPattern::from_str("1.0.0..2.0.0")?;
 
         assert_that!(pattern.from)
             .is_some()
@@ -452,6 +427,7 @@ mod test {
         assert_that!(pattern.to)
             .is_some()
             .is_equal_to("2.0.0".to_string());
+        Ok(())
     }
 
     #[test]
@@ -474,7 +450,7 @@ mod test {
         let format_version = |release: &Release| format!("{}", release.version);
 
         // Act
-        let release = repo.get_release_range(RevspecPattern::from("0.32.1..0.32.3"))?;
+        let release = repo.get_release_range(RevspecPattern::from_str("0.32.1..0.32.3")?)?;
 
         // Assert
         assert_that!(format_version(&release)).is_equal_to("0.32.3".to_string());
@@ -520,7 +496,7 @@ mod test {
 
         repo.commit("feat: a commit", false)?;
 
-        let commit_range = repo.get_commit_range(&RevspecPattern::from("..1.0.0"))?;
+        let commit_range = repo.get_commit_range(&RevspecPattern::from_str("..1.0.0")?)?;
 
         assert_that!(commit_range.from).is_equal_to(OidOf::Other(start));
         assert_that!(commit_range.to.to_string()).is_equal_to("1.0.0".to_string());
@@ -563,9 +539,9 @@ mod test {
         )?;
 
         let commit_range_package =
-            repo.get_commit_range_for_package(&RevspecPattern::from("..HEAD"), "one")?;
+            repo.get_commit_range_for_package(&RevspecPattern::from_str("..HEAD")?, "one")?;
         let commit_range_global =
-            repo.get_commit_range_for_monorepo_global(&RevspecPattern::from("..HEAD"))?;
+            repo.get_commit_range_for_monorepo_global(&RevspecPattern::from_str("..HEAD")?)?;
 
         assert_that!(commit_range_package.commits).has_length(1);
         assert_that!(commit_range_global.commits).has_length(1);
@@ -592,7 +568,7 @@ mod test {
         )?;
 
         // Act
-        let commit_range = repo.get_commit_range(&RevspecPattern::from("..1.0.0"))?;
+        let commit_range = repo.get_commit_range(&RevspecPattern::from_str("..1.0.0")?)?;
 
         // Assert
         assert_that!(commit_range.from).is_equal_to(OidOf::Other(start));
@@ -611,7 +587,7 @@ mod test {
         let v3_0_0 = OidOf::Tag(Tag::from_str("3.0.0", Some(v3_0_0))?);
 
         // Act
-        let range = repo.get_commit_range(&RevspecPattern::from("1.0.0..3.0.0"))?;
+        let range = repo.get_commit_range(&RevspecPattern::from_str("1.0.0..3.0.0")?)?;
 
         // Assert
         assert_that!(range.from).is_equal_to(v1_0_0);
@@ -639,7 +615,7 @@ mod test {
         let v1_0_0 = OidOf::Tag(Tag::from_str("1.0.0", Some(v1_0_0))?);
 
         // Act
-        let range = repo.get_commit_range(&RevspecPattern::from("1.0.0.."))?;
+        let range = repo.get_commit_range(&RevspecPattern::from_str("1.0.0..")?)?;
 
         // Assert
         assert_that!(range.from).is_equal_to(v1_0_0);
@@ -684,7 +660,7 @@ mod test {
         let v3_0_0 = OidOf::Tag(Tag::from_str("3.0.0", Some(v3_0_0))?);
 
         // Act
-        let range = repo.get_commit_range(&RevspecPattern::from("..3.0.0"))?;
+        let range = repo.get_commit_range(&RevspecPattern::from_str("..3.0.0")?)?;
 
         // Assert
         assert_that!(range.from).is_equal_to(v2_1_1);
@@ -706,7 +682,7 @@ mod test {
         };
 
         // Act
-        let mut release = repo.get_release_range(RevspecPattern::from(".."))?;
+        let mut release = repo.get_release_range(RevspecPattern::from_str("..")?)?;
         let mut count = 0;
 
         while let Some(previous) = release.previous {
@@ -750,7 +726,7 @@ mod test {
         let pattern = format!("{}..", &from[0..7]);
 
         // Act
-        let release = repo.get_release_range(RevspecPattern::from(pattern.as_str()))?;
+        let release = repo.get_release_range(RevspecPattern::from_str(pattern.as_str())?)?;
 
         // Assert
         let oids: Vec<String> = release
@@ -795,7 +771,7 @@ mod test {
         let pattern = format!("{}..", &from[0..7]);
 
         // Act
-        let release = repo.get_release_range(RevspecPattern::from(pattern.as_str()))?;
+        let release = repo.get_release_range(RevspecPattern::from_str(pattern.as_str())?)?;
 
         // Assert
         let head_to_v1: Vec<String> = release
diff --git a/src/settings/mod.rs b/src/settings/mod.rs
index 21de236..6230dc0 100644
--- a/src/settings/mod.rs
+++ b/src/settings/mod.rs
@@ -36,6 +36,7 @@ pub struct Settings {
     pub monorepo_version_separator: Option<String>,
     pub branch_whitelist: Vec<String>,
     pub tag_prefix: Option<String>,
+    pub skip_ci: Option<String>,
     pub pre_bump_hooks: Vec<String>,
     pub post_bump_hooks: Vec<String>,
     pub pre_package_bump_hooks: Vec<String>,
@@ -56,6 +57,7 @@ impl Default for Settings {
             monorepo_version_separator: None,
             branch_whitelist: vec![],
             tag_prefix: None,
+            skip_ci: None,
             pre_bump_hooks: vec![],
             post_bump_hooks: vec![],
             pre_package_bump_hooks: vec![],


Test patch:
diff --git a/tests/lib_tests/bump.rs b/tests/lib_tests/bump.rs
index d3037f6..77a1f9f 100644
--- a/tests/lib_tests/bump.rs
+++ b/tests/lib_tests/bump.rs
@@ -21,7 +21,7 @@ fn bump_ok() -> Result<()> {
     let mut cocogitto = CocoGitto::get()?;
 
     // Act
-    let result = cocogitto.create_version(IncrementCommand::Auto, None, None, None, false);
+    let result = cocogitto.create_version(IncrementCommand::Auto, None, None, None, false, None);
 
     // Assert
     assert_that!(result).is_ok();
@@ -47,6 +47,7 @@ fn annotated_bump_ok() -> Result<()> {
         None,
         Some(String::from("Release version {{version}}")),
         false,
+        None,
     );
 
     // Assert
@@ -68,7 +69,8 @@ fn monorepo_bump_ok() -> Result<()> {
     let mut cocogitto = CocoGitto::get()?;
 
     // Act
-    let result = cocogitto.create_monorepo_version(IncrementCommand::Auto, None, None, None, false);
+    let result =
+        cocogitto.create_monorepo_version(IncrementCommand::Auto, None, None, None, false, None);
 
     // Assert
     assert_that!(result).is_ok();
@@ -93,7 +95,7 @@ fn monorepo_bump_manual_ok() -> Result<()> {
 
     // Act
     let result =
-        cocogitto.create_monorepo_version(IncrementCommand::Major, None, None, None, false);
+        cocogitto.create_monorepo_version(IncrementCommand::Major, None, None, None, false, None);
 
     // Assert
     assert_that!(result).is_ok();
@@ -114,7 +116,8 @@ fn monorepo_with_tag_prefix_bump_ok() -> Result<()> {
     let mut cocogitto = CocoGitto::get()?;
 
     // Act
-    let result = cocogitto.create_monorepo_version(IncrementCommand::Auto, None, None, None, false);
+    let result =
+        cocogitto.create_monorepo_version(IncrementCommand::Auto, None, None, None, false, None);
 
     // Assert
     assert_that!(result).is_ok();
@@ -142,6 +145,7 @@ fn package_bump_ok() -> Result<()> {
         None,
         None,
         false,
+        None,
     );
 
     // Assert
@@ -209,6 +213,7 @@ fn consecutive_package_bump_ok() -> Result<()> {
         None,
         None,
         false,
+        None,
     )?;
 
     cocogitto.create_package_version(
@@ -218,6 +223,7 @@ fn consecutive_package_bump_ok() -> Result<()> {
         None,
         None,
         false,
+        None,
     )?;
 
     run_cmd!(
@@ -233,6 +239,7 @@ fn consecutive_package_bump_ok() -> Result<()> {
         None,
         None,
         false,
+        None,
     )?;
 
     // Assert
@@ -254,7 +261,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, None, false);
+    let result = cocogitto.create_version(IncrementCommand::Auto, None, None, None, false, None);
 
     // Assert
     assert_that!(result).is_ok();
@@ -313,7 +320,7 @@ fn auto_bump_package_only_ok() -> Result<()> {
     let mut cocogitto = CocoGitto::get()?;
 
     // Act
-    cocogitto.create_all_package_version_auto(None, None, false)?;
+    cocogitto.create_all_package_version_auto(None, None, false, None)?;
 
     assert_tag_exists("jenkins-0.1.0")?;
     assert_tag_exists("thumbor-0.1.0")?;
@@ -325,7 +332,7 @@ fn auto_bump_package_only_ok() -> Result<()> {
         git commit -m "fix(jenkins): bug fix on jenkins package";
     )?;
 
-    cocogitto.create_all_package_version_auto(None, None, false)?;
+    cocogitto.create_all_package_version_auto(None, None, false, None)?;
 
     // Assert
     assert_tag_exists("jenkins-0.1.1")?;
@@ -377,7 +384,7 @@ fn bump_with_whitelisted_branch_ok() -> Result<()> {
     let mut cocogitto = CocoGitto::get()?;
 
     // Act
-    let result = cocogitto.create_version(IncrementCommand::Auto, None, None, None, false);
+    let result = cocogitto.create_version(IncrementCommand::Auto, None, None, None, false, None);
 
     // Assert
     assert_that!(result).is_ok();
@@ -402,7 +409,7 @@ fn bump_with_whitelisted_branch_fails() -> Result<()> {
     let mut cocogitto = CocoGitto::get()?;
 
     // Act
-    let result = cocogitto.create_version(IncrementCommand::Auto, None, None, None, false);
+    let result = cocogitto.create_version(IncrementCommand::Auto, None, None, None, false, None);
 
     // Assert
     assert_that!(result.unwrap_err().to_string()).is_equal_to(
@@ -431,7 +438,7 @@ fn bump_with_whitelisted_branch_pattern_ok() -> Result<()> {
     let mut cocogitto = CocoGitto::get()?;
 
     // Act
-    let result = cocogitto.create_version(IncrementCommand::Auto, None, None, None, false);
+    let result = cocogitto.create_version(IncrementCommand::Auto, None, None, None, false, None);
 
     // Assert
     assert_that!(result).is_ok();
@@ -456,7 +463,7 @@ fn bump_with_whitelisted_branch_pattern_err() -> Result<()> {
     let mut cocogitto = CocoGitto::get()?;
 
     // Act
-    let result = cocogitto.create_version(IncrementCommand::Auto, None, None, None, false);
+    let result = cocogitto.create_version(IncrementCommand::Auto, None, None, None, false, None);
 
     // Assert
     assert_that!(result).is_err();
@@ -513,7 +520,7 @@ fn bump_no_error_should_be_thrown_on_only_chore_docs_commit() -> Result<()> {
     let mut cocogitto = CocoGitto::get()?;
 
     // Act
-    cocogitto.create_monorepo_version(IncrementCommand::Auto, None, None, None, false)?;
+    cocogitto.create_monorepo_version(IncrementCommand::Auto, None, None, None, false, None)?;
 
     run_cmd!(
         echo "chore on jenkins" > jenkins/fix;
@@ -524,7 +531,7 @@ fn bump_no_error_should_be_thrown_on_only_chore_docs_commit() -> Result<()> {
         git commit -m "docs(jenkins): jenkins docs";
     )?;
 
-    cocogitto.create_monorepo_version(IncrementCommand::Auto, None, None, None, false)?;
+    cocogitto.create_monorepo_version(IncrementCommand::Auto, None, None, None, false, None)?;
 
     cocogitto.create_package_version(
         ("jenkins", &jenkins()),
@@ -533,6 +540,7 @@ fn bump_no_error_should_be_thrown_on_only_chore_docs_commit() -> Result<()> {
         None,
         None,
         false,
+        None,
     )?;
 
     run_cmd!(
@@ -541,7 +549,7 @@ fn bump_no_error_should_be_thrown_on_only_chore_docs_commit() -> Result<()> {
         git commit -m "feat(thumbor): more feat on thumbor";
     )?;
 
-    cocogitto.create_monorepo_version(IncrementCommand::Auto, None, None, None, false)?;
+    cocogitto.create_monorepo_version(IncrementCommand::Auto, None, None, None, false, None)?;
 
     // Assert
     assert_tag_exists("jenkins-0.1.0")?;
@@ -582,7 +590,8 @@ fn error_on_no_conventionnal_commits_found_for_monorepo() -> Result<()> {
     let mut cocogitto = CocoGitto::get()?;
 
     // Act
-    let first_result = cocogitto.create_version(IncrementCommand::Auto, None, None, None, false);
+    let first_result =
+        cocogitto.create_version(IncrementCommand::Auto, None, None, None, false, None);
 
     // Assert
     assert_that!(first_result).is_ok();
@@ -595,7 +604,8 @@ fn error_on_no_conventionnal_commits_found_for_monorepo() -> Result<()> {
     git_commit("second unconventional feature commit")?;
 
     // Act
-    let second_result = cocogitto.create_version(IncrementCommand::Auto, None, None, None, false);
+    let second_result =
+        cocogitto.create_version(IncrementCommand::Auto, None, None, None, false, None);
 
     // Assert
     assert_that!(second_result).is_err();
@@ -648,6 +658,7 @@ fn error_on_no_conventionnal_commits_found_for_package() -> Result<()> {
         None,
         None,
         false,
+        None,
     );
 
     assert_that!(first_result).is_ok();
@@ -665,6 +676,7 @@ fn error_on_no_conventionnal_commits_found_for_package() -> Result<()> {
         None,
         None,
         false,
+        None,
     );
 
     assert_that!(second_result).is_err();
@@ -721,7 +733,8 @@ fn bump_with_unconventionnal_and_conventional_commits_found_for_packages() -> Re
     let mut cocogitto = CocoGitto::get()?;
 
     // Act
-    let result = cocogitto.create_monorepo_version(IncrementCommand::Auto, None, None, None, false);
+    let result =
+        cocogitto.create_monorepo_version(IncrementCommand::Auto, None, None, None, false, None);
 
     // Assert
     assert_that!(result).is_ok();