diff --git a/cog.toml b/cog.toml index 3e285bdf..c46b6f4b 100644 --- a/cog.toml +++ b/cog.toml @@ -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" }, diff --git a/src/bin/cog.rs b/src/bin/cog.rs index e3eaace9..65287a04 100644 --- a/src/bin/cog.rs +++ b/src/bin/cog.rs @@ -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; @@ -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 { @@ -99,6 +101,24 @@ enum Cli { /// Generate the changelog for a specific git tag #[structopt(short, long)] at: Option, + + /// 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, + + /// Url to use during template generation + #[structopt(name = "remote", long, required_if("template", "remote"))] + remote: Option, + + /// Repository owner to use during template generation + #[structopt(name = "owner", long, required_if("template", "remote"))] + owner: Option, + + /// Name of the repository used during template generation + #[structopt(name = "repository", long, required_if("template", "remote"))] + repository: Option, }, /// Commit changelog from latest tag to HEAD and create new tag @@ -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); diff --git a/src/conventional/changelog/mod.rs b/src/conventional/changelog/mod.rs index 7bef41c8..c5c486c0 100644 --- a/src/conventional/changelog/mod.rs +++ b/src/conventional/changelog/mod.rs @@ -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. \ @@ -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 { + pub fn to_markdown(&self, template: Template) -> Result { + let renderer = Renderer::try_new(template)?; renderer.render(self) } - pub fn write_to_file>(&self, path: S, renderer: Renderer) -> anyhow::Result<()> { + pub fn write_to_file>(&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()) diff --git a/src/conventional/changelog/release.rs b/src/conventional/changelog/release.rs index 5365ab52..e1a52a57 100644 --- a/src/conventional/changelog/release.rs +++ b/src/conventional/changelog/release.rs @@ -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; @@ -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); diff --git a/src/conventional/changelog/renderer.rs b/src/conventional/changelog/renderer.rs index 6922f9eb..484a100e 100644 --- a/src/conventional/changelog/renderer.rs +++ b/src/conventional/changelog/renderer.rs @@ -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 { + pub fn try_new(template: Template) -> Result { 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 { - 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() diff --git a/src/conventional/changelog/template.rs b/src/conventional/changelog/template.rs new file mode 100644 index 00000000..53eea127 --- /dev/null +++ b/src/conventional/changelog/template.rs @@ -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, + pub kind: TemplateKind, +} + +impl Template { + pub fn from_arg(value: &str, context: Option) -> Result { + 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 { + 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, 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 + } +} diff --git a/src/conventional/changelog/template/github b/src/conventional/changelog/template/remote similarity index 95% rename from src/conventional/changelog/template/github rename to src/conventional/changelog/template/remote index 3250acde..4bb04ac8 100644 --- a/src/conventional/changelog/template/github +++ b/src/conventional/changelog/template/remote @@ -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 %} \ @@ -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 %} \ diff --git a/src/conventional/changelog/template/simple b/src/conventional/changelog/template/simple index a4a29cff..e6a65ec9 100644 --- a/src/conventional/changelog/template/simple +++ b/src/conventional/changelog/template/simple @@ -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 %} \ diff --git a/src/lib.rs b/src/lib.rs index a51cc2c8..e04f744c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,7 @@ use log::filter::CommitFilters; use settings::{HookType, Settings}; use crate::conventional::changelog::release::{ChangelogCommit, Release}; +use crate::conventional::changelog::template::Template; use crate::hook::HookVersion; pub mod conventional; @@ -462,7 +463,7 @@ impl CocoGitto { let path = settings::changelog_path(); - changelog.write_to_file(path, settings::renderer())?; + changelog.write_to_file(path, SETTINGS.to_changelog_template().unwrap_or_default())?; let current = self .repository @@ -519,12 +520,10 @@ impl CocoGitto { Ok(()) } - pub fn get_changelog_at_tag(&self, tag: &str) -> Result { + pub fn get_changelog_at_tag(&self, tag: &str, template: Template) -> Result { let changelog = self.get_changelog(None, Some(tag))?; - changelog - .to_markdown(settings::renderer()) - .map_err(|err| anyhow!(err)) + changelog.to_markdown(template).map_err(|err| anyhow!(err)) } /// ## Get a changelog between two oids diff --git a/src/settings.rs b/src/settings.rs index 060ee7cc..92b4e693 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -5,7 +5,7 @@ use crate::conventional::commit::CommitConfig; use crate::git::repository::Repository; use crate::{CommitsMetadata, CONFIG_PATH, SETTINGS}; -use crate::conventional::changelog::renderer::Renderer; +use crate::conventional::changelog::template::{RemoteContext, Template}; use anyhow::{anyhow, Result}; use config::{Config, File}; use conventional_commit_parser::commit::CommitType; @@ -40,7 +40,8 @@ pub struct Settings { #[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)] #[serde(deny_unknown_fields, default)] pub struct Changelog { - pub github: bool, + pub template: Option, + pub remote: Option, pub path: PathBuf, pub owner: Option, pub repository: Option, @@ -50,7 +51,8 @@ pub struct Changelog { impl Default for Changelog { fn default() -> Self { Changelog { - github: false, + template: None, + remote: None, path: PathBuf::from("CHANGELOG.md"), owner: None, repository: None, @@ -79,14 +81,6 @@ pub fn changelog_path() -> &'static PathBuf { &SETTINGS.changelog.path } -pub fn renderer() -> Renderer { - if SETTINGS.changelog.github { - Renderer::github() - } else { - Renderer::default() - } -} - #[derive(Debug, Deserialize, Serialize, Default, Eq, PartialEq)] #[serde(deny_unknown_fields)] pub struct BumpProfile { @@ -172,4 +166,35 @@ impl Settings { HookType::PostBump => &profile.post_bump_hooks, } } + + pub fn to_changelog_template(&self) -> Option