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, ) -> 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, 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, branch: &str, ) -> Result, 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, 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, next_version: &Version, source: &std::path::PathBuf, ) -> Result<(std::path::PathBuf, String, Option), 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, ) -> anyhow::Result, Version)>> { let commit_strs = commits .iter() .map(|c| c.commit.message.clone()) .collect::>(); 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) -> Version { significant_tag .map(|st| Version::try_from(st).unwrap()) .unwrap_or(Version::new(0, 0, 0)) }