258 lines
8.3 KiB
Rust
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))
|
|
}
|