cuddle-please/crates/cuddle-please-commands/src/release_command.rs
kjuulh 113e5282ef
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
fix(crate): initial pr always included the entire changelog
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-04 15:01:32 +02:00

258 lines
8.3 KiB
Rust

use cuddle_please_frontend::PleaseConfig;
use ::semver::Version;
use anyhow::Context;
use cuddle_please_misc::{
changelog_parser, get_most_significant_version, ChangeLogBuilder, Commit, DynRemoteGitClient,
DynUi, NextVersion, Tag, VcsClient,
};
pub struct ReleaseCommandHandler {
ui: DynUi,
config: PleaseConfig,
git_client: VcsClient,
gitea_client: DynRemoteGitClient,
}
impl ReleaseCommandHandler {
pub fn new(
ui: DynUi,
config: PleaseConfig,
git_client: VcsClient,
gitea_client: DynRemoteGitClient,
) -> Self {
Self {
ui,
config,
git_client,
gitea_client,
}
}
pub fn execute(&self, dry_run: bool) -> anyhow::Result<()> {
tracing::debug!("running command: release");
let owner = self.config.get_owner();
let repository = self.config.get_repository();
let branch = self.config.get_branch();
let source = self.config.get_source();
self.ui.write_str_ln("running releaser");
self.check_git_remote_connection(owner, repository)?;
tracing::trace!("connected to git remote");
let significant_tag = self.get_most_significant_tag(owner, repository)?;
tracing::trace!("found lastest release tag");
let commits =
self.fetch_commits_since_last_tag(owner, repository, &significant_tag, branch)?;
tracing::trace!("fetched commits since last version");
let current_version = get_current_version(significant_tag);
tracing::trace!("found current version: {}", current_version.to_string());
self.ui
.write_str_ln(&format!("found current version: {}", current_version));
let conventional_commit_results = parse_conventional_commits(current_version, commits)?;
tracing::trace!("parsing conventional commits");
if conventional_commit_results.is_none() {
tracing::debug!("found no new commits, aborting early");
self.ui
.write_str_ln("no new commits found, no release required");
return Ok(());
}
let (commit_strs, next_version) = conventional_commit_results.unwrap();
self.ui
.write_str_ln(&format!("calculated next version: {}", next_version));
tracing::trace!("creating changelog");
let (changelog_placement, changelog, changelog_last_changes) =
compose_changelog(&commit_strs, &next_version, source)?;
if let Some(first_commit) = commit_strs.first() {
if first_commit.contains("chore(release): ") {
tracing::trace!("creating release");
self.ui.write_str_ln("creating release");
self.create_release(
dry_run,
owner,
repository,
&next_version,
changelog_last_changes,
)?;
return Ok(());
}
}
tracing::trace!("creating pull-request");
self.create_pull_request(
changelog_placement,
changelog,
next_version,
dry_run,
owner,
repository,
changelog_last_changes,
branch,
)?;
Ok(())
}
fn create_release(
&self,
dry_run: bool,
owner: &str,
repository: &str,
next_version: &Version,
changelog_last_changes: Option<String>,
) -> Result<(), anyhow::Error> {
if !dry_run {
self.gitea_client.create_release(
owner,
repository,
&next_version.to_string(),
&changelog_last_changes.unwrap(),
!next_version.pre.is_empty(),
)?;
} else {
tracing::debug!("creating release (dry_run)");
};
Ok(())
}
fn create_pull_request(
&self,
changelog_placement: std::path::PathBuf,
changelog: String,
next_version: Version,
dry_run: bool,
owner: &str,
repository: &str,
changelog_last_changes: Option<String>,
branch: &str,
) -> Result<(), anyhow::Error> {
self.ui
.write_str_ln("creating and checking out release branch");
self.git_client.checkout_branch()?;
std::fs::write(changelog_placement, changelog.as_bytes())?;
self.ui.write_str_ln("committed changelog files");
self.git_client
.commit_and_push(next_version.to_string(), dry_run)?;
let _pr_number = match self.gitea_client.get_pull_request(owner, repository)? {
Some(existing_pr) => {
self.ui.write_str_ln("found existing pull request");
self.ui.write_str_ln("updating pull request");
if !dry_run {
self.gitea_client.update_pull_request(
owner,
repository,
&next_version.to_string(),
&changelog_last_changes
.ok_or(anyhow::anyhow!("could not get the latest changes"))?,
existing_pr,
)?
} else {
tracing::debug!("updating pull request (dry_run)");
1
}
}
None => {
self.ui.write_str_ln("creating pull request");
if !dry_run {
self.gitea_client.create_pull_request(
owner,
repository,
&next_version.to_string(),
&changelog_last_changes
.ok_or(anyhow::anyhow!("could not get the latest changes"))?,
branch,
)?
} else {
tracing::debug!("creating pull request (dry_run)");
1
}
}
};
Ok(())
}
fn fetch_commits_since_last_tag(
&self,
owner: &str,
repository: &str,
significant_tag: &Option<Tag>,
branch: &str,
) -> Result<Vec<Commit>, anyhow::Error> {
let commits = self.gitea_client.get_commits_since(
owner,
repository,
significant_tag.as_ref().map(|st| st.commit.sha.as_str()),
branch,
)?;
Ok(commits)
}
fn get_most_significant_tag(
&self,
owner: &str,
repository: &str,
) -> Result<Option<Tag>, anyhow::Error> {
let tags = self.gitea_client.get_tags(owner, repository)?;
let significant_tag = get_most_significant_version(tags.iter().collect());
Ok(significant_tag.cloned())
}
fn check_git_remote_connection(
&self,
owner: &str,
repository: &str,
) -> Result<(), anyhow::Error> {
self.gitea_client
.connect(owner, repository)
.context("failed to connect to gitea repository")?;
Ok(())
}
}
fn compose_changelog(
commit_strs: &Vec<String>,
next_version: &Version,
source: &std::path::PathBuf,
) -> Result<(std::path::PathBuf, String, Option<String>), anyhow::Error> {
let builder = ChangeLogBuilder::new(commit_strs, next_version.to_string()).build();
let changelog_placement = source.join("CHANGELOG.md");
let changelog = match std::fs::read_to_string(&changelog_placement).ok() {
Some(existing_changelog) => builder.prepend(existing_changelog)?,
None => builder.generate()?,
};
let changelog_last_changes = changelog_parser::last_changes(&changelog)?;
Ok((changelog_placement, changelog, changelog_last_changes))
}
fn parse_conventional_commits(
current_version: Version,
commits: Vec<Commit>,
) -> anyhow::Result<Option<(Vec<String>, Version)>> {
let commit_strs = commits
.iter()
.map(|c| c.commit.message.clone())
.collect::<Vec<_>>();
if commit_strs.is_empty() {
tracing::info!("no commits to base release on");
return Ok(None);
}
let next_version = current_version.next(&commit_strs);
Ok(Some((commit_strs, next_version)))
}
fn get_current_version(significant_tag: Option<Tag>) -> Version {
significant_tag
.map(|st| Version::try_from(st).unwrap())
.unwrap_or(Version::new(0, 0, 0))
}