use std::{
borrow::{Borrow, Cow},
fmt,
fmt::{Debug, Display},
ops::Deref,
};
use changesets::Release;
use itertools::Itertools;
#[cfg(feature = "miette")]
use miette::Diagnostic;
use relative_path::RelativePathBuf;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tracing::debug;
use crate::{
action::Action,
changes::{
conventional_commit::changes_from_commit_messages, Change, ChangeSource, CHANGESET_DIR,
},
release_notes::{ReleaseNotes, TimeError},
semver::{Label, PackageVersions, PreReleaseNotFound, Rule, StableRule, Version},
versioned_file::{GoVersioning, SetError, VersionedFile},
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Package {
pub name: Name,
pub versions: PackageVersions,
versioned_files: Option<Vec<VersionedFile>>,
pub release_notes: ReleaseNotes,
pub scopes: Option<Vec<String>>,
}
impl Package {
pub fn new<S: AsRef<str> + Debug>(
name: Name,
git_tags: &[S],
versioned_files: Vec<VersionedFile>,
release_notes: ReleaseNotes,
scopes: Option<Vec<String>>,
) -> Result<Self, NewError> {
if let Some(first) = versioned_files.first() {
if let Some(conflict) = versioned_files
.iter()
.find(|f| f.version() != first.version())
{
return Err(NewError::InconsistentVersions(
Box::new(first.clone()),
Box::new(conflict.clone()),
));
}
}
debug!("Looking for Git tags matching package name.");
let mut versions = PackageVersions::from_tags(name.as_custom(), git_tags);
if let Some(version_from_files) = versioned_files.first().map(VersionedFile::version) {
versions.update_version(version_from_files.clone());
}
Ok(Self {
name,
versions,
versioned_files: Some(versioned_files),
release_notes,
scopes,
})
}
pub fn bump_version(
&mut self,
bump: Bump,
go_versioning: GoVersioning,
) -> Result<Vec<Action>, BumpError> {
let Some(versioned_files) = self.versioned_files.take() else {
return Err(BumpError::PackageAlreadyBumped);
};
match bump {
Bump::Manual(version) => {
self.versions.update_version(version);
}
Bump::Rule(rule) => {
self.versions.bump(rule)?;
}
};
let version = self.versions.clone().into_latest();
versioned_files
.into_iter()
.map(|f| f.set_version(&version, go_versioning))
.process_results(|iter| iter.flatten().collect())
.map_err(BumpError::SetError)
}
#[must_use]
pub fn get_changes(&self, changeset: &[Release], commit_messages: &[String]) -> Vec<Change> {
changes_from_commit_messages(
commit_messages,
self.scopes.as_ref(),
&self.release_notes.sections,
)
.chain(Change::from_changesets(&self.name, changeset))
.collect()
}
pub fn apply_changes(
&mut self,
changes: &[Change],
config: ChangeConfig,
) -> Result<Vec<Action>, BumpError> {
if let Name::Custom(package_name) = &self.name {
debug!("Determining new version for {package_name}");
}
let mut actions = match config {
ChangeConfig::Force(version) => {
debug!("Using overridden version {version}");
self.bump_version(Bump::Manual(version), GoVersioning::BumpMajor)?
}
ChangeConfig::Calculate {
prerelease_label,
go_versioning,
} => {
let stable_rule = StableRule::from(changes);
let rule = if let Some(pre_label) = prerelease_label {
Rule::Pre {
label: pre_label.clone(),
stable_rule,
}
} else {
stable_rule.into()
};
self.bump_version(Bump::Rule(rule), go_versioning)?
}
};
let version = self.versions.clone().into_latest();
actions.extend(changes.iter().filter_map(|change| {
if let ChangeSource::ChangeFile(unique_id) = &change.original_source {
if version.is_prerelease() {
None
} else {
Some(Action::RemoveFile {
path: RelativePathBuf::from(CHANGESET_DIR).join(unique_id.to_file_name()),
})
}
} else {
None
}
}));
actions.extend(self.release_notes.create_release(version, changes)?);
Ok(actions)
}
}
pub enum ChangeConfig {
Force(Version),
Calculate {
prerelease_label: Option<Label>,
go_versioning: GoVersioning,
},
}
#[derive(Debug, Error)]
#[cfg_attr(feature = "miette", derive(Diagnostic))]
pub enum NewError {
#[error("Found inconsistent versions in package: {} had {} and {} had {}", .0.path(), .0.version(), .1.path(), .1.version())]
#[cfg_attr(
feature = "miette",
diagnostic(
code = "knope_versioning::inconsistent_versions",
url = "https://knope.tech/reference/concepts/package/#version",
help = "All files in a package must have the same version"
)
)]
InconsistentVersions(Box<VersionedFile>, Box<VersionedFile>),
#[error("Packages must have at least one versioned file")]
NoPackages,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(untagged)]
pub enum Name {
Custom(String),
#[default]
Default,
}
impl Name {
const DEFAULT: &'static str = "default";
#[must_use]
pub fn as_custom(&self) -> Option<&str> {
match self {
Self::Custom(name) => Some(name),
Self::Default => None,
}
}
}
impl Display for Name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Custom(name) => write!(f, "{name}"),
Self::Default => write!(f, "{}", Self::DEFAULT),
}
}
}
impl AsRef<str> for Name {
fn as_ref(&self) -> &str {
match self {
Self::Custom(name) => name,
Self::Default => Self::DEFAULT,
}
}
}
impl Deref for Name {
type Target = str;
fn deref(&self) -> &Self::Target {
match self {
Self::Custom(name) => name,
Self::Default => Self::DEFAULT,
}
}
}
impl From<&str> for Name {
fn from(name: &str) -> Self {
Self::Custom(name.to_string())
}
}
impl From<String> for Name {
fn from(name: String) -> Self {
Self::Custom(name)
}
}
impl From<Cow<'_, str>> for Name {
fn from(name: Cow<str>) -> Self {
Self::Custom(name.into_owned())
}
}
impl Borrow<str> for Name {
fn borrow(&self) -> &str {
match self {
Self::Custom(name) => name,
Self::Default => Self::DEFAULT,
}
}
}
impl PartialEq<String> for Name {
fn eq(&self, str: &String) -> bool {
str == self.as_ref()
}
}
pub enum Bump {
Manual(Version),
Rule(Rule),
}
#[derive(Debug, Error)]
#[cfg_attr(feature = "miette", derive(Diagnostic))]
pub enum BumpError {
#[error(transparent)]
#[cfg_attr(feature = "miette", diagnostic(transparent))]
SetError(#[from] SetError),
#[error(transparent)]
PreReleaseNotFound(#[from] PreReleaseNotFound),
#[error("Package version has already been updated")]
#[cfg_attr(
feature = "miette",
diagnostic(
code = "knope_versioning::package_already_bumped",
url = "https://knope.tech/reference/concepts/package/#version",
help = "You can only run a single BumpVersion or PrepareRelease step per workflow"
)
)]
PackageAlreadyBumped,
#[error(transparent)]
#[cfg_attr(feature = "miette", diagnostic(transparent))]
Time(#[from] TimeError),
}