diff --git a/src/cli.rs b/src/cli.rs index c212a02..b4ff326 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -35,20 +35,36 @@ pub struct NextVersionArgs { short = 'i' )] pub increment: Increment, - #[clap(help = "Get next version based on pattern", long, short = 'p')] + #[clap(help = "Get next version based on a given pattern", long, short = 'p')] pub pattern: Option, - #[clap(help = "Tag current commit as next version", long, short = 't', action)] + #[clap( + help = "Tag current commit as the next version pattern", + long, + short = 't', + action + )] pub tag: bool, #[clap(help = "Verbose output", long, short = 'V', action)] pub verbose: bool, + #[clap( + help = "Create new branch as the next version pattern", + long, + short = 'b', + action + )] + pub branch: bool, } #[derive(Args, Debug)] pub struct LastVersionArgs { - #[clap(help = "Get last version based on last version", long, short = 'p')] + #[clap(help = "Get last version based on a given pattern", long, short = 'p')] pub pattern: Option, - #[clap(help = "Check out to last version", long, short = 'c', action)] + #[clap(help = "Check out to the last version", long, short = 'c', action)] pub checkout: bool, #[clap(help = "Verbose output", long, short = 'V', action)] pub verbose: bool, + #[clap(help = "Get last version based on tag", long, short = 't', action)] + pub tag: bool, + #[clap(help = "Get last version based on branch", long, short = 'b', action)] + pub branch: bool, } diff --git a/src/gitutils.rs b/src/gitutils.rs index 5d6b5e3..d19f0e6 100644 --- a/src/gitutils.rs +++ b/src/gitutils.rs @@ -202,3 +202,17 @@ fn git_callbacks() -> git2::RemoteCallbacks<'static> { }); cb } + +pub fn create_branch( + repo: &Repository, + name: &str, + force: bool, + options: Option<&CommandOptions>, +) -> Result<(), git2::Error> { + let default = CommandOptions::default(); + let opts = options.unwrap_or(&default); + let commit = repo.head()?.peel_to_commit()?; + repo.branch(name, &commit, force)?; + print_verbose(&format!("Created branch '{}'", name), opts.verbose); + Result::Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index d0fb2dd..f30fb6f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,4 +4,5 @@ mod testutils; pub mod cli; pub mod gitutils; pub mod service; +pub mod version_source; pub mod versioning; diff --git a/src/service.rs b/src/service.rs index ce01018..b779f04 100644 --- a/src/service.rs +++ b/src/service.rs @@ -2,6 +2,7 @@ use std::path::Path; use crate::cli::{LastVersionArgs, NextVersionArgs}; use crate::gitutils::{self, CommandOptions}; +use crate::version_source::{BranchVersionSource, TagVersionSource, VersionSource}; use crate::versioning::Versioner; pub fn last_version(path: &Path, args: &LastVersionArgs) -> Option { @@ -16,11 +17,13 @@ pub fn last_version(path: &Path, args: &LastVersionArgs) -> Option { .pattern .clone() .unwrap_or("v{major}.{minor}.{patch}".to_string()); - let versioner = get_versioner(&repo, pattern); + let versioner = versioner_factory(&repo, pattern, args.branch); if let Some(version) = versioner.last_version() { if args.checkout { - gitutils::checkout_tag(&repo, &version.tag, Some(&opts)) - .expect("Failed to checkout tag"); + let version_source = version_source_factory(args.branch); + version_source + .checkout(&repo, &version.tag) + .expect("Failed to checkout version"); } println!("{}", version.tag); Some(version.tag) @@ -41,13 +44,14 @@ pub fn next_version(path: &Path, args: &NextVersionArgs) -> Option { .pattern .clone() .unwrap_or("v{major}.{minor}.{patch}".to_string()); - let versioner = get_versioner(&repo, pattern); + let versioner = versioner_factory(&repo, pattern, args.branch); if let Some(version) = versioner.next_version(args.increment.clone()) { - if args.tag { - let head = repo.head().unwrap(); - let head_id = head.target().unwrap(); - gitutils::tag_oid(&repo, head_id, version.tag.as_str()).expect("Failed to tag"); + if args.tag || args.branch { + let version_source = version_source_factory(args.branch); + version_source + .create_new(&repo, &version.tag) + .expect("Failed to create new version"); } println!("{}", version.tag); Some(version.tag) @@ -57,15 +61,18 @@ pub fn next_version(path: &Path, args: &NextVersionArgs) -> Option { } } -fn get_versioner(repo: &git2::Repository, pattern: String) -> Versioner { - let tag_names = repo - .tag_names(Some("*")) - .expect("Failed to fetch tags") - .iter() - .map(|s| s.unwrap().to_string()) - .collect::>(); - let versioner = Versioner::new(tag_names, pattern); - versioner +fn version_source_factory(use_branches: bool) -> Box { + if use_branches { + Box::new(BranchVersionSource) + } else { + Box::new(TagVersionSource) + } +} + +fn versioner_factory(repo: &git2::Repository, pattern: String, use_branches: bool) -> Versioner { + let version_source = version_source_factory(use_branches); + let versions = version_source.get_all_versions(repo); + Versioner::new(versions, pattern) } #[cfg(test)] @@ -102,6 +109,8 @@ mod tests { pattern: Some("flopha@{major}.{minor}.{patch}".to_string()), checkout: false, verbose: false, + branch: false, + tag: true, }; let result = last_version(td.path(), &args); @@ -126,6 +135,8 @@ mod tests { pattern: Some("flopha@{major}.{minor}.{patch}".to_string()), checkout: false, verbose: false, + branch: false, + tag: true, }; let result = last_version(td.path(), &args); @@ -156,6 +167,8 @@ mod tests { pattern: Some("flopha@{major}.{minor}.{patch}".to_string()), checkout: true, verbose: false, + branch: false, + tag: true, }; last_version(td.path(), &args); @@ -194,6 +207,7 @@ mod tests { increment: Increment::Patch, tag: false, verbose: false, + branch: false, }; let result = next_version(td.path(), &args); @@ -226,6 +240,7 @@ mod tests { increment: Increment::Patch, tag: true, verbose: false, + branch: false, }; next_version(td.path(), &args); @@ -235,6 +250,78 @@ mod tests { assert_eq!(tag_id, head_id); } + #[test] + fn test_last_version_returns_last_version_with_given_pattern_for_branches() { + // Given + let (td, repo) = testutils::init_repo(); + let (_remote_td, mut remote) = testutils::init_remote(&repo); + + let branches = vec![ + "release/0.1.0", + "release/1.0.0", + "release/1.0.1", + "release/1.1.1", + "release/1.1.9", + "release/2.10.11", + "release/1.1.10", + "release/2.9.9", + "release/2.10.10", + ]; + for branch in branches { + create_new_remote_branch(&repo, &mut remote, branch); + } + + // When + let args = LastVersionArgs { + pattern: Some("release/{major}.{minor}.{patch}".to_string()), + checkout: false, + verbose: false, + tag: false, + branch: true, + }; + + let result = last_version(td.path(), &args); + + // Then + assert_eq!(result.unwrap(), "release/2.10.11"); + } + + #[test] + fn test_next_version_returns_next_version_with_given_pattern_for_branches() { + // Given + let (td, repo) = testutils::init_repo(); + let (_remote_td, mut remote) = testutils::init_remote(&repo); + let branches = vec![ + "release/0.1.0", + "release/1.0.0", + "release/1.0.1", + "release/1.1.1", + "release/1.1.9", + "release/2.10.11", + "release/1.1.10", + "release/2.9.9", + "release/2.10.10", + ]; + for branch in branches { + create_new_remote_branch(&repo, &mut remote, branch); + } + gitutils::checkout_branch(&repo, "release/2.10.11", false, None).unwrap(); + gitutils::commit(&repo, "New commit").unwrap(); + + // When + let args = NextVersionArgs { + pattern: Some("release/{major}.{minor}.{patch}".to_string()), + increment: Increment::Patch, + tag: false, + verbose: false, + branch: true, + }; + let result = next_version(td.path(), &args); + + // Then + assert_eq!(result.unwrap(), "release/2.10.12") + } + fn create_new_remote_tag( repo: &git2::Repository, remote: &mut git2::Remote, @@ -249,4 +336,11 @@ mod tests { repo.tag_delete(tag).unwrap(); // delete local tag } } + + fn create_new_remote_branch(repo: &git2::Repository, remote: &mut git2::Remote, branch: &str) { + gitutils::checkout_branch(repo, branch, true, None).unwrap(); + gitutils::commit(repo, "New commit").unwrap(); + let mut branch = repo.find_branch(branch, git2::BranchType::Local).unwrap(); + gitutils::push_branch(remote, &mut branch, None).unwrap(); + } } diff --git a/src/version_source.rs b/src/version_source.rs new file mode 100644 index 0000000..e9bdfe7 --- /dev/null +++ b/src/version_source.rs @@ -0,0 +1,50 @@ +use crate::gitutils; +use git2::Repository; + +pub trait VersionSource { + fn get_all_versions(&self, repo: &Repository) -> Vec; + fn checkout(&self, repo: &Repository, version: &str) -> Result<(), git2::Error>; + fn create_new(&self, repo: &Repository, version: &str) -> Result<(), git2::Error>; +} + +pub struct TagVersionSource; +pub struct BranchVersionSource; + +impl VersionSource for TagVersionSource { + fn get_all_versions(&self, repo: &Repository) -> Vec { + repo.tag_names(Some("*")) + .expect("Failed to fetch tags") + .iter() + .filter_map(|s| s.map(|s| s.to_string())) + .collect() + } + + fn checkout(&self, repo: &Repository, version: &str) -> Result<(), git2::Error> { + gitutils::checkout_tag(repo, version, None) + } + + fn create_new(&self, repo: &Repository, version: &str) -> Result<(), git2::Error> { + let head = repo.head()?; + let head_id = head.target().unwrap(); + gitutils::tag_oid(repo, head_id, version)?; + Ok(()) + } +} + +impl VersionSource for BranchVersionSource { + fn get_all_versions(&self, repo: &Repository) -> Vec { + repo.branches(Some(git2::BranchType::Local)) + .expect("Failed to fetch branches") + .filter_map(|b| b.ok()) + .filter_map(|(branch, _)| branch.name().ok().flatten().map(|s| s.to_string())) + .collect() + } + + fn checkout(&self, repo: &Repository, version: &str) -> Result<(), git2::Error> { + gitutils::checkout_branch(repo, version, false, None) + } + + fn create_new(&self, repo: &Repository, version: &str) -> Result<(), git2::Error> { + gitutils::checkout_branch(repo, version, true, None) + } +}