Skip to content

Commit

Permalink
transform history segments into changelog parts (#198)
Browse files Browse the repository at this point in the history
  • Loading branch information
Byron committed Sep 23, 2021
1 parent 40e9075 commit 348b05c
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 41 deletions.
54 changes: 46 additions & 8 deletions 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<time::OffsetDateTime>,
},
}

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
}),
}
Expand Down
6 changes: 4 additions & 2 deletions 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<String>) -> anyhow::Result<()> {
let ctx = crate::Context::new(crates)?;
Expand All @@ -13,8 +13,10 @@ pub fn changelog(opts: Options, crates: Vec<String>) -> 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,
);
}
Expand Down
2 changes: 1 addition & 1 deletion cargo-smart-release/src/commit/history.rs
Expand Up @@ -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>,
}
Expand Down
44 changes: 25 additions & 19 deletions cargo-smart-release/src/git.rs
Expand Up @@ -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};

Expand Down Expand Up @@ -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<Option<commit::History>> {
Expand Down Expand Up @@ -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<Vec<commit::history::Segment<'h>>> {
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 = {
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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()
Expand All @@ -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![],
};

Expand Down Expand Up @@ -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],
},
)),
Expand All @@ -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())
Expand All @@ -297,7 +302,7 @@ pub mod history {
let num_commits = segments.iter().map(|s| s.history.len()).sum::<usize>();
log::trace!(
"{}: Found {} relevant commits out of {} in {} segments {}s ({:.0} commits/s)",
crate_name,
package.name,
num_commits,
history.items.len(),
segments.len(),
Expand All @@ -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())
}
2 changes: 1 addition & 1 deletion cargo-smart-release/src/lib.rs
@@ -1,5 +1,5 @@
pub struct ChangeLog {
_segments: Vec<changelog::Segment>,
_segments: Vec<changelog::Section>,
}

pub mod changelog;
Expand Down
68 changes: 58 additions & 10 deletions cargo-smart-release/src/utils.rs
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<Version> {
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<Version> {
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] {
Expand All @@ -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;
Expand Down Expand Up @@ -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()));
Expand Down

0 comments on commit 348b05c

Please sign in to comment.