Skip to content

Commit

Permalink
feat(changelog): add custom template
Browse files Browse the repository at this point in the history
  • Loading branch information
Paul Delafosse authored and oknozor committed Nov 30, 2021
1 parent 9c52c23 commit ad2bcd2
Show file tree
Hide file tree
Showing 10 changed files with 236 additions and 66 deletions.
8 changes: 4 additions & 4 deletions cog.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,14 @@ post_bump_hooks = [
ex = { changelog_title = "This is the markdown title for `ex` commit type" }

[changelog]
github = true
# Optional changelog path ("CHANGELOG.md" if omitted)
path = "CHANGELOG.md"
template = "remote"
remote = "github.com"
repository = "cocogitto"
owner = "oknozor"
# Optional contributor list
# intended to map git signature to github username
# and generate changelog links to their github profiles
# intended to map git signature to remote username
# and generate changelog links to their remote profiles
authors = [
{ signature = "Paul Delafosse", username = "oknozor" },
{ signature = "Jack Dorland", username = "jackdorland" },
Expand Down
55 changes: 52 additions & 3 deletions src/bin/cog.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#![cfg(not(tarpaulin_include))]
use std::path::PathBuf;

use cocogitto::conventional::changelog::template::{RemoteContext, Template};
use cocogitto::conventional::commit;
use cocogitto::conventional::version::VersionIncrement;
use cocogitto::git::hook::HookKind;
Expand Down Expand Up @@ -85,6 +86,7 @@ enum Cli {
message: String,
},

// FIXME : pass revpec pattern instead of from, to
/// Display a changelog for the given commit oid range
#[structopt(no_version, settings = SUBCOMMAND_SETTINGS)]
Changelog {
Expand All @@ -99,6 +101,24 @@ enum Cli {
/// Generate the changelog for a specific git tag
#[structopt(short, long)]
at: Option<String>,

/// Generate the changelog with the given template.
/// Possible values are 'remote', 'default' or the path to your template.
/// If not specified cog will use cog.toml template config or fallback to 'default'.
#[structopt(name = "template", long, short = "p")]
template: Option<String>,

/// Url to use during template generation
#[structopt(name = "remote", long, required_if("template", "remote"))]
remote: Option<String>,

/// Repository owner to use during template generation
#[structopt(name = "owner", long, required_if("template", "remote"))]
owner: Option<String>,

/// Name of the repository used during template generation
#[structopt(name = "repository", long, required_if("template", "remote"))]
repository: Option<String>,
},

/// Commit changelog from latest tag to HEAD and create new tag
Expand Down Expand Up @@ -254,13 +274,42 @@ fn main() -> Result<()> {
.write_all(content.as_bytes())
.context("failed to write log into the pager")?;
}
Cli::Changelog { from, to, at } => {
Cli::Changelog {
from,
to,
at,
template,
remote,
owner,
repository,
} => {
let cocogitto = CocoGitto::get()?;

// Get a template either from arg or from config
let template = match template {
None => SETTINGS.to_changelog_template(),
Some(template) => {
let context = if template == "remote" {
let remote = remote.expect("'remote' should be set for remote template");
let repository =
repository.expect("'repository' should be set for remote template");
let owner = owner.expect("'owner' should be set for remote template");
Some(RemoteContext::new(remote, repository, owner))
} else {
None
};

Some(Template::from_arg(&template, context)?)
}
};

let template = template.unwrap_or_default();

let result = match at {
Some(at) => cocogitto.get_changelog_at_tag(&at)?,
Some(at) => cocogitto.get_changelog_at_tag(&at, template)?,
None => {
let changelog = cocogitto.get_changelog(from.as_deref(), to.as_deref())?;
changelog.to_markdown(cocogitto::settings::renderer())?
changelog.to_markdown(template)?
}
};
println!("{}", result);
Expand Down
8 changes: 6 additions & 2 deletions src/conventional/changelog/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use crate::conventional::changelog::release::Release;
use crate::conventional::changelog::renderer::Renderer;

use crate::conventional::changelog::template::Template;
use std::fs;
use std::path::Path;

pub(crate) mod release;
pub(crate) mod renderer;
pub(crate) mod serde;
pub mod template;

const DEFAULT_HEADER: &str =
"# Changelog\nAll notable changes to this project will be documented in this file. \
Expand All @@ -16,11 +18,13 @@ const DEFAULT_FOOTER: &str =
"Changelog generated by [cocogitto](https://github.com/oknozor/cocogitto).";

impl Release<'_> {
pub fn to_markdown(&self, renderer: Renderer) -> Result<String, tera::Error> {
pub fn to_markdown(&self, template: Template) -> Result<String, tera::Error> {
let renderer = Renderer::try_new(template)?;
renderer.render(self)
}

pub fn write_to_file<S: AsRef<Path>>(&self, path: S, renderer: Renderer) -> anyhow::Result<()> {
pub fn write_to_file<S: AsRef<Path>>(&self, path: S, template: Template) -> anyhow::Result<()> {
let renderer = Renderer::try_new(template)?;
let changelog = renderer.render(self)?;

let mut changelog_content = fs::read_to_string(path.as_ref())
Expand Down
10 changes: 9 additions & 1 deletion src/conventional/changelog/release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ mod test {

use crate::conventional::changelog::release::{ChangelogCommit, Release};
use crate::conventional::changelog::renderer::Renderer;
use crate::conventional::changelog::template::{RemoteContext, Template, TemplateKind};
use crate::conventional::commit::Commit;
use crate::git::oid::OidOf;
use crate::git::tag::Tag;
Expand Down Expand Up @@ -89,7 +90,14 @@ mod test {
fn should_render_github_template() -> Result<()> {
// Arrange
let release = Release::fixture();
let renderer = Renderer::github();
let renderer = Renderer::try_new(Template {
context: Some(RemoteContext::new(
"github.com".into(),
"cocogitto".into(),
"oknozor".into(),
)),
kind: TemplateKind::Remote,
})?;

// Act
let changelog = renderer.render(&release);
Expand Down
56 changes: 19 additions & 37 deletions src/conventional/changelog/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,66 +4,48 @@ use itertools::Itertools;
use tera::{get_json_pointer, to_value, try_get_value, Context, Tera, Value};

use crate::conventional::changelog::release::Release;
use crate::SETTINGS;

const DEFAULT_TEMPLATE: &[u8] = include_bytes!("template/simple");
const DEFAULT_TEMPLATE_NAME: &str = "default_template";
const GITHUB_TEMPLATE: &[u8] = include_bytes!("template/github");
const GITHUB_TEMPLATE_NAME: &str = "github_template";
use crate::conventional::changelog::template::Template;

#[derive(Debug)]
pub struct Renderer {
tera: Tera,
template_name: String,
template: Template,
}

impl Default for Renderer {
fn default() -> Self {
let template = String::from_utf8_lossy(DEFAULT_TEMPLATE);
Self::new(template.as_ref(), DEFAULT_TEMPLATE_NAME).unwrap()
Self::try_new(Template::default()).expect("Failed to load renderer for default template")
}
}

impl Renderer {
pub fn github() -> Self {
let template = String::from_utf8_lossy(GITHUB_TEMPLATE);
Self::new(template.as_ref(), GITHUB_TEMPLATE_NAME).unwrap()
}

pub fn new(template: &str, template_name: &str) -> Result<Self, tera::Error> {
pub fn try_new(template: Template) -> Result<Self, tera::Error> {
let mut tera = Tera::default();
let content = template.kind.get()?;
let content = String::from_utf8_lossy(content.as_slice());

tera.add_raw_template(template_name, template.as_ref())?;
tera.add_raw_template(template.kind.name(), content.as_ref())?;
tera.register_filter("upper_first", Self::upper_first_filter);
tera.register_filter("unscoped", Self::unscoped);

Ok(Renderer {
tera,
template_name: template_name.into(),
})
Ok(Renderer { tera, template })
}

pub(crate) fn render(&self, version: &Release) -> Result<String, tera::Error> {
let template_context = Context::from_serialize(version)?;

let mut context = tera::Context::new();
let settings = &SETTINGS.changelog;

if settings.github {
if let (Some(owner), Some(repo)) = (&settings.owner, &settings.repository) {
context.insert("platform", "https://github.com/");
context.insert("owner", owner);
context.insert(
"repository_url",
&format!("https://github.com/{}/{}", owner, repo),
);
}
};
let mut template_context = Context::from_serialize(version)?;

let context = self
.template
.context
.as_ref()
.map(|context| context.to_tera_context());

context.extend(template_context);
if let Some(context) = context {
template_context.extend(context);
}

self.tera
.render(&self.template_name, &context)
.render(self.template.kind.name(), &template_context)
.map(|changelog| {
changelog
.lines()
Expand Down
103 changes: 103 additions & 0 deletions src/conventional/changelog/template.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
use anyhow::bail;
use anyhow::Result;
use std::io;
use std::path::PathBuf;

const DEFAULT_TEMPLATE: &[u8] = include_bytes!("template/simple");
const DEFAULT_TEMPLATE_NAME: &str = "default";
const REMOTE_TEMPLATE: &[u8] = include_bytes!("template/remote");
const REMOTE_TEMPLATE_NAME: &str = "remote";

#[derive(Debug, Default)]
pub struct Template {
pub context: Option<RemoteContext>,
pub kind: TemplateKind,
}

impl Template {
pub fn from_arg(value: &str, context: Option<RemoteContext>) -> Result<Self> {
let template = TemplateKind::from_arg(value)?;

Ok(Template {
context,
kind: template,
})
}
}

#[derive(Debug)]
pub enum TemplateKind {
Default,
Remote,
Custom(PathBuf),
}

impl Default for TemplateKind {
fn default() -> Self {
TemplateKind::Default
}
}

impl TemplateKind {
/// Returns either a predefined template or a custom template
fn from_arg(value: &str) -> Result<Self> {
match value {
DEFAULT_TEMPLATE_NAME => Ok(TemplateKind::Default),
REMOTE_TEMPLATE_NAME => Ok(TemplateKind::Remote),
path => {
let path = PathBuf::from(path);
if !path.exists() {
bail!("Changelog template not found at {:?}", path);
}

Ok(TemplateKind::Custom(path))
}
}
}

pub(crate) fn get(&self) -> Result<Vec<u8>, io::Error> {
match self {
TemplateKind::Default => Ok(DEFAULT_TEMPLATE.to_vec()),
TemplateKind::Remote => Ok(REMOTE_TEMPLATE.to_vec()),
TemplateKind::Custom(path) => std::fs::read(path),
}
}

pub(crate) const fn name(&self) -> &'static str {
match self {
TemplateKind::Default => DEFAULT_TEMPLATE_NAME,
TemplateKind::Remote => REMOTE_TEMPLATE_NAME,
TemplateKind::Custom(_) => "custom_template",
}
}
}

/// A wrapper to append remote repository information to template context
#[derive(Debug)]
pub struct RemoteContext {
remote: String,
repository: String,
owner: String,
}

impl RemoteContext {
pub fn new(remote: String, repository: String, owner: String) -> Self {
Self {
remote,
repository,
owner,
}
}

pub(crate) fn to_tera_context(&self) -> tera::Context {
let mut context = tera::Context::new();
context.insert("platform", &format!("https://{}", self.remote.as_str()));
context.insert("owner", self.owner.as_str());
context.insert(
"repository_url",
&format!("https://{}/{}/{}", self.remote, self.owner, self.repository),
);

context
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
{% for commit in scoped_commits | sort(attribute="scope") %} \
{% if commit.author and repository_url %} \
{% set author = "@" ~ commit.author %} \
{% set author_link = "https://github.com/" ~ commit.author %} \
{% set author_link = platform ~ "/" ~ commit.author %} \
{% set author = "[" ~ author ~ "](" ~ author_link ~ ")" %} \
{% else %} \
{% set author = commit.signature %} \
Expand All @@ -33,7 +33,7 @@
{% for commit in typed_commits | unscoped %} \
{% if commit.author and repository_url %} \
{% set author = "@" ~ commit.author %} \
{% set author_link = "https://github.com/" ~ author %} \
{% set author_link = platform ~ "/" ~ commit.author %} \
{% set author = "[" ~ author ~ "](" ~ author_link ~ ")" %} \
{% else %} \
{% set author = commit.signature %} \
Expand Down
2 changes: 1 addition & 1 deletion src/conventional/changelog/template/simple
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
\
{% for scope, scoped_commits in typed_commits | group_by(attribute="scope") %} \
{% for commit in scoped_commits | sort(attribute="scope") %} \
{% if commit.author and repository_url %} \
{% if commit.author %} \
{% set author = "*" ~ commit.author ~ "*" %} \
{% else %} \
{% set author = commit.signature %} \
Expand Down

0 comments on commit ad2bcd2

Please sign in to comment.