diff --git a/crates/cuddle-please/src/cliff/mod.rs b/crates/cuddle-please/src/cliff/mod.rs index badfb85..7e17d92 100644 --- a/crates/cuddle-please/src/cliff/mod.rs +++ b/crates/cuddle-please/src/cliff/mod.rs @@ -20,10 +20,13 @@ impl ChangeLogBuilder { pub fn new(commits: C, version: impl Into) -> Self where C: IntoIterator, - C::Item: Into, + C::Item: AsRef, { Self { - commits: commits.into_iter().map(|s| s.into()).collect(), + commits: commits + .into_iter() + .map(|s| s.as_ref().to_string()) + .collect(), version: version.into(), config: None, release_date: None, @@ -236,7 +239,7 @@ fn default_changelog_body_config(release_link: Option<&str>) -> String { } } -mod changelog_parser { +pub mod changelog_parser { use std::{fs::read_to_string, path::Path}; use anyhow::Context; @@ -264,9 +267,8 @@ mod changelog_parser { None } - pub fn last_changes(changelog: &Path) -> anyhow::Result> { - let changelog = read_to_string(changelog).context("can't read changelog file")?; - last_changes_from_str(&changelog) + pub fn last_changes(changelog: &str) -> anyhow::Result> { + last_changes_from_str(changelog) } pub fn last_changes_from_str(changelog: &str) -> anyhow::Result> { diff --git a/crates/cuddle-please/src/command.rs b/crates/cuddle-please/src/command.rs index e0e7d2d..0c193a6 100644 --- a/crates/cuddle-please/src/command.rs +++ b/crates/cuddle-please/src/command.rs @@ -5,16 +5,22 @@ use std::{ sync::{Arc, Mutex}, }; +use ::semver::Version; use anyhow::Context; use clap::{Args, Parser, Subcommand}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use crate::{ + cliff::{self, changelog_parser}, environment::get_from_environment, git_client::VcsClient, - gitea_client::GiteaClient, + gitea_client::{GiteaClient, Tag}, ui::{ConsoleUi, DynUi}, - versioning::semver::get_most_significant_version, + versioning::{ + conventional_parse::VersionIncrement, + next_version::NextVersion, + semver::{self, get_most_significant_version}, + }, }; #[derive(Parser)] @@ -61,6 +67,14 @@ struct GlobalArgs { #[arg(long, global = true, help_heading = "Global")] source: Option, + /// which branch is being run from + #[arg(long, global = true, help_heading = "Global")] + branch: Option, + + /// whether to run in dry run mode (i.e. no pushes or releases) + #[arg(long, global = true, help_heading = "Global")] + dry_run: bool, + /// no version control system, forces please to allow no .git/ or friends #[arg( long, @@ -211,7 +225,7 @@ impl Command { let commits = client.get_commits_since( self.global.owner.unwrap(), self.global.repo.unwrap(), - sha, + Some(sha), branch, )?; self.ui.write_str_ln("got commits from gitea"); @@ -258,23 +272,127 @@ impl Command { // 2. Parse the cuddle.please.yaml let cuddle.please.yaml take precedence // 2a. if not existing use default. // 2b. if not in a git repo abort. (unless --no-vcs is turned added) - let _config = self.get_config(¤t_dir, stdin)?; - let _git_client = self.get_git(¤t_dir)?; + let config = self.get_config(¤t_dir, stdin)?; + + let owner = self.global.owner.as_ref().expect("owner to be set"); + let repo = self.global.repo.as_ref().expect("repo to be set"); + let branch = self.global.branch.as_ref().expect("branch to be set"); + + let git_client = self.get_git(¤t_dir)?; // 3. Create gitea client and do a health check + let gitea_client = self.get_gitea_client(); + gitea_client + .connect(owner, repo) + .context("failed to connect to gitea repository")?; // 4. Fetch git tags for the current repository - // 5. Fetch git commits since last git tag + let tags = gitea_client.get_tags(owner, repo)?; - // 6. Slice commits since last git tag + let significant_tag = get_most_significant_version(tags.iter().collect()); + + // 5. Fetch git commits since last git tag + let commits = gitea_client.get_commits_since( + owner, + repo, + significant_tag.map(|st| st.commit.sha.clone()), + branch, + )?; // 7. Create a versioning client + let current_version = significant_tag + .map(|st| Version::try_from(st).unwrap()) + .unwrap_or(Version::new(0, 1, 0)); + // 8. Parse conventional commits and determine next version - // 9a. Check for open pr. - // 10a. If exists parse history, rebase from master and rewrite pr + let commit_strs = commits + .iter() + .map(|c| c.commit.message.as_str()) + .collect::>(); + + if commit_strs.is_empty() { + tracing::info!("no commits to base release on"); + return Ok(()); + } + + let next_version = current_version.next(&commit_strs); + + // Compose changelog + let builder = + cliff::ChangeLogBuilder::new(&commit_strs, next_version.to_string()).build(); + + let changelog_placement = self + .global + .source + .as_ref() + .map(|s| s.join("CHANGELOG.md")) + .unwrap_or(PathBuf::from("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)?; // 9b. check for release commit and release, if release exists continue // 10b. create release + if let Some(first_commit) = commit_strs.first() { + if first_commit.contains("chore(release): ") { + if !self.global.dry_run { + gitea_client.create_release( + owner, + repo, + next_version.to_string(), + changelog_last_changes.unwrap(), + !next_version.pre.is_empty(), + )?; + } else { + tracing::debug!("creating release (dry_run)"); + } + + return Ok(()); + } + } + + // 9a. Create / Update Pr + // Create or update branch + git_client.checkout_branch()?; + + std::fs::write(changelog_placement, changelog.as_bytes())?; + + git_client.commit_and_push(next_version.to_string(), self.global.dry_run)?; + + let pr_number = match gitea_client.get_pull_request(owner, repo)? { + Some(existing_pr) => { + if !self.global.dry_run { + gitea_client.update_pull_request( + owner, + repo, + next_version.to_string(), + changelog_last_changes.unwrap(), + existing_pr, + )? + } else { + tracing::debug!("updating pull request (dry_run)"); + 1 + } + } + None => { + if !self.global.dry_run { + gitea_client.create_pull_request( + owner, + repo, + next_version.to_string(), + changelog, + self.global.branch.clone().unwrap(), + )? + } else { + tracing::debug!("creating pull request (dry_run)"); + 1 + } + } + }; } } @@ -295,6 +413,13 @@ impl Command { Ok(()) } + + fn get_gitea_client(&self) -> GiteaClient { + GiteaClient::new( + self.global.api_url.clone().expect("api_url to be set"), + self.global.token.clone(), + ) + } } #[derive(Debug, Clone, Subcommand)] diff --git a/crates/cuddle-please/src/gitea_client.rs b/crates/cuddle-please/src/gitea_client.rs index f14304a..5c24380 100644 --- a/crates/cuddle-please/src/gitea_client.rs +++ b/crates/cuddle-please/src/gitea_client.rs @@ -1,4 +1,6 @@ +use anyhow::Context; use reqwest::header::{HeaderMap, HeaderValue}; +use semver::Version; use serde::{Deserialize, Serialize}; #[allow(dead_code)] @@ -42,12 +44,17 @@ impl GiteaClient { pub fn connect(&self, owner: impl Into, repo: impl Into) -> anyhow::Result<()> { let client = self.create_client()?; + let owner = owner.into(); + let repo = repo.into(); + + tracing::trace!(owner = &owner, repo = &repo, "gitea connect"); + let request = client .get(format!( "{}/api/v1/repos/{}/{}", &self.url.trim_end_matches("/"), - owner.into(), - repo.into() + owner, + repo )) .build()?; @@ -91,7 +98,7 @@ impl GiteaClient { &self, owner: impl Into, repo: impl Into, - since_sha: impl Into, + since_sha: Option>, branch: impl Into, ) -> anyhow::Result> { let get_commits_since_page = |owner: &str, @@ -147,7 +154,7 @@ impl GiteaClient { &self, owner: impl Into, repo: impl Into, - since_sha: impl Into, + since_sha: Option>, branch: impl Into, get_commits: F, ) -> anyhow::Result> @@ -159,19 +166,23 @@ impl GiteaClient { let owner: String = owner.into(); let repo: String = repo.into(); - let since_sha: String = since_sha.into(); + let since_sha: Option = since_sha.map(|ss| ss.into()); let branch: String = branch.into(); let mut found_commit = false; loop { let (new_commits, has_more) = get_commits(&owner, &repo, &branch, page)?; for commit in new_commits { - if commit.sha.contains(&since_sha) { - found_commit = true; - } else { - if !found_commit { - commits.push(commit); + if let Some(since_sha) = &since_sha { + if commit.sha.contains(since_sha) { + found_commit = true; + } else { + if !found_commit { + commits.push(commit); + } } + } else { + commits.push(commit); } } @@ -181,10 +192,10 @@ impl GiteaClient { page += 1; } - if found_commit == false { + if found_commit == false && since_sha.is_some() { return Err(anyhow::anyhow!( "sha was not found in commit chain: {} on branch: {}", - since_sha, + since_sha.unwrap_or("".into()), branch )); } @@ -355,10 +366,11 @@ impl GiteaClient { ); let request = client .patch(format!( - "{}/api/v1/repos/{}/{}/pulls", + "{}/api/v1/repos/{}/{}/pulls/{}", &self.url.trim_end_matches("/"), owner, repo, + index )) .json(&request) .build()?; @@ -481,6 +493,29 @@ pub struct Tag { pub commit: TagCommit, } +impl TryFrom for Version { + type Error = anyhow::Error; + + fn try_from(value: Tag) -> Result { + tracing::trace!(name = &value.name, "parsing tag into version"); + value + .name + .parse::() + .context("could not get version from tag") + } +} +impl TryFrom<&Tag> for Version { + type Error = anyhow::Error; + + fn try_from(value: &Tag) -> Result { + tracing::trace!(name = &value.name, "parsing tag into version"); + value + .name + .parse::() + .context("could not get version from tag") + } +} + #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] pub struct TagCommit { pub created: String, @@ -531,7 +566,7 @@ mod test { let commits = client.get_commits_since_inner( "owner", "repo", - sha, + Some(sha), "some-branch", |_, _, _, page| -> anyhow::Result<(Vec, bool)> { let commit_page = api_res.get(page - 1).unwrap(); diff --git a/crates/cuddle-please/src/versioning/semver.rs b/crates/cuddle-please/src/versioning/semver.rs index b6ac7d1..8e3b3de 100644 --- a/crates/cuddle-please/src/versioning/semver.rs +++ b/crates/cuddle-please/src/versioning/semver.rs @@ -1,6 +1,6 @@ use std::cmp::Reverse; -use crate::gitea_client::{Tag}; +use crate::gitea_client::Tag; use semver::Version; pub fn get_most_significant_version<'a>(tags: Vec<&'a Tag>) -> Option<&'a Tag> { @@ -16,7 +16,13 @@ pub fn get_most_significant_version<'a>(tags: Vec<&'a Tag>) -> Option<&'a Tag> { .collect(); versions.sort_unstable_by_key(|(_, version)| Reverse(version.clone())); - versions.first().map(|(tag, _)| *tag) + let tag = versions.first().map(|(tag, _)| *tag); + + if let Some(tag) = tag { + tracing::trace!(name = &tag.name, "found most significant tag with version"); + } + + tag } #[cfg(test)]