feat: with all the way through

Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
Kasper Juul Hermansen 2023-07-31 13:27:55 +02:00
parent 86eabad6fe
commit df96de1cd0
Signed by: kjuulh
GPG Key ID: 9AA7BC13CE474394
4 changed files with 199 additions and 31 deletions

View File

@ -20,10 +20,13 @@ impl ChangeLogBuilder {
pub fn new<C>(commits: C, version: impl Into<String>) -> Self
where
C: IntoIterator,
C::Item: Into<String>,
C::Item: AsRef<str>,
{
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<Option<String>> {
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<Option<String>> {
last_changes_from_str(changelog)
}
pub fn last_changes_from_str(changelog: &str) -> anyhow::Result<Option<String>> {

View File

@ -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<PathBuf>,
/// which branch is being run from
#[arg(long, global = true, help_heading = "Global")]
branch: Option<String>,
/// 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(&current_dir, stdin)?;
let _git_client = self.get_git(&current_dir)?;
let config = self.get_config(&current_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(&current_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::<Vec<&str>>();
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)]

View File

@ -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<String>, repo: impl Into<String>) -> 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<String>,
repo: impl Into<String>,
since_sha: impl Into<String>,
since_sha: Option<impl Into<String>>,
branch: impl Into<String>,
) -> anyhow::Result<Vec<Commit>> {
let get_commits_since_page = |owner: &str,
@ -147,7 +154,7 @@ impl GiteaClient {
&self,
owner: impl Into<String>,
repo: impl Into<String>,
since_sha: impl Into<String>,
since_sha: Option<impl Into<String>>,
branch: impl Into<String>,
get_commits: F,
) -> anyhow::Result<Vec<Commit>>
@ -159,20 +166,24 @@ impl GiteaClient {
let owner: String = owner.into();
let repo: String = repo.into();
let since_sha: String = since_sha.into();
let since_sha: Option<String> = 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) {
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);
}
}
if !has_more {
@ -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<Tag> for Version {
type Error = anyhow::Error;
fn try_from(value: Tag) -> Result<Self, Self::Error> {
tracing::trace!(name = &value.name, "parsing tag into version");
value
.name
.parse::<Version>()
.context("could not get version from tag")
}
}
impl TryFrom<&Tag> for Version {
type Error = anyhow::Error;
fn try_from(value: &Tag) -> Result<Self, Self::Error> {
tracing::trace!(name = &value.name, "parsing tag into version");
value
.name
.parse::<Version>()
.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<Commit>, bool)> {
let commit_page = api_res.get(page - 1).unwrap();

View File

@ -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)]