Skip to content

Commit

Permalink
✨ Enable branch based versioning
Browse files Browse the repository at this point in the history
  • Loading branch information
sjquant committed Sep 6, 2024
1 parent 478f269 commit 601c464
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 21 deletions.
24 changes: 20 additions & 4 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
#[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<String>,
#[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,
}
14 changes: 14 additions & 0 deletions src/gitutils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ mod testutils;
pub mod cli;
pub mod gitutils;
pub mod service;
pub mod version_source;
pub mod versioning;
128 changes: 111 additions & 17 deletions src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
Expand All @@ -16,11 +17,13 @@ pub fn last_version(path: &Path, args: &LastVersionArgs) -> Option<String> {
.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)
Expand All @@ -41,13 +44,14 @@ pub fn next_version(path: &Path, args: &NextVersionArgs) -> Option<String> {
.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)
Expand All @@ -57,15 +61,18 @@ pub fn next_version(path: &Path, args: &NextVersionArgs) -> Option<String> {
}
}

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::<Vec<_>>();
let versioner = Versioner::new(tag_names, pattern);
versioner
fn version_source_factory(use_branches: bool) -> Box<dyn VersionSource> {
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)]
Expand Down Expand Up @@ -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);
Expand All @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -194,6 +207,7 @@ mod tests {
increment: Increment::Patch,
tag: false,
verbose: false,
branch: false,
};
let result = next_version(td.path(), &args);

Expand Down Expand Up @@ -226,6 +240,7 @@ mod tests {
increment: Increment::Patch,
tag: true,
verbose: false,
branch: false,
};
next_version(td.path(), &args);

Expand All @@ -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,
Expand All @@ -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();
}
}
50 changes: 50 additions & 0 deletions src/version_source.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use crate::gitutils;
use git2::Repository;

pub trait VersionSource {
fn get_all_versions(&self, repo: &Repository) -> Vec<String>;
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<String> {
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<String> {
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)
}
}

0 comments on commit 601c464

Please sign in to comment.