diff --git a/src/gitea.rs b/src/gitea.rs index acb60e9..b0cb56f 100644 --- a/src/gitea.rs +++ b/src/gitea.rs @@ -1,14 +1,17 @@ use reqwest::Client; +use semver::Version; +use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; #[derive(serde::Deserialize, serde::Serialize)] pub struct GitCommit { - commit: GitCommitDetails, + pub commit: GitCommitDetails, + pub sha: String, } #[derive(serde::Deserialize, serde::Serialize)] pub struct GitCommitDetails { - message: String, + pub message: String, } pub async fn get_pull_request_commits( @@ -47,6 +50,226 @@ pub async fn get_pull_request_commits( Ok(commit_titles) } +#[derive(Serialize, Deserialize, Debug)] +pub struct ReleaseRequest { + pub tag_name: String, + pub target_commitish: String, + pub name: String, + pub body: String, + pub draft: bool, + pub prerelease: bool, +} + +#[derive(Deserialize, Debug)] +pub struct ReleaseResponse { + pub id: u64, + pub url: String, + // Add other fields as needed +} + +pub async fn create_gitea_release( + base_url: &str, + owner: &str, + repo: &str, + access_token: &str, + release_request: &ReleaseRequest, +) -> eyre::Result { + let client = Client::new(); + let releases_url = format!("{}/api/v1/repos/{}/{}/releases", base_url, owner, repo); + + let response = client + .post(&releases_url) + .bearer_auth(access_token) + .json(release_request) + .send() + .await?; + + if !response.status().is_success() { + eyre::bail!("failed to handle request: {}", response.text().await?); + } + + let response = response.json::().await?; + + Ok(response) +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Release { + pub id: u64, + pub tag_name: String, + pub draft: bool, + pub prerelease: bool, // Add other fields as needed + pub target_commitish: String, + pub name: String, + pub body: String, +} + +pub async fn get_newest_release( + base_url: &str, + owner: &str, + repo: &str, + personal_access_token: &str, +) -> eyre::Result { + let client = Client::new(); + let releases_url = format!("{}/api/v1/repos/{}/{}/releases", base_url, owner, repo); + + let response = client + .get(&releases_url) + .bearer_auth(personal_access_token) + .send() + .await?; + + if !response.status().is_success() { + eyre::bail!("failed to handle request: {}", response.text().await?); + } + + let response = response.json::>().await?; + + match response.first() { + Some(release) => Ok(release.clone()), + None => Err(eyre::anyhow!("No releases found")), + } +} + +pub async fn is_newest_release_draft( + gitea_base_url: &str, + owner: &str, + repo: &str, + personal_access_token: &str, +) -> eyre::Result<(bool, Release)> { + let release = get_newest_release(gitea_base_url, owner, repo, personal_access_token).await?; + Ok((release.draft, release)) +} + +#[derive(Serialize, Debug)] +pub struct UpdateReleaseRequest { + pub tag_name: Option, + pub target_commitish: Option, + pub name: Option, + pub body: Option, + pub draft: Option, + pub prerelease: Option, +} + +pub async fn modify_release( + base_url: &str, + owner: &str, + repo: &str, + token: &str, + release_id: u64, + update_request: &UpdateReleaseRequest, +) -> eyre::Result { + tracing::info!( + base_url = base_url, + owner = owner, + repo = repo, + release_id = release_id, + "modifying release" + ); + + let client = Client::new(); + let release_url = format!( + "{}/api/v1/repos/{}/{}/releases/{}", + base_url, owner, repo, release_id + ); + + let response = client + .patch(&release_url) + .bearer_auth(token) + .json(&update_request) + .send() + .await?; + + if !response.status().is_success() { + eyre::bail!("failed to handle request: {}", response.text().await?); + } + + let response = response.json::().await?; + + Ok(response) +} + +#[derive(Deserialize, Debug)] +pub struct Commit { + pub sha: String, +} + +#[derive(Deserialize, Debug)] +pub struct Tag { + pub name: String, + pub commit: Commit, // Add other fields as needed +} + +pub async fn get_highest_semver_from_tags( + base_url: &str, + owner: &str, + repo: &str, + token: &str, +) -> eyre::Result<(Version, Tag)> { + let client = Client::new(); + let tags_url = format!("{}/api/v1/repos/{}/{}/tags", base_url, owner, repo); + + let response = client.get(&tags_url).bearer_auth(token).send().await?; + + if !response.status().is_success() { + eyre::bail!("failed to handle request: {}", response.text().await?); + } + + let response = response.json::>().await?; + + let highest_version = response + .into_iter() + .filter_map(|tag| { + Version::parse(&tag.name.replace("v", "")) + .and_then(|v| Ok((v, tag))) + .ok() + }) + .max_by(|(x, _), (y, _)| x.cmp(y)); + + match highest_version { + Some(version) => Ok(version), + None => Err(eyre::anyhow!("No valid SemVer tags found",)), + } +} + +pub async fn get_commits_from_commit_to_newest( + base_url: &str, + owner: &str, + repo: &str, + token: &str, + branch: &str, + start_commit_sha: &str, +) -> eyre::Result> { + let client = Client::new(); + let commits_url = format!( + "{}/api/v1/repos/{}/{}/commits?sha={}", + base_url, owner, repo, branch + ); + + let response = client.get(&commits_url).bearer_auth(token).send().await?; + + if !response.status().is_success() { + eyre::bail!("failed to handle request: {}", response.text().await?); + } + + //tracing::info!(request = response.text().await?, "test"); + + let mut response = response.json::>().await?; + //let mut response: Vec = Vec::new(); + + let index_of_start_commit = response + .iter() + .position(|commit| commit.sha == start_commit_sha); + + match index_of_start_commit { + Some(index) => { + response.truncate(index); + Ok(response) + } + None => Err(eyre::anyhow!("Start commit not found",)), + } +} + #[cfg(test)] mod tests { use tracing_test::traced_test; diff --git a/src/main.rs b/src/main.rs index 0883c8f..e8c1cf7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,8 @@ mod validate_pr; use clap::Command; use tracing_subscriber::{filter::LevelFilter, EnvFilter}; +use self::gitea::{get_highest_semver_from_tags, ReleaseRequest, UpdateReleaseRequest}; + const VERSION: &'static str = ""; #[tokio::main] @@ -14,15 +16,16 @@ async fn main() -> eyre::Result<()> { } color_eyre::install().unwrap(); - tracing_subscriber::fmt() - .pretty() - .with_env_filter(EnvFilter::default().add_directive(LevelFilter::INFO.into())) - .init(); - let matches = clap::Command::new("releaser") .version(VERSION) .author("kjuulh ") .about("A CLI app for releasing software and managing versions") + .arg( + clap::Arg::new("quiet") + .long("quiet") + .short('q') + .action(clap::ArgAction::SetTrue), + ) .subcommand(Command::new("release").about("Release the software")) .subcommand( Command::new("validate").about("Validate the semantic versioning of the software"), @@ -44,9 +47,44 @@ async fn main() -> eyre::Result<()> { .required(true), ), ) - .subcommand(Command::new("bump").about("Bump the semantic versioning in git tags")) + .subcommand( + Command::new("bump") + .about("Bump the semantic versioning in git tags") + .arg(clap::Arg::new("owner").long("owner").required(true)) + .arg(clap::Arg::new("repo").long("repo").required(true)) + .arg(clap::Arg::new("branch").long("branch").required(true)), + ) + .subcommand( + Command::new("release:gitea") + .about("Create a release on gitea") + .arg(clap::Arg::new("owner").long("owner").required(true)) + .arg(clap::Arg::new("repo").long("repo").required(true)) + .arg(clap::Arg::new("version").long("version").required(true)) + .arg(clap::Arg::new("branch").long("branch").required(true)) + .arg( + clap::Arg::new("generate-changelog") + .long("generate-changelog") + .action(clap::ArgAction::SetTrue), + ), + ) .get_matches(); + let quiet = matches.get_one::("quiet"); + match quiet { + Some(true) => {} + _ => { + tracing_subscriber::fmt() + .pretty() + .with_env_filter( + EnvFilter::from_default_env().add_directive(LevelFilter::INFO.into()), + ) + .init(); + } + } + + let base_url = &std::env::var("GITEA_BASE_URL").unwrap(); + let token = &std::env::var("GITEA_ACCESS_TOKEN").unwrap(); + match matches.subcommand() { Some(("release", sub_matches)) => {} Some(("validate", sub_matches)) => {} @@ -60,12 +98,99 @@ async fn main() -> eyre::Result<()> { .await? { Some(version) => { - tracing::info!(version = version, "bumping version") + tracing::info!(version = version, "bumping version"); + + print!("{}\n", version) } None => tracing::info!(version = current_version, "no version bump required!"), } } - Some(("bump", sub_matches)) => {} + Some(("bump", sub_matches)) => { + let owner = sub_matches.get_one::("owner").unwrap(); + let repo = sub_matches.get_one::("repo").unwrap(); + let branch = sub_matches.get_one::("branch").unwrap(); + let default_version = "0.0.0"; + + let version = match get_highest_semver_from_tags(base_url, owner, repo, token).await { + Ok((version, tag)) => { + tracing::debug!(version = version.to_string(), "got tag from repo"); + + match validate_pr::validate_commits( + &owner, + &repo, + &tag.commit.sha, + branch, + &version.to_string(), + ) + .await? + { + Some(version) => { + tracing::info!(version = version, "bumping version"); + + print!("{}\n", version) + } + None => { + tracing::info!(version = default_version, "no version bump required!") + } + } + + version.to_string() + } + _ => default_version.to_string(), + }; + } + Some(("release:gitea", sub_matches)) => { + let default_generate_changelog = false; + let owner = sub_matches.get_one::("owner").unwrap(); + let repo = sub_matches.get_one::("repo").unwrap(); + let version = sub_matches.get_one::("version").unwrap(); + let branch = sub_matches.get_one::("branch").unwrap(); + let generate_changelog = sub_matches + .get_one::("generate-changelog") + .unwrap_or(&default_generate_changelog); + + match crate::gitea::is_newest_release_draft(base_url, owner, repo, token).await { + Ok((true, release)) => { + let resp = crate::gitea::modify_release( + base_url, + owner, + repo, + token, + release.id, + &UpdateReleaseRequest { + body: None, + draft: Some(release.draft), + tag_name: Some(version.clone()), + name: Some(version.clone()), + prerelease: None, + target_commitish: Some(branch.clone()), + }, + ) + .await?; + + tracing::info!("updated release") + } + _ => { + let resp = crate::gitea::create_gitea_release( + base_url, + owner, + repo, + token, + &ReleaseRequest { + tag_name: version.clone(), + target_commitish: branch.clone(), + name: version.clone(), + body: "".into(), + draft: true, + prerelease: false, + }, + ) + .await?; + + tracing::info!(url = resp.url, "created release") + } + } + } _ => unreachable!(), } diff --git a/src/validate_pr.rs b/src/validate_pr.rs index 227be6f..e25139a 100644 --- a/src/validate_pr.rs +++ b/src/validate_pr.rs @@ -1,3 +1,5 @@ +use crate::gitea; + pub async fn validate_pr( owner: &str, repo: &str, @@ -16,3 +18,34 @@ pub async fn validate_pr( None => Ok(None), } } + +pub(crate) async fn validate_commits( + owner: &str, + repo: &str, + start_commit: &str, + branch: &str, + current_version: &str, +) -> eyre::Result> { + let base_url = &std::env::var("GITEA_BASE_URL").unwrap(); + let token = &std::env::var("GITEA_ACCESS_TOKEN").unwrap(); + + let commits = crate::gitea::get_commits_from_commit_to_newest( + base_url, + owner, + repo, + token, + branch, + start_commit, + ) + .await?; + + let commit_titles = commits + .into_iter() + .map(|c| c.commit.message) + .collect::>(); + + match crate::semantic::get_most_significant_bump(&commit_titles) { + Some(bump) => Ok(Some(crate::semantic::bump_semver(current_version, bump)?)), + None => Ok(None), + } +}