kjuulh
5fd902f87c
All checks were successful
continuous-integration/drone/push Build is passing
Allows commit bodies to show up in release notes, this is something I'd prefer as my releases are usually short, and I'd like to see these as I don't use pull requests as often, and often miss the context, as I don't link to commits currently. Also fixes a lot of warnings and reintroduces failing tests, still not perfect, but better than before. Co-authored-by: kjuulh <contact@kjuulh.io> Co-committed-by: kjuulh <contact@kjuulh.io>
267 lines
8.5 KiB
Rust
267 lines
8.5 KiB
Rust
use std::path::Path;
|
|
|
|
use cuddle_please_actions::Actions;
|
|
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,
|
|
actions: Actions,
|
|
}
|
|
|
|
impl ReleaseCommandHandler {
|
|
pub fn new(
|
|
ui: DynUi,
|
|
config: PleaseConfig,
|
|
git_client: VcsClient,
|
|
gitea_client: DynRemoteGitClient,
|
|
actions: Actions,
|
|
) -> Self {
|
|
Self {
|
|
ui,
|
|
config,
|
|
git_client,
|
|
gitea_client,
|
|
actions,
|
|
}
|
|
}
|
|
|
|
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)?;
|
|
|
|
self.actions.execute(&next_version)?;
|
|
|
|
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(())
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
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: &Path,
|
|
) -> 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))
|
|
}
|