use std::fmt;
use convco::{
commit_type_eq, open_repo, CommitParser, CommitTrait, Config, ConvcoError, Increment, Repo,
RevWalkOptions, Type,
};
use semver::{Prerelease, Version};
use crate::{cli::VersionCommand, cmd::Command};
enum Label {
Major,
Minor,
Patch,
Release,
Prerelease,
}
impl fmt::Display for Label {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::Major => write!(f, "major"),
Self::Minor => write!(f, "minor"),
Self::Patch => write!(f, "patch"),
Self::Release => write!(f, "release"),
Self::Prerelease => write!(f, "prerelease"),
}
}
}
fn same_base_version(left: &Version, right: &Version) -> bool {
left.major == right.major && left.minor == right.minor && left.patch == right.patch
}
fn prerelease_matches(pre: &semver::Prerelease, prerelease: &Prerelease) -> bool {
pre.rsplit_once('.')
.map(|(label, _)| label == prerelease.as_str())
.unwrap_or(false)
}
fn find_matching_prerelease<C: CommitTrait>(
version: &Version,
prerelease: &Prerelease,
commit_id: &str,
semvers: &[(Version, C)],
) -> Option<Prerelease> {
let mut prereleases = semvers
.iter()
.filter(|(tag_version, commit)| {
same_base_version(tag_version, version)
&& commit.id() == commit_id
&& prerelease_matches(&tag_version.pre, prerelease)
})
.map(|(version, _)| version.clone())
.collect::<Vec<_>>();
prereleases.sort();
prereleases.last().map(|version| version.pre.clone())
}
fn find_last_prerelease<C: CommitTrait>(
version: &Version,
prerelease: &Prerelease,
semvers: &[(Version, C)],
) -> Option<Prerelease> {
let mut prereleases = semvers
.iter()
.filter(|(tag_version, _)| {
same_base_version(tag_version, version)
&& prerelease_matches(&tag_version.pre, prerelease)
})
.map(|(version, _)| version.clone())
.collect::<Vec<_>>();
prereleases.sort();
prereleases.last().map(|version| version.pre.clone())
}
fn calc_prerelease<C: CommitTrait>(
version: &mut Version,
prerelease: &Prerelease,
semvers: &[(Version, C)],
commit_id: &str,
) {
if let Some(prerelease) = find_matching_prerelease(version, prerelease, commit_id, semvers) {
version.pre = prerelease;
} else {
if let Some(prerelease) = find_last_prerelease(version, prerelease, semvers) {
version.pre = prerelease;
}
version.increment_prerelease(prerelease);
}
}
fn ensure_prerelease_base_is_unreleased<C: CommitTrait>(
version: &Version,
semvers: &[(Version, C)],
) -> Result<(), ConvcoError> {
if version.pre.is_empty() {
return Ok(());
}
if semvers.iter().any(|(tag_version, _)| {
same_base_version(tag_version, version) && tag_version.pre.is_empty()
}) {
let mut release = version.clone();
release.pre = semver::Prerelease::EMPTY;
release.build = semver::BuildMetadata::EMPTY;
return Err(ConvcoError::PrereleaseBaseAlreadyReleased {
release,
prerelease: version.clone(),
});
}
Ok(())
}
impl VersionCommand {
fn get_version(
&self,
scope_regex: String,
strip_regex: String,
types: Vec<convco::Type>,
mut initial_bump_version: Version,
treat_major_zero_as_stable: bool,
) -> Result<(Version, Label, String), ConvcoError> {
let repo = open_repo()?;
let prefix = self.prefix.as_str();
let ignore_prereleases = self.bump || self.ignore_prereleases;
let semvers = repo.semver_tags(prefix)?;
let rev = Repo::revparse_single(&repo, &self.rev)?;
let last_version = repo.find_last_version(&rev, ignore_prereleases, &semvers)?;
match last_version {
None => {
let commit = Repo::revparse_single(&repo, &self.rev)?;
let commit_sha = CommitTrait::id(&commit);
let mut version = Version::new(0, 0, 0);
if self.bump {
if self.prerelease.is_empty() {
let label = match (
initial_bump_version.major,
initial_bump_version.minor,
initial_bump_version.patch,
) {
(_, 0, 0) => Label::Major,
(_, _, 0) => Label::Minor,
_ => Label::Patch,
};
Ok((initial_bump_version, label, commit_sha))
} else {
calc_prerelease(
&mut initial_bump_version,
&self.prerelease,
&semvers,
&CommitTrait::id(&commit),
);
ensure_prerelease_base_is_unreleased(&initial_bump_version, &semvers)?;
Ok((initial_bump_version, Label::Prerelease, commit_sha))
}
} else if self.patch {
version.patch = 1;
Ok((version, Label::Patch, commit_sha))
} else if self.minor {
version.minor = 1;
Ok((version, Label::Minor, commit_sha))
} else if self.major {
version.major = 1;
Ok((version, Label::Major, commit_sha))
} else {
Ok((version, Label::Patch, commit_sha))
}
}
Some((mut version, commit)) => {
let v = if self.major {
version.increment_major();
(version, Label::Major, CommitTrait::id(&commit))
} else if self.minor {
version.increment_minor();
(version, Label::Minor, CommitTrait::id(&commit))
} else if self.patch {
version.increment_patch();
(version, Label::Patch, CommitTrait::id(&commit))
} else if self.bump {
if version.is_prerelease() {
if self.prerelease.is_empty() {
version.pre_clear();
version.build_clear();
(version, Label::Release, CommitTrait::id(&commit))
} else {
version.increment_prerelease(&self.prerelease);
ensure_prerelease_base_is_unreleased(&version, &semvers)?;
(version, Label::Prerelease, CommitTrait::id(&commit))
}
} else {
let parser = CommitParser::builder()
.scope_regex(scope_regex)
.strip_regex(strip_regex)
.build();
self.find_bump_version(
&repo,
commit,
version,
&parser,
&types,
&semvers,
treat_major_zero_as_stable,
)?
}
} else {
(version, Label::Release, CommitTrait::id(&commit))
};
Ok(v)
}
}
}
fn find_bump_version<'a, R, C>(
&self,
repo: &'a R,
commit: C,
last_version: semver::Version,
parser: &'a CommitParser,
types: &[Type],
semvers: &[(Version, C)],
treat_major_zero_as_stable: bool,
) -> Result<(Version, Label, String), ConvcoError>
where
R: Repo<'a, CommitTrait = C>,
C: CommitTrait,
{
let mut last_version = last_version;
let to_rev = repo.revparse_single(&self.rev)?;
let options = RevWalkOptions {
from_rev: vec![commit],
to_rev,
first_parent: false,
no_merge_commits: false,
no_revert_commits: false,
paths: self.paths.clone(),
parser,
};
let revwalk = repo.revwalk(options)?;
let mut major = false;
let mut minor = false;
let mut patch = false;
let major_version_zero = last_version.major == 0 && !treat_major_zero_as_stable;
let mut commit_sha = None;
for commit in revwalk.flatten() {
if commit_sha.is_none() {
commit_sha = Some(commit.commit.id());
}
if commit.conventional_commit.is_breaking() {
if major_version_zero {
minor = true;
} else {
major = true;
}
break;
}
let option_commit_type = types
.iter()
.find(|x| commit_type_eq(&x.r#type, &commit.conventional_commit.r#type));
if let Some(some_commit_type) = option_commit_type {
match (&some_commit_type.increment, major_version_zero) {
(Increment::Major, _) => major = true,
(Increment::Minor, true) => patch = true,
(Increment::Minor, false) => minor = true,
(Increment::Patch, _) => patch = true,
_ => {}
}
}
}
let label = match (major, minor, patch) {
(true, _, _) => {
last_version.increment_major();
Label::Major
}
(false, true, _) => {
last_version.increment_minor();
Label::Minor
}
(false, false, true) => {
last_version.increment_patch();
Label::Patch
}
_ => Label::Release,
};
let commit_sha = commit_sha.unwrap_or_default();
if !self.prerelease.is_empty() {
calc_prerelease(&mut last_version, &self.prerelease, semvers, &commit_sha);
ensure_prerelease_base_is_unreleased(&last_version, semvers)?;
}
Ok((last_version, label, commit_sha))
}
}
impl Command for VersionCommand {
fn exec(&self, config: Config) -> anyhow::Result<()> {
let initial_bump_version = self
.initial_bump_version
.clone()
.unwrap_or(config.initial_bump_version);
let treat_major_zero_as_stable =
self.treat_major_zero_as_stable || config.treat_major_zero_as_stable;
let (version, label, commit_sha) = self.get_version(
config.scope_regex,
config.strip_regex,
config.types,
initial_bump_version,
treat_major_zero_as_stable,
)?;
if self.label {
println!("{label}");
} else if self.commit_sha {
println!("{commit_sha}");
} else if self.print_prefix {
println!("{}{version}", self.prefix);
} else {
println!("{version}");
}
Ok(())
}
}
trait VersionExt {
fn increment_major(&mut self);
fn increment_minor(&mut self);
fn increment_patch(&mut self);
fn increment_prerelease(&mut self, prerelease: &semver::Prerelease);
fn pre_clear(&mut self);
fn build_clear(&mut self);
fn is_prerelease(&self) -> bool;
}
impl VersionExt for Version {
fn increment_major(&mut self) {
self.major += 1;
self.minor = 0;
self.patch = 0;
self.pre = semver::Prerelease::EMPTY;
self.build = semver::BuildMetadata::EMPTY;
}
fn increment_minor(&mut self) {
self.minor += 1;
self.patch = 0;
self.pre = semver::Prerelease::EMPTY;
self.build = semver::BuildMetadata::EMPTY;
}
fn increment_patch(&mut self) {
self.patch += 1;
self.pre = semver::Prerelease::EMPTY;
self.build = semver::BuildMetadata::EMPTY;
}
fn increment_prerelease(&mut self, prerelease: &semver::Prerelease) {
if self.pre.is_empty() {
self.pre = semver::Prerelease::new(format!("{prerelease}.1").as_str()).unwrap();
} else {
let next = self
.pre
.split_once('.')
.and_then(|(_, number)| number.parse::<u64>().ok())
.unwrap_or_default()
+ 1;
self.pre = semver::Prerelease::new(format!("{prerelease}.{next}").as_str()).unwrap();
}
}
fn build_clear(&mut self) {
self.build = semver::BuildMetadata::EMPTY;
}
fn pre_clear(&mut self) {
self.pre = semver::Prerelease::EMPTY
}
fn is_prerelease(&self) -> bool {
!self.pre.is_empty()
}
}