From 348b05cbe6e93e871393a6db9d1ebfea59ec7fdb Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 23 Sep 2021 13:52:59 +0800 Subject: [PATCH] transform history segments into changelog parts (#198) --- cargo-smart-release/src/changelog.rs | 54 ++++++++++++--- .../src/command/changelog/mod.rs | 6 +- cargo-smart-release/src/commit/history.rs | 2 +- cargo-smart-release/src/git.rs | 44 ++++++------ cargo-smart-release/src/lib.rs | 2 +- cargo-smart-release/src/utils.rs | 68 ++++++++++++++++--- 6 files changed, 135 insertions(+), 41 deletions(-) diff --git a/cargo-smart-release/src/changelog.rs b/cargo-smart-release/src/changelog.rs index f4419c458d..e395639fbb 100644 --- a/cargo-smart-release/src/changelog.rs +++ b/cargo-smart-release/src/changelog.rs @@ -1,30 +1,68 @@ -use crate::{commit, ChangeLog}; +use cargo_metadata::Package; use git_repository as git; -pub enum Segment { +use crate::{commit, utils, utils::is_top_level_package, ChangeLog}; +use git_repository::prelude::ObjectIdExt; + +pub enum Section { /// A part of a changelog which couldn't be understood and is taken in verbatim. This is usually the pre-amble of the changelog /// or a custom footer. Verbatim(String), /// A segment describing a particular release - Release { name: Version, date: time::Date }, + Release { + name: Version, + date: Option, + }, } pub enum Version { Unreleased, Semantic(semver::Version), } -impl Segment { - pub fn from_history_segment(_segment: &commit::history::Segment<'_>, _repo: &git::Easy) -> Self { - todo!("segment from history item") +impl Section { + pub fn from_history_segment(package: &Package, segment: &commit::history::Segment<'_>, repo: &git::Easy) -> Self { + let package_name = (!is_top_level_package(&package.manifest_path, repo)).then(|| package.name.as_str()); + + let version = crate::git::try_strip_tag_path(segment.head.name.to_ref()) + .map(|tag_name| { + Version::Semantic( + utils::parse_possibly_prefixed_tag_version(package_name, tag_name) + .expect("here we always have a valid version as it passed a filter"), + ) + }) + .unwrap_or_else(|| Version::Unreleased); + + let time = segment + .head + .peeled + .expect("all refs here are peeled") + .attach(repo) + .object() + .expect("object exists") + .commit() + .expect("target is a commit") + .committer + .time; + let date_time = time::OffsetDateTime::from_unix_timestamp(time.time as i64) + .expect("always valid unix time") + .replace_offset(time::UtcOffset::from_whole_seconds(time.offset).expect("valid offset")); + Section::Release { + name: version, + date: date_time.into(), + } } } impl ChangeLog { - pub fn from_history_segments(segments: &[commit::history::Segment<'_>], repo: &git::Easy) -> Self { + pub fn from_history_segments( + package: &Package, + segments: &[commit::history::Segment<'_>], + repo: &git::Easy, + ) -> Self { ChangeLog { _segments: segments.iter().fold(Vec::new(), |mut acc, item| { - acc.push(Segment::from_history_segment(item, repo)); + acc.push(Section::from_history_segment(package, item, repo)); acc }), } diff --git a/cargo-smart-release/src/command/changelog/mod.rs b/cargo-smart-release/src/command/changelog/mod.rs index 94ad8e8ad8..9b2e6676d1 100644 --- a/cargo-smart-release/src/command/changelog/mod.rs +++ b/cargo-smart-release/src/command/changelog/mod.rs @@ -1,4 +1,4 @@ -use crate::{command::changelog::Options, git, ChangeLog}; +use crate::{command::changelog::Options, git, utils::package_by_name, ChangeLog}; pub fn changelog(opts: Options, crates: Vec) -> anyhow::Result<()> { let ctx = crate::Context::new(crates)?; @@ -13,8 +13,10 @@ pub fn changelog(opts: Options, crates: Vec) -> anyhow::Result<()> { Some(history) => history, }; for crate_name in &crate_names { + let package = package_by_name(&ctx.meta, crate_name)?; let _log = ChangeLog::from_history_segments( - &git::history::crate_ref_segments(crate_name, &ctx, &history)?, + package, + &git::history::crate_ref_segments(package, &ctx, &history)?, &ctx.repo, ); } diff --git a/cargo-smart-release/src/commit/history.rs b/cargo-smart-release/src/commit/history.rs index 6ecc97a3b3..19b1459064 100644 --- a/cargo-smart-release/src/commit/history.rs +++ b/cargo-smart-release/src/commit/history.rs @@ -4,7 +4,7 @@ use crate::commit::Message; /// A head reference will all commits that are 'governed' by it, that is are in its exclusive ancestry. pub struct Segment<'a> { - pub _head: git::refs::Reference, + pub head: git::refs::Reference, /// only relevant history items, that is those that change code in the respective crate. pub history: Vec<&'a Item>, } diff --git a/cargo-smart-release/src/git.rs b/cargo-smart-release/src/git.rs index 1a0cbaca08..43636faccc 100644 --- a/cargo-smart-release/src/git.rs +++ b/cargo-smart-release/src/git.rs @@ -2,7 +2,12 @@ use std::process::Command; use anyhow::{anyhow, bail}; use cargo_metadata::Package; -use git_repository::{bstr::ByteSlice, easy::object, prelude::ReferenceAccessExt}; +use git_repository::{ + bstr::{BStr, ByteSlice}, + easy::object, + prelude::ReferenceAccessExt, + refs::FullNameRef, +}; use crate::utils::{component_to_bytes, tag_name}; @@ -83,16 +88,18 @@ pub mod history { use std::{cell::RefCell, collections::BTreeMap, iter::FromIterator, path::PathBuf, time::Instant}; use anyhow::bail; + use cargo_metadata::Package; use git_repository as git; use git_repository::{ - bstr::{BStr, ByteSlice}, + bstr::ByteSlice, easy::head, prelude::{CacheAccessExt, ObjectAccessExt, ReferenceAccessExt, ReferenceExt}, }; use crate::{ commit, - utils::{component_to_bytes, is_tag_name, is_tag_version, package_by_name, tag_prefix}, + git::strip_tag_path, + utils::{component_to_bytes, is_tag_name, is_tag_version, tag_prefix}, }; pub fn collect(repo: &git::Easy) -> anyhow::Result> { @@ -146,12 +153,10 @@ pub mod history { /// Return the head reference followed by all tags affecting `crate_name` as per our tag name rules, ordered by ancestry. pub fn crate_ref_segments<'h>( - crate_name: &str, + package: &Package, ctx: &crate::Context, history: &'h commit::History, ) -> anyhow::Result>> { - let meta = &ctx.meta; - let package = package_by_name(meta, crate_name)?; let tag_prefix = tag_prefix(package, &ctx.repo); let start = Instant::now(); let mut tags_by_commit = { @@ -161,7 +166,7 @@ pub mod history { refs.prefixed(PathBuf::from(format!("refs/tags/{}-", prefix)))? .peeled() .filter_map(|r| r.ok().map(|r| r.detach())) - .filter(|r| is_tag_name(prefix, strip_tag_path(r.name.as_bstr()))) + .filter(|r| is_tag_name(prefix, strip_tag_path(r.name.to_ref()))) .map(|r| { let t = r.peeled.expect("already peeled"); (t, r) @@ -171,7 +176,7 @@ pub mod history { refs.prefixed("refs/tags")? .peeled() .filter_map(|r| r.ok().map(|r| r.detach())) - .filter(|r| is_tag_version(strip_tag_path(r.name.as_bstr()))) + .filter(|r| is_tag_version(strip_tag_path(r.name.to_ref()))) .map(|r| { let t = r.peeled.expect("already peeled"); (t, r) @@ -183,7 +188,7 @@ pub mod history { let elapsed = start.elapsed(); log::trace!( "{}: Mapped {} tags in {}s ({:.0} refs/s)", - crate_name, + package.name, tags_by_commit.len(), elapsed.as_secs_f32(), tags_by_commit.len() as f32 / elapsed.as_secs_f32() @@ -192,7 +197,7 @@ pub mod history { let start = Instant::now(); let mut segments = Vec::new(); let mut segment = commit::history::Segment { - _head: history.head.to_owned(), + head: history.head.to_owned(), history: vec![], }; @@ -273,7 +278,7 @@ pub mod history { Some(next_ref) => segments.push(std::mem::replace( &mut segment, commit::history::Segment { - _head: next_ref, + head: next_ref, history: vec![item], }, )), @@ -284,7 +289,7 @@ pub mod history { if !tags_by_commit.is_empty() { log::warn!( "{}: The following tags were on branches which are ignored during traversal: {}", - crate_name, + package.name, tags_by_commit .into_values() .map(|v| v.name.as_bstr().to_str_lossy().into_owned()) @@ -297,7 +302,7 @@ pub mod history { let num_commits = segments.iter().map(|s| s.history.len()).sum::(); log::trace!( "{}: Found {} relevant commits out of {} in {} segments {}s ({:.0} commits/s)", - crate_name, + package.name, num_commits, history.items.len(), segments.len(), @@ -307,11 +312,12 @@ pub mod history { Ok(segments) } +} - fn strip_tag_path(fullname: &BStr) -> &BStr { - fullname - .strip_prefix(b"refs/tags/") - .expect("prefix iteration works") - .as_bstr() - } +pub fn strip_tag_path(name: FullNameRef<'_>) -> &BStr { + try_strip_tag_path(name).expect("prefix iteration works") +} + +pub fn try_strip_tag_path(name: FullNameRef<'_>) -> Option<&BStr> { + name.as_bstr().strip_prefix(b"refs/tags/").map(|b| b.as_bstr()) } diff --git a/cargo-smart-release/src/lib.rs b/cargo-smart-release/src/lib.rs index a47f0d736c..6395e4a7f2 100644 --- a/cargo-smart-release/src/lib.rs +++ b/cargo-smart-release/src/lib.rs @@ -1,5 +1,5 @@ pub struct ChangeLog { - _segments: Vec, + _segments: Vec, } pub mod changelog; diff --git a/cargo-smart-release/src/utils.rs b/cargo-smart-release/src/utils.rs index 0f0636ee61..c145f1207e 100644 --- a/cargo-smart-release/src/utils.rs +++ b/cargo-smart-release/src/utils.rs @@ -4,6 +4,7 @@ use cargo_metadata::{ Dependency, Metadata, Package, PackageId, }; use git_repository as git; +use git_repository::bstr::{BStr, ByteSlice}; use semver::Version; pub fn will(not_really: bool) -> &'static str { @@ -98,20 +99,34 @@ fn tag_name_inner(package_name: Option<&str>, version: &str) -> String { } } +pub fn parse_possibly_prefixed_tag_version(package_name: Option<&str>, tag_name: &BStr) -> Option { + match package_name { + Some(name) => tag_name + .strip_prefix(name.as_bytes()) + .and_then(|r| r.strip_prefix(b"-")) + .and_then(|possibly_version| parse_tag_version(possibly_version.as_bstr())), + None => parse_tag_version(tag_name), + } +} + +pub fn parse_tag_version(name: &BStr) -> Option { + name.strip_prefix(b"v") + .and_then(|v| v.to_str().ok()) + .and_then(|v| Version::parse(v).ok()) +} + pub fn is_tag_name(package_name: &str, tag_name: &git::bstr::BStr) -> bool { - use git::bstr::ByteSlice; match tag_name .strip_prefix(package_name.as_bytes()) .and_then(|r| r.strip_prefix(b"-")) { None => false, - Some(possibly_version) => is_tag_version(possibly_version.as_bstr()), + Some(possibly_version) => parse_tag_version(possibly_version.as_bstr()).is_some(), } } pub fn is_tag_version(name: &git::bstr::BStr) -> bool { - use git::bstr::ByteSlice; - name.starts_with_str(b"v") && name.split_str(b".").count() >= 3 + parse_tag_version(name).is_some() } pub fn component_to_bytes(c: Utf8Component<'_>) -> &[u8] { @@ -123,6 +138,39 @@ pub fn component_to_bytes(c: Utf8Component<'_>) -> &[u8] { #[cfg(test)] mod tests { + mod parse_possibly_prefixed_tag_version { + mod matches { + use git_repository::bstr::ByteSlice; + use semver::Version; + + use crate::utils::{parse_possibly_prefixed_tag_version, tag_name_inner}; + + #[test] + fn whatever_tag_name_would_return() { + assert_eq!( + parse_possibly_prefixed_tag_version( + "git-test".into(), + tag_name_inner("git-test".into(), "1.0.1").as_bytes().as_bstr() + ), + Version::parse("1.0.1").expect("valid").into() + ); + + assert_eq!( + parse_possibly_prefixed_tag_version( + "single".into(), + tag_name_inner("single".into(), "0.0.1-beta.1").as_bytes().as_bstr() + ), + Version::parse("0.0.1-beta.1").expect("valid").into() + ); + + assert_eq!( + parse_possibly_prefixed_tag_version(None, tag_name_inner(None, "0.0.1+123.x").as_bytes().as_bstr()), + Version::parse("0.0.1+123.x").expect("valid").into() + ); + } + } + } + mod is_tag_name { mod no_match { use git_repository::bstr::ByteSlice; @@ -170,18 +218,18 @@ mod tests { fn invalid_prefix() { assert!(!is_tag_version(b"x0.0.1".as_bstr())); } + + #[test] + fn funky() { + assert!(!is_tag_version(b"vers0.0.1".as_bstr())); + assert!(!is_tag_version(b"vHi.Ho.yada-anythingreally".as_bstr())); + } } mod matches { use git_repository::bstr::ByteSlice; use crate::utils::is_tag_version; - #[test] - fn funky() { - assert!(is_tag_version(b"vers0.0.1".as_bstr())); - assert!(is_tag_version(b"vHi.Ho.yada-anythingreally".as_bstr())); - } - #[test] fn pre_release() { assert!(is_tag_version(b"v0.0.1".as_bstr()));