├── src
├── macros.rs
├── lib.rs
├── git.rs
├── error.rs
├── fmt.rs
├── sectionmap.rs
├── config.rs
├── link_style.rs
├── fmt
│ ├── md_writer.rs
│ └── json_writer.rs
└── clog.rs
├── .clog.toml
├── .gitignore
├── changelog.md
├── .github
└── workflows
│ ├── security_audit.yml
│ ├── lint.yml
│ └── ci.yml
├── Cargo.toml
├── LICENSE
├── rustfmt.toml
├── examples
└── clog.toml
└── README.md
/src/macros.rs:
--------------------------------------------------------------------------------
1 | macro_rules! regex(
2 | ($s:expr) => (::regex::Regex::new($s).unwrap());
3 | );
4 |
--------------------------------------------------------------------------------
/.clog.toml:
--------------------------------------------------------------------------------
1 | [clog]
2 | repository = "https://github.com/clog-tool/clog-lib"
3 | from-latest-tag = true
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled files
2 | *.o
3 | *.so
4 | *.rlib
5 | *.dll
6 |
7 | # Executables
8 | *.exe
9 |
10 | # Generated by Cargo
11 | /target/
12 | Cargo.lock
13 |
14 | #Others
15 | .DS_Store
16 |
17 | # Temporary files
18 | .*~
19 |
--------------------------------------------------------------------------------
/changelog.md:
--------------------------------------------------------------------------------
1 |
2 | ## 0.1.0 Crazy Dog (2022-08-10)
3 |
4 |
5 |
6 |
7 |
8 | ## 0.1.0 Crazy Dog (2022-08-10)
9 |
10 |
11 |
12 |
13 |
14 | ## 0.1.0 Crazy Dog (2022-08-10)
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![doc = include_str!("../README.md")]
2 |
3 | #[macro_use]
4 | mod macros;
5 | mod clog;
6 | mod config;
7 | pub mod error;
8 | pub mod fmt;
9 | pub mod git;
10 | mod link_style;
11 | mod sectionmap;
12 |
13 | pub use crate::{clog::Clog, link_style::LinkStyle, sectionmap::SectionMap};
14 |
15 | // The default config file
16 | const DEFAULT_CONFIG_FILE: &str = ".clog.toml";
17 |
--------------------------------------------------------------------------------
/.github/workflows/security_audit.yml:
--------------------------------------------------------------------------------
1 | ---
2 | on:
3 | push:
4 | paths:
5 | - '**/Cargo.toml'
6 | - '**/Cargo.lock'
7 |
8 | name: Security audit
9 |
10 | jobs:
11 | security_audit:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v1
15 | - uses: actions-rs/audit-check@v1
16 | with:
17 | token: ${{ secrets.GITHUB_TOKEN }}
18 |
--------------------------------------------------------------------------------
/src/git.rs:
--------------------------------------------------------------------------------
1 | /// The struct representation of a `Commit`
2 | #[derive(Debug, Clone)]
3 | pub struct Commit {
4 | /// The 40 char hash
5 | pub hash: String,
6 | /// The commit subject
7 | pub subject: String,
8 | /// The component (if any)
9 | pub component: String,
10 | /// Any issues this commit closes
11 | pub closes: Vec,
12 | /// Any issues this commit breaks
13 | pub breaks: Vec,
14 | /// The commit type (or alias)
15 | pub commit_type: String,
16 | }
17 |
18 | /// A convienience type for multiple commits
19 | pub type Commits = Vec;
20 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | ---
2 | on: pull_request
3 | name: PR Lints
4 | jobs:
5 | lints:
6 | runs-on: ubuntu-latest
7 | steps:
8 | - uses: actions/checkout@v2
9 |
10 | - uses: actions-rs/toolchain@v1
11 | with:
12 | pdrofile: minimal
13 | toolchain: nightly
14 | override: true
15 | components: clippy, rustfmt
16 |
17 | - uses: actions-rs/clippy-check@v1
18 | with:
19 | token: ${{ secrets.GITHUB_TOKEN }}
20 | args: --all-features
21 |
22 | - uses: actions-rs/cargo@v1
23 | with:
24 | command: fmt
25 | args: --all -- --check
26 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | ---
2 | on: [push, pull_request]
3 |
4 | name: Continuous Integration
5 |
6 | jobs:
7 | ci:
8 | runs-on: ubuntu-latest
9 | strategy:
10 | matrix:
11 | rust:
12 | - stable
13 | - nightly
14 | - 1.67.1 # MSRV
15 |
16 | steps:
17 | - uses: actions/checkout@v2
18 |
19 | - uses: actions-rs/toolchain@v1
20 | with:
21 | pdrofile: minimal
22 | toolchain: ${{ matrix.rust }}
23 | override: true
24 |
25 | - uses: actions-rs/cargo@v1
26 | with:
27 | command: build
28 |
29 | - uses: actions-rs/cargo@v1
30 | with:
31 | command: test
32 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | repository = "https://github.com/clog-tool/clog-lib"
3 | readme = "README.md"
4 | keywords = ["git", "log", "changelog", "parser", "parse"]
5 | license = "MIT"
6 | name = "clog"
7 | edition = "2021"
8 | version = "0.11.0"
9 | rust-version = "1.67.1" # MSRV
10 | authors = ["Christoph Burgdorf "]
11 | description = "A conventional changelog for the rest of us"
12 | exclude = ["docs/*"]
13 |
14 | [dependencies]
15 | indexmap = { version = "1.0.1", features = ["serde"] }
16 | regex = "1.6.0"
17 | toml = "0.5.9"
18 | time = { version = "0.3.12", features = ["formatting"] }
19 | thiserror = "1.0.32"
20 | strum = { version = "0.24.1", features = ["derive"] }
21 | log = "0.4.17"
22 | serde = { version = "1.0.143", features = ["derive"] }
23 |
24 | [features]
25 | default = []
26 | debug = [] # For debugging output
27 | unstable = []
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 thoughtram
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/rustfmt.toml:
--------------------------------------------------------------------------------
1 | # https://rust-lang.github.io/rustfmt/?version=master&search=#edition
2 | edition = "2021"
3 | # https://rust-lang.github.io/rustfmt/?version=master&search=#fn_single_line
4 | fn_single_line = true
5 | # https://rust-lang.github.io/rustfmt/?version=master&search=#format_code_in_doc_comments
6 | format_code_in_doc_comments = true
7 | # https://rust-lang.github.io/rustfmt/?version=master&search=#format_macro_matchers
8 | format_macro_matchers = true
9 | # https://rust-lang.github.io/rustfmt/?version=master&search=#imports_granularity
10 | imports_granularity = "Crate"
11 | # https://rust-lang.github.io/rustfmt/?version=master&search=#normalize_comments
12 | normalize_comments = true
13 | # https://rust-lang.github.io/rustfmt/?version=master&search=#reorder_impl_items
14 | reorder_impl_items = true
15 | # https://rust-lang.github.io/rustfmt/?version=master&search=#unstable_features
16 | unstable_features = true
17 | # https://rust-lang.github.io/rustfmt/?version=master&search=#use_field_init_shorthand
18 | use_field_init_shorthand = true
19 | # https://rust-lang.github.io/rustfmt/?version=master&search=#use_try_shorthand
20 | use_try_shorthand = true
21 | # https://rust-lang.github.io/rustfmt/?version=master&search=#wrap_comments
22 | wrap_comments = true
23 |
--------------------------------------------------------------------------------
/src/error.rs:
--------------------------------------------------------------------------------
1 | use std::{path::PathBuf, result::Result as StdResult};
2 |
3 | use thiserror::Error;
4 |
5 | pub type Result = StdResult;
6 |
7 | /// An enum for describing and handling various errors encountered while dealing
8 | /// with `clog` building, or writing of changelogs.
9 | #[derive(Debug, Error)]
10 | pub enum Error {
11 | #[error("Found unknown component '{0}' that does not correspond to a Changelog Section")]
12 | UnknownComponent(String),
13 |
14 | #[error("failed to parse config file: {0}")]
15 | ConfigParse(PathBuf),
16 |
17 | #[error("incorrect format for config file: {0}")]
18 | ConfigFormat(PathBuf),
19 |
20 | #[error("cannot get current directory")]
21 | CurrentDir,
22 |
23 | #[error("unrecognized link-style field")]
24 | LinkStyle,
25 |
26 | #[error("fatal I/O error with output file")]
27 | Io(#[from] std::io::Error),
28 |
29 | #[error("failed to convert date/time to string format")]
30 | TimeFormat(#[from] time::error::Format),
31 |
32 | #[error("failed to convert date/time to string format")]
33 | Time(#[from] time::Error),
34 |
35 | #[error("failed to convert {0} to valid ChangelogFormat")]
36 | ChangelogFormat(String),
37 |
38 | #[error("Failed to parse TOML configuration file")]
39 | Toml(#[from] toml::de::Error),
40 |
41 | #[error("unknown fatal error")]
42 | Unknown,
43 | }
44 |
--------------------------------------------------------------------------------
/src/fmt.rs:
--------------------------------------------------------------------------------
1 | mod json_writer;
2 | mod md_writer;
3 |
4 | use std::{result::Result as StdResult, str::FromStr};
5 |
6 | use strum::{Display, EnumString};
7 |
8 | pub use self::{json_writer::JsonWriter, md_writer::MarkdownWriter};
9 | use crate::{clog::Clog, error::Result, sectionmap::SectionMap};
10 |
11 | #[derive(Copy, Clone, PartialEq, Eq, Debug, Default, EnumString, Display)]
12 | #[strum(ascii_case_insensitive)]
13 | pub enum ChangelogFormat {
14 | Json,
15 | #[default]
16 | Markdown,
17 | }
18 |
19 | impl<'de> serde::de::Deserialize<'de> for ChangelogFormat {
20 | fn deserialize(deserializer: D) -> StdResult
21 | where
22 | D: serde::de::Deserializer<'de>,
23 | {
24 | let s = String::deserialize(deserializer)?;
25 | FromStr::from_str(&s).map_err(serde::de::Error::custom)
26 | }
27 | }
28 |
29 | /// A trait that allows writing the results of a `clog` run which can then be
30 | /// written in an arbitrary format. The single required function
31 | /// `write_changelog()` accepts a `clog::SectionMap` which can be thought of
32 | /// similiar to a `clog` "AST" of sorts.
33 | ///
34 | /// `clog` provides two default implementors of this traint,
35 | /// `clog::fmt::MarkdownWriter` and `clog::fmt::JsonWriter` for writing Markdown
36 | /// and JSON respectively
37 | pub trait FormatWriter {
38 | /// Writes a changelog from a given `clog::SectionMap` which can be thought
39 | /// of as an "AST" of sorts
40 | fn write_changelog(&mut self, options: &Clog, section_map: &SectionMap) -> Result<()>;
41 | }
42 |
--------------------------------------------------------------------------------
/src/sectionmap.rs:
--------------------------------------------------------------------------------
1 | use std::collections::{BTreeMap, HashMap};
2 |
3 | use crate::git::Commit;
4 |
5 | /// The second level of the changelog, i.e. the components -> commit information
6 | pub type ComponentMap = BTreeMap>;
7 |
8 | /// A struct which holds sections to and components->commits map
9 | pub struct SectionMap {
10 | /// The top level map of the changelog, i.e. sections -> components
11 | pub sections: HashMap,
12 | }
13 |
14 | impl SectionMap {
15 | /// Creates a section map from a vector of commits, which we can then
16 | /// iterate through and write
17 | ///
18 | /// # Example
19 | ///
20 | /// ```no_run
21 | /// # use std::fs::File;
22 | /// # use clog::{Clog, SectionMap};
23 | /// # use clog::fmt::{FormatWriter, MarkdownWriter};
24 | /// let clog = Clog::new().unwrap();
25 | ///
26 | /// // Get the commits we're interested in...
27 | /// let sm = SectionMap::from_commits(clog.get_commits().unwrap());
28 | ///
29 | /// // Create a file to hold our results, which the MardownWriter will wrap (note, .unwrap() is only
30 | /// // used to keep the example short and concise)
31 | /// let mut file = File::create("my_changelog.md").ok().unwrap();
32 | ///
33 | /// // Create the MarkdownWriter
34 | /// let mut writer = MarkdownWriter::new(&mut file);
35 | ///
36 | /// // Use the MarkdownWriter to write the changelog
37 | /// clog.write_changelog_with(&mut writer).unwrap();
38 | /// ```
39 | pub fn from_commits(commits: Vec) -> SectionMap {
40 | let mut sm = SectionMap {
41 | sections: HashMap::new(),
42 | };
43 |
44 | for entry in commits {
45 | if !entry.breaks.is_empty() {
46 | let comp_map = sm
47 | .sections
48 | .entry("Breaking Changes".to_owned())
49 | .or_default();
50 | let sec_map = comp_map.entry(entry.component.clone()).or_default();
51 | sec_map.push(entry.clone());
52 | }
53 | let comp_map = sm.sections.entry(entry.commit_type.clone()).or_default();
54 | let sec_map = comp_map.entry(entry.component.clone()).or_default();
55 | sec_map.push(entry);
56 | }
57 |
58 | sm
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/examples/clog.toml:
--------------------------------------------------------------------------------
1 | [clog]
2 | # A repository link with the trailing '.git' which will be used to generate
3 | # all commit and issue links
4 | repository = "https://github.com/clog-tool/clog-lib"
5 | # A constant release title
6 | subtitle = "my awesome title"
7 |
8 | # specify the style of commit links to generate, defaults to "github" if omitted
9 | link-style = "github"
10 |
11 | # The preferred way to set a constant changelog. This file will be read for old changelog
12 | # data, then prepended to for new changelog data. It's the equivilant to setting
13 | # both infile and outfile to the same file.
14 | #
15 | # Do not use with outfile or infile fields!
16 | #
17 | # Defaults to stdout when omitted
18 | changelog = "mychangelog.md"
19 |
20 | # This sets an output file only! If it exists already, new changelog data will be
21 | # prepended, if not it will be created.
22 | #
23 | # This is useful in conjunction with the infile field if you have a separate file
24 | # that you would like to append after newly created clog data
25 | #
26 | # Defaults to stdout when omitted
27 | outfile = "MyChangelog.md"
28 |
29 | # This sets the input file old! Any data inside this file will be appended to any
30 | # new data that clog picks up
31 | #
32 | # This is useful in conjunction with the outfile field where you may wish to read
33 | # from one file and append that data to the clog output in another
34 | infile = "My_old_changelog.md"
35 |
36 | # This sets the output format. There are two options "json" or "markdown" and
37 | # defaults to "markdown" when omitted
38 | output-format = "json"
39 |
40 | # If you use tags, you can set the following if you wish to only pick
41 | # up changes since your latest tag
42 | from-latest-tag = true
43 |
44 | # The working or project directory
45 | git-work-tree = "/myproject"
46 |
47 | # the git metadata directory
48 | git-dir = "/myproject/.git"
49 |
50 | # `clog` will display three sections in your changelog, `Features`,
51 | # `Performance`, and `Bug Fixes` by default. You can add additional sections
52 | # with a `[sections]` table. The `[sections]` table contains the header and
53 | # aliases.
54 | [sections]
55 | MySection = ["mysec", "ms"]
56 | # You can also use spaces in the section names
57 | "Another Section" = ["another"]
58 |
59 |
60 | # `clog` will use the exact component string given in your commit message (i.e.
61 | # `feat(comp): message` will be displayed as as the "comp" component in the
62 | # changelog output. If you want to display a longer string for a component in
63 | # your changelog, you can define aliases in a `[components]` table
64 | [components]
65 | MyLongComponentName = ["long", "comp"]
66 |
--------------------------------------------------------------------------------
/src/config.rs:
--------------------------------------------------------------------------------
1 | use std::{collections::HashMap, path::PathBuf};
2 |
3 | use indexmap::IndexMap;
4 | use serde::Deserialize;
5 |
6 | use crate::{fmt::ChangelogFormat, link_style::LinkStyle};
7 |
8 | #[derive(Debug, Clone, Deserialize)]
9 | pub struct RawCfg {
10 | pub clog: RawClogCfg,
11 | #[serde(default)]
12 | pub sections: IndexMap>,
13 | #[serde(default)]
14 | pub components: HashMap>,
15 | }
16 | #[derive(Debug, Clone, Default, Deserialize)]
17 | #[serde(default, rename_all = "kebab-case")]
18 | pub struct RawClogCfg {
19 | pub changelog: Option,
20 | pub from_latest_tag: bool,
21 | pub repository: Option,
22 | pub infile: Option,
23 | pub subtitle: Option,
24 | pub outfile: Option,
25 | pub git_dir: Option,
26 | pub git_work_tree: Option,
27 | pub link_style: LinkStyle,
28 | pub output_format: ChangelogFormat,
29 | }
30 |
31 | #[cfg(test)]
32 | mod tests {
33 | use super::*;
34 |
35 | #[test]
36 | fn from_config() {
37 | let cfg = include_str!("../examples/clog.toml");
38 | let res = toml::from_str(cfg);
39 | assert!(res.is_ok(), "{res:?}");
40 | let cfg: RawCfg = res.unwrap();
41 |
42 | assert_eq!(
43 | cfg.clog.repository,
44 | Some("https://github.com/clog-tool/clog-lib".into())
45 | );
46 | assert_eq!(cfg.clog.subtitle, Some("my awesome title".into()));
47 | assert_eq!(cfg.clog.link_style, LinkStyle::Github);
48 | assert_eq!(cfg.clog.changelog, Some("mychangelog.md".into()));
49 | assert_eq!(cfg.clog.outfile, Some("MyChangelog.md".into()));
50 | assert_eq!(cfg.clog.infile, Some("My_old_changelog.md".into()));
51 | assert_eq!(cfg.clog.output_format, ChangelogFormat::Json);
52 | assert_eq!(cfg.clog.git_work_tree, Some("/myproject".into()));
53 | assert_eq!(cfg.clog.git_dir, Some("/myproject/.git".into()));
54 | assert!(cfg.clog.from_latest_tag);
55 | assert_eq!(
56 | cfg.sections.get("MySection"),
57 | Some(&vec!["mysec".into(), "ms".into()])
58 | );
59 | assert_eq!(
60 | cfg.sections.get("Another Section"),
61 | Some(&vec!["another".into()])
62 | );
63 | assert_eq!(
64 | cfg.components.get("MyLongComponentName"),
65 | Some(&vec!["long".into(), "comp".into()])
66 | );
67 | }
68 |
69 | #[test]
70 | fn dogfood_config() {
71 | let cfg = include_str!("../.clog.toml");
72 | let res = toml::from_str(cfg);
73 | assert!(res.is_ok(), "{res:?}");
74 | let cfg: RawCfg = res.unwrap();
75 |
76 | assert_eq!(
77 | cfg.clog.repository,
78 | Some("https://github.com/clog-tool/clog-lib".into())
79 | );
80 | assert_eq!(cfg.clog.link_style, LinkStyle::Github);
81 | assert!(cfg.clog.from_latest_tag);
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/link_style.rs:
--------------------------------------------------------------------------------
1 | use std::str::FromStr;
2 |
3 | use strum::{Display, EnumString};
4 |
5 | /// Determines the hyperlink style used in commit and issue links. Defaults to
6 | /// `LinksStyle::Github`
7 | ///
8 | /// # Example
9 | ///
10 | /// ```no_run
11 | /// # use clog::{LinkStyle, Clog};
12 | /// let clog = Clog::new().unwrap();
13 | /// clog.link_style(LinkStyle::Stash);
14 | /// ```
15 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Default, Display, EnumString)]
16 | #[strum(ascii_case_insensitive)]
17 | pub enum LinkStyle {
18 | #[default]
19 | Github,
20 | Gitlab,
21 | Stash,
22 | Cgit,
23 | }
24 |
25 | impl<'de> serde::de::Deserialize<'de> for LinkStyle {
26 | fn deserialize(deserializer: D) -> Result
27 | where
28 | D: serde::de::Deserializer<'de>,
29 | {
30 | let s = String::deserialize(deserializer)?;
31 | FromStr::from_str(&s).map_err(serde::de::Error::custom)
32 | }
33 | }
34 |
35 | impl LinkStyle {
36 | /// Gets a hyperlink url to an issue in the specified format.
37 | ///
38 | /// # Example
39 | ///
40 | /// ```no_run
41 | /// # use clog::{LinkStyle, Clog};
42 | /// let link = LinkStyle::Github;
43 | /// let issue = link.issue_link("141", Some("https://github.com/thoughtram/clog"));
44 | ///
45 | /// assert_eq!("https://github.com/thoughtram/clog/issues/141", issue);
46 | /// ```
47 | pub fn issue_link>(&self, issue: S, repo: Option) -> String {
48 | let issue = issue.as_ref();
49 | if let Some(link) = repo {
50 | let link = link.as_ref();
51 | match *self {
52 | LinkStyle::Github | LinkStyle::Gitlab => format!("{link}/issues/{issue}"),
53 | // cgit does not support issues
54 | LinkStyle::Stash | LinkStyle::Cgit => issue.to_string(),
55 | }
56 | } else {
57 | issue.to_string()
58 | }
59 | }
60 |
61 | /// Gets a hyperlink url to a commit in the specified format.
62 | ///
63 | /// # Example
64 | /// ```no_run
65 | /// # use clog::{LinkStyle, Clog};
66 | /// let link = LinkStyle::Github;
67 | /// let commit = link.commit_link(
68 | /// "123abc891234567890abcdefabc4567898724",
69 | /// Some("https://github.com/clog-tool/clog-lib"),
70 | /// );
71 | ///
72 | /// assert_eq!(
73 | /// "https://github.com/thoughtram/clog/commit/123abc891234567890abcdefabc4567898724",
74 | /// commit
75 | /// );
76 | /// ```
77 | pub fn commit_link>(&self, hash: S, repo: Option) -> String {
78 | let hash = hash.as_ref();
79 | if let Some(link) = repo {
80 | let link = link.as_ref();
81 | match *self {
82 | LinkStyle::Github | LinkStyle::Gitlab => format!("{link}/commit/{hash}"),
83 | LinkStyle::Stash => format!("{link}/commits/{hash}"),
84 | LinkStyle::Cgit => format!("{link}/commit/?id={hash}"),
85 | }
86 | } else {
87 | (hash[0..8]).to_string()
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | `clog`
2 |
3 | ![Rust Version][rustc-image]
4 | [![crates.io][crate-image]][crate-link]
5 | [![Dependency Status][deps-image]][deps-link]
6 | [![docs-image][docs-image]][docs-link]
7 |
8 | A library for generating a [conventional][convention] changelog from git
9 | metadata, written in Rust
10 |
11 | ## About
12 |
13 | `clog` creates a changelog automatically from your local git metadata. See the
14 | `clog`s [changelog.md][our_changelog] for an example.
15 |
16 | The way this works, is every time you make a commit, you ensure your commit
17 | subject line follows the [conventional][convention] format.
18 |
19 | *NOTE:* `clog` also supports empty components by making commit messages such as
20 | `alias: message` or `alias(): message` (i.e. without the component)
21 |
22 | ## Usage
23 |
24 | There are two ways to use `clog`, as a binary via the command line (See
25 | [clog-cli][clog_cli] for details) or as a library in your applications.
26 |
27 | See the [documentation][docs-link] for information on using `clog` in your
28 | applications.
29 |
30 | In order to see it in action, you'll need a repository that already has some of
31 | those specially crafted commit messages in it's history. For this, we'll use
32 | the `clog` repository itself.
33 |
34 | 1. Clone the `clog-lib` repository (we will clone to our home directory to
35 | make things simple, feel free to change it)
36 |
37 | ```sh
38 | $ git clone https://github.com/clog-tool/clog-lib
39 | ```
40 |
41 | 2. Add `clog` as a dependency in your `Cargo.toml`
42 |
43 | ```toml
44 | [dependencies]
45 | clog = "*"
46 | ```
47 |
48 | 3. Use the following in your `src/main.rs`
49 |
50 | ```rust
51 | extern crate clog;
52 |
53 | use clog::Clog;
54 |
55 | fn main() {
56 | // Create the struct
57 | let mut clog = Clog::with_git_work_tree("~/clog")
58 | .unwrap()
59 | .repository("https://github.com/thoughtram/clog")
60 | .subtitle("Crazy Dog")
61 | .changelog("changelog.md")
62 | .from("6d8183f")
63 | .version("0.1.0");
64 |
65 | // Write the changelog to the current working directory
66 | //
67 | // Alternatively we could have used .write_changelog_to("/somedir/some_file.md")
68 | clog.write_changelog().unwrap();
69 | }
70 | ```
71 |
72 | 4. Compile and run `$ cargo build --release && ./target/release/bin_name
73 | 5. View the output in your favorite markdown viewer! `$ vim changelog.md`
74 |
75 | ### Configuration
76 |
77 | `clog` can also be configured using a configuration file in TOML.
78 |
79 | See the `examples/clog.toml` for available options.
80 |
81 | ## Companion Projects
82 |
83 | - [`clog-cli`](http://github.com/clog-tool/clog-cli/) - A command line tool
84 | that uses this library to generate changelogs.
85 | - [Commitizen](http://commitizen.github.io/cz-cli/) - A command line tool that
86 | helps you writing better commit messages.
87 |
88 | ## LICENSE
89 |
90 | clog is licensed under the MIT Open Source license. For more information, see the LICENSE file in this repository.
91 |
92 | [//]: # (badges)
93 |
94 | [docs-image]: https://img.shields.io/docsrs/clog
95 | [docs-link]: https://docs.rs/clog
96 | [rustc-image]: https://img.shields.io/badge/rustc-1.56+-blue.svg
97 | [crate-image]: https://img.shields.io/crates/v/clog.svg
98 | [crate-link]: https://crates.io/crates/clog
99 | [deps-image]: https://deps.rs/repo/github/clog-tool/clog-lib/status.svg
100 | [deps-link]: https://deps.rs/repo/github/clog-tool/clog-lib/
101 |
102 |
103 | [//]: # (Links)
104 |
105 | [convention]: https://github.com/ajoslin/conventional-changelog/blob/a5505865ff3dd710cf757f50530e73ef0ca641da/conventions/angular.md
106 | [our_changelog]: https://github.com/clog-tool/clog-lib/blob/master/changelog.md
107 | [clog_cli]: https://github.com/clog-tool/clog-cli
108 |
--------------------------------------------------------------------------------
/src/fmt/md_writer.rs:
--------------------------------------------------------------------------------
1 | use std::{collections::BTreeMap, io};
2 |
3 | use time::OffsetDateTime;
4 |
5 | use crate::{clog::Clog, error::Result, fmt::FormatWriter, git::Commit, sectionmap::SectionMap};
6 |
7 | /// Wraps a `std::io::Write` object to write `clog` output in a Markdown format
8 | ///
9 | /// # Example
10 | ///
11 | /// ```no_run
12 | /// # use std::fs::File;
13 | /// # use clog::{SectionMap, Clog, fmt::MarkdownWriter};
14 | /// let clog = Clog::new().unwrap();
15 | ///
16 | /// // Get the commits we're interested in...
17 | /// let sm = SectionMap::from_commits(clog.get_commits().unwrap());
18 | ///
19 | /// // Create a file to hold our results, which the MardownWriter will wrap (note, .unwrap() is only
20 | /// // used to keep the example short and concise)
21 | /// let mut file = File::create("my_changelog.md").ok().unwrap();
22 | ///
23 | /// // Create the MarkdownWriter
24 | /// let mut writer = MarkdownWriter::new(&mut file);
25 | ///
26 | /// // Use the MarkdownWriter to write the changelog
27 | /// clog.write_changelog_with(&mut writer).unwrap();
28 | /// ```
29 | pub struct MarkdownWriter<'a>(&'a mut dyn io::Write);
30 |
31 | impl<'a> MarkdownWriter<'a> {
32 | /// Creates a new instance of the `MarkdownWriter` struct using a
33 | /// `std::io::Write` object.
34 | ///
35 | /// # Example
36 | ///
37 | /// ```no_run
38 | /// # use std::io::BufWriter;
39 | /// # use clog::{Clog, fmt::MarkdownWriter};
40 | /// let clog = Clog::new().unwrap();
41 | ///
42 | /// // Create a MarkdownWriter to wrap stdout
43 | /// let out = std::io::stdout();
44 | /// let mut out_buf = BufWriter::new(out.lock());
45 | /// let mut writer = MarkdownWriter::new(&mut out_buf);
46 | /// ```
47 | pub fn new(writer: &'a mut T) -> MarkdownWriter<'a> {
48 | MarkdownWriter(writer)
49 | }
50 |
51 | fn write_header(&mut self, options: &Clog) -> Result<()> {
52 | let subtitle = options.subtitle.clone().unwrap_or_default();
53 | let version = options.version.clone().unwrap_or_default();
54 |
55 | let version_text = if options.patch_ver {
56 | format!("### {version} {subtitle}")
57 | } else {
58 | format!("## {version} {subtitle}")
59 | };
60 |
61 | let now = OffsetDateTime::now_utc();
62 | // unwrap because the format description is static
63 | let date = now.format(&time::format_description::parse("[year]-[month]-[day]").unwrap())?;
64 | writeln!(
65 | self.0,
66 | "\n{version_text} ({date})\n",
67 | )
68 | .map_err(Into::into)
69 | }
70 |
71 | /// Writes a particular section of a changelog
72 | fn write_section(
73 | &mut self,
74 | options: &Clog,
75 | title: &str,
76 | section: &BTreeMap<&String, &Vec>,
77 | ) -> Result<()> {
78 | if section.is_empty() {
79 | return Ok(());
80 | }
81 |
82 | self.0
83 | .write_all(format!("\n#### {title}\n\n")[..].as_bytes())?;
84 |
85 | for (component, entries) in section.iter() {
86 | let nested = (entries.len() > 1) && !component.is_empty();
87 |
88 | let prefix = if nested {
89 | writeln!(self.0, "* **{component}:**")?;
90 | " *".to_owned()
91 | } else if !component.is_empty() {
92 | format!("* **{component}:**")
93 | } else {
94 | "* ".to_string()
95 | };
96 |
97 | for entry in entries.iter() {
98 | write!(
99 | self.0,
100 | "{prefix} {} ([{}]({})",
101 | entry.subject,
102 | &entry.hash[0..8],
103 | options
104 | .link_style
105 | .commit_link(&*entry.hash, options.repo.as_deref())
106 | )?;
107 |
108 | if !entry.closes.is_empty() {
109 | let closes_string = entry
110 | .closes
111 | .iter()
112 | .map(|s| {
113 | format!(
114 | "[#{s}]({})",
115 | options.link_style.issue_link(s, options.repo.as_ref())
116 | )
117 | })
118 | .collect::>()
119 | .join(", ");
120 |
121 | write!(self.0, ", closes {closes_string}")?;
122 | }
123 | if !entry.breaks.is_empty() {
124 | let breaks_string = entry
125 | .breaks
126 | .iter()
127 | .map(|s| {
128 | format!(
129 | "[#{s}]({})",
130 | options.link_style.issue_link(s, options.repo.as_ref())
131 | )
132 | })
133 | .collect::>()
134 | .join(", ");
135 |
136 | // 5 = "[#]()" i.e. a commit message that only said "BREAKING"
137 | if breaks_string.len() != 5 {
138 | write!(self.0, ", breaks {breaks_string}")?;
139 | }
140 | }
141 |
142 | writeln!(self.0, ")")?;
143 | }
144 | }
145 |
146 | Ok(())
147 | }
148 |
149 | /// Writes some contents to the `Write` writer object
150 | #[allow(dead_code)]
151 | fn write(&mut self, content: &str) -> Result<()> {
152 | write!(self.0, "\n\n\n")?;
153 | write!(self.0, "{}", content).map_err(Into::into)
154 | }
155 | }
156 |
157 | impl<'a> FormatWriter for MarkdownWriter<'a> {
158 | fn write_changelog(&mut self, options: &Clog, sm: &SectionMap) -> Result<()> {
159 | self.write_header(options)?;
160 |
161 | // Get the section names ordered from `options.section_map`
162 | let s_it = options
163 | .section_map
164 | .keys()
165 | .filter_map(|sec| sm.sections.get(sec).map(|secmap| (sec, secmap)));
166 | for (sec, secmap) in s_it {
167 | self.write_section(
168 | options,
169 | &sec[..],
170 | &secmap.iter().collect::>(),
171 | )?;
172 | }
173 |
174 | self.0.flush().map_err(Into::into)
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/src/fmt/json_writer.rs:
--------------------------------------------------------------------------------
1 | use std::{collections::BTreeMap, io};
2 |
3 | use log::debug;
4 | use time::OffsetDateTime;
5 |
6 | use crate::{clog::Clog, error::Result, fmt::FormatWriter, git::Commit, sectionmap::SectionMap};
7 |
8 | /// Wraps a `std::io::Write` object to write `clog` output in a JSON format
9 | ///
10 | /// # Example
11 | ///
12 | /// ```no_run
13 | /// # use std::fs::File;
14 | /// # use clog::{SectionMap, Clog, fmt::JsonWriter};
15 | /// let clog = Clog::new().unwrap();
16 | ///
17 | /// // Get the commits we're interested in...
18 | /// let sm = SectionMap::from_commits(clog.get_commits().unwrap());
19 | ///
20 | /// // Create a file to hold our results, which the JsonWriter will wrap (note, .unwrap() is only
21 | /// // used to keep the example short and concise)
22 | /// let mut file = File::create("my_changelog.json").ok().unwrap();
23 | ///
24 | /// // Create the JSON Writer
25 | /// let mut writer = JsonWriter::new(&mut file);
26 | ///
27 | /// // Use the JsonWriter to write the changelog
28 | /// clog.write_changelog_with(&mut writer).unwrap();
29 | /// ```
30 | pub struct JsonWriter<'a>(&'a mut dyn io::Write);
31 |
32 | impl<'a> JsonWriter<'a> {
33 | /// Creates a new instance of the `JsonWriter` struct using a
34 | /// `std::io::Write` object.
35 | ///
36 | /// # Example
37 | ///
38 | /// ```no_run
39 | /// # use std::io::{stdout, BufWriter};
40 | /// # use clog::{Clog, fmt::JsonWriter};
41 | /// let clog = Clog::new().unwrap();
42 | ///
43 | /// // Create a JsonWriter to wrap stdout
44 | /// let out = stdout();
45 | /// let mut out_buf = BufWriter::new(out.lock());
46 | /// let mut writer = JsonWriter::new(&mut out_buf);
47 | /// ```
48 | pub fn new(writer: &'a mut T) -> JsonWriter<'a> { JsonWriter(writer) }
49 | }
50 |
51 | impl<'a> JsonWriter<'a> {
52 | /// Writes the initial header inforamtion for a release
53 | fn write_header(&mut self, options: &Clog) -> Result<()> {
54 | write!(
55 | self.0,
56 | "\"header\":{{\"version\":{:?},\"patch_version\":{:?},\"subtitle\":{},",
57 | options.version,
58 | options.patch_ver,
59 | options.subtitle.as_deref().unwrap_or("null"),
60 | )?;
61 |
62 | let now = OffsetDateTime::now_utc();
63 | // unwrap because the format description is static
64 | let date = now.format(&time::format_description::parse("[year]-[month]-[day]").unwrap())?;
65 | write!(self.0, "\"date\":\"{}\"}},", date).map_err(Into::into)
66 | }
67 |
68 | /// Writes a particular section of a changelog
69 | fn write_section(
70 | &mut self,
71 | options: &Clog,
72 | section: &BTreeMap<&String, &Vec>,
73 | ) -> Result<()> {
74 | if section.is_empty() {
75 | write!(self.0, "\"commits\":null")?;
76 | return Ok(());
77 | }
78 |
79 | write!(self.0, "\"commits\":[")?;
80 | let mut s_it = section.iter().peekable();
81 | while let Some((component, entries)) = s_it.next() {
82 | let mut e_it = entries.iter().peekable();
83 | debug!("Writing component: {}", component);
84 | while let Some(entry) = e_it.next() {
85 | debug!("Writing commit: {}", &*entry.subject);
86 | write!(self.0, "{{\"component\":")?;
87 | if component.is_empty() {
88 | write!(self.0, "null,")?;
89 | } else {
90 | write!(self.0, "{:?},", component)?;
91 | }
92 | write!(
93 | self.0,
94 | "\"subject\":{:?},\"commit_link\":{:?},\"closes\":",
95 | entry.subject,
96 | options
97 | .link_style
98 | .commit_link(&*entry.hash, options.repo.as_deref())
99 | )?;
100 |
101 | if entry.closes.is_empty() {
102 | write!(self.0, "null,")?;
103 | } else {
104 | write!(self.0, "[")?;
105 | let mut c_it = entry.closes.iter().peekable();
106 | while let Some(issue) = c_it.next() {
107 | write!(
108 | self.0,
109 | "{{\"issue\":{},\"issue_link\":{:?}}}",
110 | issue,
111 | options.link_style.issue_link(issue, options.repo.as_ref())
112 | )?;
113 | if c_it.peek().is_some() {
114 | debug!("There are more close commits, adding comma");
115 | write!(self.0, ",")?;
116 | } else {
117 | debug!("There are no more close commits, no comma required");
118 | }
119 | }
120 | write!(self.0, "],")?;
121 | }
122 | write!(self.0, "\"breaks\":")?;
123 | if entry.breaks.is_empty() {
124 | write!(self.0, "null}}")?;
125 | } else {
126 | write!(self.0, "[")?;
127 | let mut c_it = entry.closes.iter().peekable();
128 | while let Some(issue) = c_it.next() {
129 | write!(
130 | self.0,
131 | "{{\"issue\":{},\"issue_link\":{:?}}}",
132 | issue,
133 | options.link_style.issue_link(issue, options.repo.as_ref())
134 | )?;
135 | if c_it.peek().is_some() {
136 | debug!("There are more breaks commits, adding comma");
137 | write!(self.0, ",")?;
138 | } else {
139 | debug!("There are no more breaks commits, no comma required");
140 | }
141 | }
142 | write!(self.0, "]}}")?;
143 | }
144 | if e_it.peek().is_some() {
145 | debug!("There are more commits, adding comma");
146 | write!(self.0, ",")?;
147 | } else {
148 | debug!("There are no more commits, no comma required");
149 | }
150 | }
151 | if s_it.peek().is_some() {
152 | debug!("There are more sections, adding comma");
153 | write!(self.0, ",")?;
154 | } else {
155 | debug!("There are no more commits, no comma required");
156 | }
157 | }
158 | write!(self.0, "]").map_err(Into::into)
159 | }
160 |
161 | /// Writes some contents to the `Write` writer object
162 | #[allow(dead_code)]
163 | fn write(&mut self, content: &str) -> io::Result<()> { write!(self.0, "{}", content) }
164 | }
165 |
166 | impl<'a> FormatWriter for JsonWriter<'a> {
167 | fn write_changelog(&mut self, options: &Clog, sm: &SectionMap) -> Result<()> {
168 | debug!("Writing JSON changelog");
169 | write!(self.0, "{{")?;
170 | self.write_header(options)?;
171 |
172 | write!(self.0, "\"sections\":")?;
173 | let mut s_it = options
174 | .section_map
175 | .keys()
176 | .filter_map(|sec| sm.sections.get(sec).map(|compmap| (sec, compmap)))
177 | .peekable();
178 | if s_it.peek().is_some() {
179 | debug!("There are sections to write");
180 | write!(self.0, "[")?;
181 | while let Some((sec, compmap)) = s_it.next() {
182 | debug!("Writing section: {sec}");
183 | write!(self.0, "{{\"title\":{sec:?},")?;
184 |
185 | self.write_section(options, &compmap.iter().collect::>())?;
186 |
187 | write!(self.0, "}}")?;
188 | if s_it.peek().is_some() {
189 | debug!("There are more sections, adding comma");
190 | write!(self.0, ",")?;
191 | } else {
192 | debug!("There are no more sections, no comma required");
193 | }
194 | }
195 | write!(self.0, "]")?;
196 | } else {
197 | debug!("There are no sections to write");
198 | write!(self.0, "null")?;
199 | }
200 |
201 | write!(self.0, "}}")?;
202 | debug!("Finished writing sections, flushing");
203 | self.0.flush().map_err(Into::into)
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/src/clog.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | collections::HashMap,
3 | convert::AsRef,
4 | env,
5 | fs::File,
6 | io::{stdout, BufWriter, Read, Write},
7 | path::{Path, PathBuf},
8 | process::Command,
9 | result::Result as StdResult,
10 | };
11 |
12 | use indexmap::IndexMap;
13 | use log::debug;
14 | use regex::Regex;
15 |
16 | use crate::{
17 | config::RawCfg,
18 | error::{Error, Result},
19 | fmt::{ChangelogFormat, FormatWriter, JsonWriter, MarkdownWriter},
20 | git::{Commit, Commits},
21 | link_style::LinkStyle,
22 | sectionmap::SectionMap,
23 | DEFAULT_CONFIG_FILE,
24 | };
25 |
26 | fn regex_default() -> Regex { regex!(r"^([^:\(]+?)(?:\(([^\)]*?)?\))?:(.*)") }
27 | fn closes_regex_default() -> Regex { regex!(r"(?:Closes|Fixes|Resolves)\s((?:#(\d+)(?:,\s)?)+)") }
28 | fn breaks_regex_default() -> Regex { regex!(r"(?:Breaks|Broke)\s((?:#(\d+)(?:,\s)?)+)") }
29 | fn breaking_regex_default() -> Regex { regex!(r"(?i:breaking)") }
30 |
31 | /// The base struct used to set options and interact with the library.
32 | #[derive(Debug, Clone)]
33 | pub struct Clog {
34 | /// The repository used for the base of hyper-links
35 | pub repo: Option,
36 | /// The link style to used for commit and issue hyper-links
37 | pub link_style: LinkStyle,
38 | /// The file to use as the old changelog data to be appended to anything new
39 | /// found.
40 | pub infile: Option,
41 | /// The subtitle for the release
42 | pub subtitle: Option,
43 | /// The file to use as the changelog output file (Defaults to `stdout`)
44 | pub outfile: Option,
45 | /// Maps out the sections and aliases used to trigger those sections. The
46 | /// keys are the section name, and the values are an array of aliases.
47 | pub section_map: IndexMap>,
48 | /// Maps out the components and aliases used to trigger those components.
49 | /// The keys are the component name, and the values are an array of
50 | /// aliases.
51 | pub component_map: HashMap>,
52 | /// The git dir with all the meta-data (Typically the `.git` sub-directory
53 | /// of the project)
54 | pub git_dir: Option,
55 | /// The format to output the changelog in (Defaults to Markdown)
56 | pub out_format: ChangelogFormat,
57 | /// The grep search pattern used to find commits we are interested in
58 | /// (Defaults to: "^ft|^feat|^fx|^fix|^perf|^unk|BREAKING\'")
59 | pub grep: String,
60 | /// The format of the commit output from `git log` (Defaults to:
61 | /// "%H%n%s%n%b%n==END==")
62 | pub format: String,
63 | /// The working directory of the git project (typically the project
64 | /// directory, or parent of the `.git` directory)
65 | pub git_work_tree: Option,
66 | /// The regex used to get components, aliases, and messages
67 | pub regex: Regex,
68 | /// The regex used to get closes issue links
69 | pub closes_regex: Regex,
70 | /// The regex used to get closes issue links
71 | pub breaks_regex: Regex,
72 | pub breaking_regex: Regex,
73 | /// Where to start looking for commits using a hash (or short hash)
74 | pub from: Option,
75 | /// Where to stop looking for commits using a hash (or short hash).
76 | /// (Defaults to `HEAD`)
77 | pub to: String,
78 | /// The version tag for the release (Defaults to the short hash of the
79 | /// latest commit)
80 | pub version: Option,
81 | /// Whether or not this is a patch version update or not. Patch versions use
82 | /// a lower markdown header (`###` instead of `##` for major and minor
83 | /// releases)
84 | pub patch_ver: bool,
85 | }
86 |
87 | impl Default for Clog {
88 | fn default() -> Self {
89 | debug!("Creating default clog with Clog::default()");
90 | let mut sections = IndexMap::new();
91 | sections.insert(
92 | "Features".to_owned(),
93 | vec!["ft".to_owned(), "feat".to_owned()],
94 | );
95 | sections.insert(
96 | "Bug Fixes".to_owned(),
97 | vec!["fx".to_owned(), "fix".to_owned()],
98 | );
99 | sections.insert("Performance".to_owned(), vec!["perf".to_owned()]);
100 | sections.insert("Unknown".to_owned(), vec!["unk".to_owned()]);
101 | sections.insert("Breaking Changes".to_owned(), vec!["breaks".to_owned()]);
102 |
103 | Clog {
104 | grep: format!(
105 | "{}BREAKING'",
106 | sections
107 | .values()
108 | .map(|v| v
109 | .iter()
110 | .fold(String::new(), |acc, al| { acc + &format!("^{}|", al)[..] }))
111 | .fold(String::new(), |acc, al| { acc + &format!("^{}|", al)[..] })
112 | ),
113 | format: "%H%n%s%n%b%n==END==".to_string(),
114 | repo: None,
115 | link_style: LinkStyle::Github,
116 | version: None,
117 | patch_ver: false,
118 | subtitle: None,
119 | from: None,
120 | to: "HEAD".to_string(),
121 | infile: None,
122 | outfile: None,
123 | section_map: sections,
124 | component_map: HashMap::new(),
125 | out_format: ChangelogFormat::Markdown,
126 | git_dir: None,
127 | git_work_tree: None,
128 | regex: regex_default(),
129 | closes_regex: closes_regex_default(),
130 | breaks_regex: breaks_regex_default(),
131 | breaking_regex: breaking_regex_default(),
132 | }
133 | }
134 | }
135 |
136 | impl TryFrom for Clog {
137 | type Error = Error;
138 |
139 | fn try_from(cfg: RawCfg) -> StdResult {
140 | let mut clog = Self {
141 | repo: cfg.clog.repository,
142 | link_style: cfg.clog.link_style,
143 | subtitle: cfg.clog.subtitle,
144 | infile: cfg.clog.changelog.clone().or(cfg.clog.infile),
145 | outfile: cfg.clog.changelog.or(cfg.clog.outfile),
146 | section_map: cfg.sections,
147 | component_map: cfg.components,
148 | out_format: cfg.clog.output_format,
149 | git_dir: cfg.clog.git_dir,
150 | git_work_tree: cfg.clog.git_work_tree,
151 | ..Self::default()
152 | };
153 | if cfg.clog.from_latest_tag {
154 | clog.from = Some(clog.get_latest_tag()?);
155 | }
156 | Ok(clog)
157 | }
158 | }
159 |
160 | impl Clog {
161 | /// Creates a default `Clog` struct using the current working directory and
162 | /// searches for the default `.clog.toml` configuration file.
163 | ///
164 | /// # Example
165 | ///
166 | /// ```no_run
167 | /// # use clog::Clog;
168 | /// let clog = Clog::new().unwrap();
169 | /// ```
170 | pub fn new() -> Result {
171 | debug!("Creating default clog with new()");
172 | debug!("Trying default config file");
173 | Clog::from_config(DEFAULT_CONFIG_FILE)
174 | }
175 |
176 | /// Creates a `Clog` struct using a specific git working directory OR
177 | /// project directory. Searches for the default configuration TOML file
178 | /// `.clog.toml`
179 | ///
180 | /// **NOTE:** If you specify a `.git` folder the parent will be used as the
181 | /// working tree, and vice versa.
182 | ///
183 | /// # Example
184 | ///
185 | /// ```no_run
186 | /// # use clog::Clog;
187 | /// let clog = Clog::with_git_work_tree("/myproject").unwrap();
188 | /// ```
189 | pub fn with_git_work_tree>(dir: P) -> Result {
190 | debug!("Creating clog with \n\tdir: {:?}", dir.as_ref());
191 | Clog::_new(Some(dir.as_ref()), None)
192 | }
193 |
194 | /// Creates a `Clog` struct a custom named TOML configuration file. Sets the
195 | /// parent directory of the configuration file to the working tree and
196 | /// sibling `.git` directory as the git directory.
197 | ///
198 | /// **NOTE:** If you specify a `.git` folder the parent will be used as the
199 | /// working tree, and vice versa.
200 | ///
201 | /// # Example
202 | ///
203 | /// ```no_run
204 | /// # use clog::Clog;
205 | /// let clog = Clog::from_config("/myproject/clog_conf.toml").unwrap();
206 | /// ```
207 | pub fn from_config>(cfg: P) -> Result {
208 | debug!("Creating clog with \n\tfile: {:?}", cfg.as_ref());
209 | Clog::_new(None, Some(cfg.as_ref()))
210 | }
211 |
212 | fn _new(dir: Option<&Path>, cfg: Option<&Path>) -> Result {
213 | debug!("Creating private clog with \n\tdir: {:?}", dir);
214 | // Determine if the cfg_file was relative or not
215 | let cfg = if let Some(cfg) = cfg {
216 | if cfg.is_relative() {
217 | debug!("file is relative");
218 | let cwd = match env::current_dir() {
219 | Ok(d) => d,
220 | Err(..) => return Err(Error::CurrentDir),
221 | };
222 | Path::new(&cwd).join(cfg)
223 | } else {
224 | debug!("file is absolute");
225 | cfg.to_path_buf()
226 | }
227 | } else {
228 | Path::new(DEFAULT_CONFIG_FILE).to_path_buf()
229 | };
230 |
231 | // if dir is None we assume whatever dir the cfg file is also contains the git
232 | // metadata
233 | let mut dir = dir.unwrap_or(&cfg).to_path_buf();
234 | dir.pop();
235 | let git_dir;
236 | let git_work_tree;
237 | if dir.ends_with(".git") {
238 | debug!("dir ends with .git");
239 | let mut wd = dir.clone();
240 | git_dir = Some(wd.clone());
241 | wd.pop();
242 | git_work_tree = Some(wd);
243 | } else {
244 | debug!("dir doesn't end with .git");
245 | let mut gd = dir.clone();
246 | git_work_tree = Some(gd.clone());
247 | gd.push(".git");
248 | git_dir = Some(gd);
249 | }
250 | Ok(Clog {
251 | git_dir,
252 | git_work_tree,
253 | ..Clog::try_config_file(&cfg)?
254 | })
255 | }
256 |
257 | // Try and create a clog object from a config file
258 | fn try_config_file(cfg_file: &Path) -> Result {
259 | debug!("Trying to use config file: {:?}", cfg_file);
260 | let mut toml_f = File::open(cfg_file)?;
261 | let mut toml_s = String::with_capacity(100);
262 |
263 | toml_f.read_to_string(&mut toml_s)?;
264 |
265 | let cfg: RawCfg = toml::from_str(&toml_s[..])?;
266 | cfg.try_into()
267 | }
268 |
269 | /// Sets the grep search pattern for finding commits.
270 | ///
271 | /// # Example
272 | ///
273 | /// ```no_run
274 | /// # use clog::Clog;
275 | /// let clog = Clog::new().unwrap().grep("BREAKS");
276 | /// ```
277 | #[must_use]
278 | pub fn grep>(mut self, g: S) -> Clog {
279 | self.grep = g.into();
280 | self
281 | }
282 |
283 | /// Sets the format for `git log` output
284 | ///
285 | /// # Example
286 | ///
287 | /// ```no_run
288 | /// # use clog::Clog;
289 | /// let clog = Clog::new().unwrap().format("%H%n%n==END==");
290 | /// ```
291 | #[must_use]
292 | pub fn format>(mut self, f: S) -> Clog {
293 | self.format = f.into();
294 | self
295 | }
296 |
297 | /// Sets the repository used for the base of hyper-links
298 | ///
299 | /// **NOTE:** Leave off the trailing `.git`
300 | ///
301 | /// **NOTE:** Anything set here will override anything in a configuration
302 | /// TOML file
303 | ///
304 | /// # Example
305 | ///
306 | /// ```no_run
307 | /// # use clog::Clog;
308 | /// let clog = Clog::new()
309 | /// .unwrap()
310 | /// .repository("https://github.com/thoughtram/clog");
311 | /// ```
312 | #[must_use]
313 | pub fn repository>(mut self, r: S) -> Clog {
314 | self.repo = Some(r.into());
315 | self
316 | }
317 |
318 | /// Sets the link style to use for hyper-links
319 | ///
320 | /// **NOTE:** Anything set here will override anything in a configuration
321 | /// TOML file
322 | ///
323 | /// # Example
324 | ///
325 | /// ```no_run
326 | /// # use clog::{Clog, LinkStyle};
327 | /// let clog = Clog::new().unwrap().link_style(LinkStyle::Stash);
328 | /// ```
329 | #[must_use]
330 | pub fn link_style(mut self, l: LinkStyle) -> Clog {
331 | self.link_style = l;
332 | self
333 | }
334 |
335 | /// Sets the version for the release
336 | ///
337 | /// **NOTE:** Anything set here will override anything in a configuration
338 | /// TOML file
339 | ///
340 | /// # Example
341 | ///
342 | /// ```no_run
343 | /// # use clog::Clog;
344 | /// let clog = Clog::new().unwrap().version("v0.2.1-beta3");
345 | /// ```
346 | #[must_use]
347 | pub fn version>(mut self, v: S) -> Clog {
348 | self.version = Some(v.into());
349 | self
350 | }
351 |
352 | /// Sets the subtitle for the release
353 | ///
354 | /// # Example
355 | ///
356 | /// ```no_run
357 | /// # use clog::Clog;
358 | /// let clog = Clog::new().unwrap().subtitle("My Awesome Release Title");
359 | /// ```
360 | #[must_use]
361 | pub fn subtitle>(mut self, s: S) -> Clog {
362 | self.subtitle = Some(s.into());
363 | self
364 | }
365 |
366 | /// Sets how far back to begin searching commits using a short hash or full
367 | /// hash
368 | ///
369 | /// **NOTE:** Anything set here will override anything in a configuration
370 | /// TOML file
371 | ///
372 | /// # Example
373 | ///
374 | /// ```no_run
375 | /// # use clog::Clog;
376 | /// let clog = Clog::new().unwrap().from("6d8183f");
377 | /// ```
378 | #[must_use]
379 | pub fn from>(mut self, f: S) -> Clog {
380 | self.from = Some(f.into());
381 | self
382 | }
383 |
384 | /// Sets what point to stop searching for commits using a short hash or full
385 | /// hash (Defaults to `HEAD`)
386 | ///
387 | /// # Example
388 | ///
389 | /// ```no_run
390 | /// # use clog::Clog;
391 | /// let clog = Clog::new().unwrap().to("123abc4d");
392 | /// ```
393 | #[must_use]
394 | pub fn to>(mut self, t: S) -> Clog {
395 | self.to = t.into();
396 | self
397 | }
398 |
399 | /// Sets the changelog file to output or prepend to (Defaults to `stdout` if
400 | /// omitted)
401 | ///
402 | /// **NOTE:** Anything set here will override anything in a configuration
403 | /// TOML file
404 | ///
405 | /// # Example
406 | ///
407 | /// ```no_run
408 | /// # use clog::Clog;
409 | /// let clog = Clog::new().unwrap().changelog("/myproject/my_changelog.md");
410 | /// ```
411 | #[must_use]
412 | pub fn changelog + Clone>(mut self, c: S) -> Clog {
413 | self.infile = Some(c.clone().into());
414 | self.outfile = Some(c.into());
415 | self
416 | }
417 |
418 | /// Sets the changelog output file to output or prepend to (Defaults to
419 | /// `stdout` if omitted), this is useful inconjunction with
420 | /// `Clog::infile()` because it allows to read previous commits from one
421 | /// place and output to another.
422 | ///
423 | /// **NOTE:** Anything set here will override anything in a configuration
424 | /// TOML file
425 | ///
426 | /// **NOTE:** This should *not* be used in conjunction with
427 | /// `Clog::changelog()`
428 | ///
429 | /// # Example
430 | ///
431 | /// ```no_run
432 | /// # use clog::Clog;
433 | /// let clog = Clog::new().unwrap().outfile("/myproject/my_changelog.md");
434 | /// ```
435 | #[must_use]
436 | pub fn outfile>(mut self, c: S) -> Clog {
437 | self.outfile = Some(c.into());
438 | self
439 | }
440 |
441 | /// Sets the changelog input file to read previous commits or changelog data
442 | /// from. This is useful inconjunction with `Clog::infile()` because it
443 | /// allows to read previous commits from one place and output to
444 | /// another.
445 | ///
446 | /// **NOTE:** Anything set here will override anything in a configuration
447 | /// TOML file
448 | ///
449 | /// **NOTE:** This should *not* be used in conjunction with
450 | /// `Clog::changelog()`
451 | ///
452 | /// # Example
453 | ///
454 | /// ```no_run
455 | /// # use clog::Clog;
456 | /// let clog = Clog::new()
457 | /// .unwrap()
458 | /// .infile("/myproject/my_old_changelog.md");
459 | /// ```
460 | #[must_use]
461 | pub fn infile>(mut self, c: S) -> Clog {
462 | self.infile = Some(c.into());
463 | self
464 | }
465 |
466 | /// Sets the `git` metadata directory (typically `.git` child of your
467 | /// project working tree)
468 | ///
469 | /// # Example
470 | ///
471 | /// ```no_run
472 | /// # use clog::Clog;
473 | /// let clog = Clog::new().unwrap().git_dir("/myproject/.git");
474 | /// ```
475 | #[must_use]
476 | pub fn git_dir>(mut self, d: P) -> Clog {
477 | self.git_dir = Some(d.as_ref().to_path_buf());
478 | self
479 | }
480 |
481 | /// Sets the `git` working tree directory (typically your project directory)
482 | ///
483 | /// # Example
484 | ///
485 | /// ```no_run
486 | /// # use clog::Clog;
487 | /// let clog = Clog::new().unwrap().git_work_tree("/myproject");
488 | /// ```
489 | #[must_use]
490 | pub fn git_work_tree>(mut self, d: P) -> Clog {
491 | self.git_work_tree = Some(d.as_ref().to_path_buf());
492 | self
493 | }
494 |
495 | /// Sets whether or not this is a patch release (defaults to `false`)
496 | ///
497 | /// **NOTE:** Setting this to true will cause the release subtitle to use a
498 | /// smaller markdown heading
499 | ///
500 | /// # Example
501 | ///
502 | /// ```no_run
503 | /// # use clog::Clog;
504 | /// let clog = Clog::new().unwrap().patch_ver(true);
505 | /// ```
506 | #[must_use]
507 | pub fn patch_ver(mut self, p: bool) -> Clog {
508 | self.patch_ver = p;
509 | self
510 | }
511 |
512 | /// The format of output for the changelog (Defaults to Markdown)
513 | ///
514 | /// # Example
515 | ///
516 | /// ```no_run
517 | /// # use clog::{fmt::ChangelogFormat,Clog};
518 | /// let clog = Clog::new().unwrap().output_format(ChangelogFormat::Json);
519 | /// ```
520 | #[must_use]
521 | pub fn output_format(mut self, f: ChangelogFormat) -> Clog {
522 | self.out_format = f;
523 | self
524 | }
525 |
526 | /// Retrieves a `Vec` of only commits we care about.
527 | ///
528 | /// # Example
529 | ///
530 | /// ```no_run
531 | /// # use clog::Clog;
532 | /// let clog = Clog::new().unwrap();
533 | /// let commits = clog.get_commits();
534 | /// ```
535 | pub fn get_commits(&self) -> Result {
536 | let range = if let Some(from) = self.from.as_ref() {
537 | format!("{from}..{}", self.to)
538 | } else {
539 | "HEAD".to_owned()
540 | };
541 |
542 | let output = Command::new("git")
543 | .arg(&self.get_git_dir()[..])
544 | .arg(&self.get_git_work_tree()[..])
545 | .arg("log")
546 | .arg("-E")
547 | .arg(&format!("--grep={}", self.grep))
548 | .arg(&format!("--format={}", self.format))
549 | .arg(&range)
550 | .output()?;
551 |
552 | Ok(String::from_utf8_lossy(&output.stdout)
553 | .split("\n==END==\n")
554 | .filter_map(|commit_str| self.parse_raw_commit(commit_str).ok())
555 | .filter(|entry| entry.commit_type != "Unknown")
556 | .collect())
557 | }
558 |
559 | #[doc(hidden)]
560 | pub fn parse_raw_commit(&self, commit_str: &str) -> Result {
561 | let mut lines = commit_str.lines();
562 | let hash = lines.next().unwrap_or_default();
563 |
564 | let (subject, component, commit_type) =
565 | match lines.next().and_then(|s| self.regex.captures(s)) {
566 | Some(caps) => {
567 | let section = caps.get(1).map(|c| c.as_str()).unwrap_or_default();
568 | let commit_type = self
569 | .section_for(section)
570 | .ok_or(Error::UnknownComponent(section.into()))?;
571 | let component = caps.get(2).map(|component| {
572 | let component = component.as_str();
573 | match self.component_for(component) {
574 | Some(alias) => alias.clone(),
575 | None => component.to_owned(),
576 | }
577 | });
578 | let subject = caps.get(3).map(|c| c.as_str());
579 | (subject, component, commit_type)
580 | }
581 | None => (
582 | None,
583 | None,
584 | self.section_for("unk")
585 | .ok_or(Error::UnknownComponent("unk".into()))?,
586 | ),
587 | };
588 | let mut closes = vec![];
589 | let mut breaks = vec![];
590 | for line in lines {
591 | if let Some(caps) = self.closes_regex.captures(line) {
592 | if let Some(cap) = caps.get(2) {
593 | closes.push(cap.as_str().to_owned());
594 | }
595 | }
596 | if let Some(caps) = self.breaks_regex.captures(line) {
597 | if let Some(cap) = caps.get(2) {
598 | breaks.push(cap.as_str().to_owned());
599 | }
600 | } else if self.breaking_regex.captures(line).is_some() {
601 | breaks.push(String::new());
602 | }
603 | }
604 |
605 | Ok(Commit {
606 | hash: hash.to_string(),
607 | subject: subject.unwrap().to_owned(),
608 | component: component.unwrap_or_default(),
609 | closes,
610 | breaks,
611 | commit_type: commit_type.to_string(),
612 | })
613 | }
614 |
615 | /// Retrieves the latest tag from the git directory
616 | ///
617 | /// # Example
618 | ///
619 | /// ```no_run
620 | /// # use clog::Clog;
621 | /// let clog = Clog::new().unwrap();
622 | /// let tag = clog.get_latest_tag().unwrap();
623 | /// ```
624 | pub fn get_latest_tag(&self) -> Result {
625 | let output = Command::new("git")
626 | .arg(&self.get_git_dir()[..])
627 | .arg(&self.get_git_work_tree()[..])
628 | .arg("rev-list")
629 | .arg("--tags")
630 | .arg("--max-count=1")
631 | .output()?;
632 | let buf = String::from_utf8_lossy(&output.stdout);
633 |
634 | Ok(buf.trim_matches('\n').to_owned())
635 | }
636 |
637 | /// Retrieves the latest tag version from the git directory
638 | ///
639 | /// # Example
640 | ///
641 | /// ```no_run
642 | /// # use clog::Clog;
643 | /// let clog = Clog::new().unwrap();
644 | /// let tag_ver = clog.get_latest_tag_ver();
645 | /// ```
646 | pub fn get_latest_tag_ver(&self) -> String {
647 | let output = Command::new("git")
648 | .arg(&self.get_git_dir()[..])
649 | .arg(&self.get_git_work_tree()[..])
650 | .arg("describe")
651 | .arg("--tags")
652 | .arg("--abbrev=0")
653 | .output()
654 | .unwrap_or_else(|e| panic!("Failed to run 'git describe' with error: {}", e));
655 |
656 | String::from_utf8_lossy(&output.stdout).into_owned()
657 | }
658 |
659 | /// Retrieves the hash of the most recent commit from the git directory
660 | /// (i.e. HEAD)
661 | ///
662 | /// # Example
663 | ///
664 | /// ```no_run
665 | /// # use clog::Clog;
666 | /// let clog = Clog::new().unwrap();
667 | /// let head_hash = clog.get_last_commit();
668 | /// ```
669 | pub fn get_last_commit(&self) -> String {
670 | let output = Command::new("git")
671 | .arg(&self.get_git_dir()[..])
672 | .arg(&self.get_git_work_tree()[..])
673 | .arg("rev-parse")
674 | .arg("HEAD")
675 | .output()
676 | .unwrap_or_else(|e| panic!("Failed to run 'git rev-parse' with error: {}", e));
677 |
678 | String::from_utf8_lossy(&output.stdout).into_owned()
679 | }
680 |
681 | fn get_git_work_tree(&self) -> String {
682 | // Check if user supplied a local git dir and working tree
683 | if self.git_work_tree.is_none() && self.git_dir.is_none() {
684 | // None was provided
685 | "".to_owned()
686 | } else if self.git_dir.is_some() {
687 | // user supplied both
688 | format!(
689 | "--work-tree={}",
690 | self.git_work_tree.clone().unwrap().to_str().unwrap()
691 | )
692 | } else {
693 | // user only supplied a working tree i.e. /home/user/mycode
694 | let mut w = self.git_work_tree.clone().unwrap();
695 | w.pop();
696 | format!("--work-tree={}", w.to_str().unwrap())
697 | }
698 | }
699 |
700 | fn get_git_dir(&self) -> String {
701 | // Check if user supplied a local git dir and working tree
702 | if self.git_dir.is_none() && self.git_work_tree.is_none() {
703 | // None was provided
704 | "".to_owned()
705 | } else if self.git_work_tree.is_some() {
706 | // user supplied both
707 | format!(
708 | "--git-dir={}",
709 | self.git_dir.clone().unwrap().to_str().unwrap()
710 | )
711 | } else {
712 | // user only supplied a git dir i.e. /home/user/mycode/.git
713 | let mut g = self.git_dir.clone().unwrap();
714 | g.push(".git");
715 | format!("--git-dir={}", g.to_str().unwrap())
716 | }
717 | }
718 |
719 | /// Retrieves the section title for a given alias
720 | ///
721 | /// # Example
722 | ///
723 | /// ```no_run
724 | /// # use clog::Clog;
725 | /// let clog = Clog::new().unwrap();
726 | /// let section = clog.section_for("feat");
727 | /// assert_eq!(Some("Features"), section);
728 | /// ```
729 | pub fn section_for(&self, alias: &str) -> Option<&str> {
730 | self.section_map
731 | .iter()
732 | .find(|&(_, v)| v.iter().any(|s| s == alias))
733 | .map(|(k, _)| &**k)
734 | }
735 |
736 | /// Retrieves the full component name for a given alias (if one is defined)
737 | ///
738 | /// # Example
739 | ///
740 | /// ```no_run
741 | /// # use clog::Clog;
742 | /// let clog = Clog::new().unwrap();
743 | /// let component = clog.component_for("will_be_none");
744 | /// assert_eq!(None, component);
745 | /// ```
746 | pub fn component_for(&self, alias: &str) -> Option<&String> {
747 | self.component_map
748 | .iter()
749 | .filter(|&(_, v)| v.iter().any(|c| c == alias))
750 | .map(|(k, _)| k)
751 | .next()
752 | }
753 |
754 | /// Writes the changelog using whatever options have been specified thus
755 | /// far.
756 | ///
757 | /// # Example
758 | ///
759 | /// ```no_run
760 | /// # use clog::Clog;
761 | /// let clog = Clog::new().unwrap();
762 | /// clog.write_changelog();
763 | /// ```
764 | pub fn write_changelog(&self) -> Result<()> {
765 | debug!("Writing changelog with preset options");
766 | if let Some(ref cl) = self.outfile {
767 | debug!("outfile set to: {:?}", cl);
768 | self.write_changelog_to(cl)
769 | } else if let Some(ref cl) = self.infile {
770 | debug!("outfile not set but infile set to: {:?}", cl);
771 | self.write_changelog_from(cl)
772 | } else {
773 | debug!("outfile and infile not set using stdout");
774 | let out = stdout();
775 | let mut out_buf = BufWriter::new(out.lock());
776 | match self.out_format {
777 | ChangelogFormat::Markdown => {
778 | let mut writer = MarkdownWriter::new(&mut out_buf);
779 | self.write_changelog_with(&mut writer)
780 | }
781 | ChangelogFormat::Json => {
782 | let mut writer = JsonWriter::new(&mut out_buf);
783 | self.write_changelog_with(&mut writer)
784 | }
785 | }
786 | }
787 | }
788 |
789 | /// Writes the changelog to a specified file, and prepends new commits if
790 | /// file exists, or creates the file if it doesn't.
791 | ///
792 | /// # Example
793 | ///
794 | /// ```no_run
795 | /// # use clog::Clog;
796 | /// let clog = Clog::new().unwrap();
797 | ///
798 | /// clog.write_changelog_to("/myproject/new_changelog.md")
799 | /// .unwrap();
800 | /// ```
801 | pub fn write_changelog_to>(&self, cl: P) -> Result<()> {
802 | debug!("Writing changelog to file: {:?}", cl.as_ref());
803 | let mut contents = String::with_capacity(256);
804 | if let Some(ref infile) = self.infile {
805 | debug!("infile set to: {:?}", infile);
806 | File::open(infile)
807 | .map(|mut f| f.read_to_string(&mut contents).ok())
808 | .ok();
809 | } else {
810 | debug!("infile not set, trying the outfile");
811 | File::open(cl.as_ref())
812 | .map(|mut f| f.read_to_string(&mut contents).ok())
813 | .ok();
814 | }
815 | contents.shrink_to_fit();
816 |
817 | let mut file = File::create(cl.as_ref())?;
818 | match self.out_format {
819 | ChangelogFormat::Markdown => {
820 | let mut writer = MarkdownWriter::new(&mut file);
821 | self.write_changelog_with(&mut writer)?;
822 | }
823 | ChangelogFormat::Json => {
824 | let mut writer = JsonWriter::new(&mut file);
825 | self.write_changelog_with(&mut writer)?;
826 | }
827 | }
828 | write!(&mut file, "\n\n\n")?;
829 |
830 | file.write_all(contents.as_bytes())?;
831 |
832 | Ok(())
833 | }
834 |
835 | /// Writes the changelog from a specified input file, and appends new
836 | /// commits
837 | ///
838 | /// # Example
839 | ///
840 | /// ```no_run
841 | /// # use clog::Clog;
842 | /// let clog = Clog::new().unwrap();
843 | ///
844 | /// clog.write_changelog_from("/myproject/new_old_changelog.md")
845 | /// .unwrap();
846 | /// ```
847 | pub fn write_changelog_from>(&self, cl: P) -> Result<()> {
848 | debug!("Writing changelog from file: {:?}", cl.as_ref());
849 | let mut contents = String::with_capacity(256);
850 | File::open(cl.as_ref())
851 | .map(|mut f| f.read_to_string(&mut contents).ok())
852 | .ok();
853 | contents.shrink_to_fit();
854 |
855 | if let Some(ref ofile) = self.outfile {
856 | debug!("outfile set to: {:?}", ofile);
857 | let mut file = File::create(ofile)?;
858 | match self.out_format {
859 | ChangelogFormat::Markdown => {
860 | let mut writer = MarkdownWriter::new(&mut file);
861 | self.write_changelog_with(&mut writer)?;
862 | }
863 | ChangelogFormat::Json => {
864 | let mut writer = JsonWriter::new(&mut file);
865 | self.write_changelog_with(&mut writer)?;
866 | }
867 | }
868 | file.write_all(contents.as_bytes())?;
869 | } else {
870 | debug!("outfile not set, using stdout");
871 | let out = stdout();
872 | let mut out_buf = BufWriter::new(out.lock());
873 | {
874 | match self.out_format {
875 | ChangelogFormat::Markdown => {
876 | let mut writer = MarkdownWriter::new(&mut out_buf);
877 | self.write_changelog_with(&mut writer)?;
878 | }
879 | ChangelogFormat::Json => {
880 | let mut writer = JsonWriter::new(&mut out_buf);
881 | self.write_changelog_with(&mut writer)?;
882 | }
883 | }
884 | }
885 | write!(&mut out_buf, "\n\n\n")?;
886 |
887 | out_buf.write_all(contents.as_bytes())?;
888 | }
889 |
890 | Ok(())
891 | }
892 |
893 | /// Writes a changelog with a specified `FormatWriter` format
894 | ///
895 | /// # Examples
896 | ///
897 | /// ```no_run
898 | /// # use clog::{Clog, fmt::{FormatWriter, MarkdownWriter}};
899 | /// # use std::io;
900 | /// let clog = Clog::new().unwrap();
901 | ///
902 | /// // Write changelog to stdout in Markdown format
903 | /// let out = io::stdout();
904 | /// let mut out_buf = io::BufWriter::new(out.lock());
905 | /// let mut writer = MarkdownWriter::new(&mut out_buf);
906 | ///
907 | /// clog.write_changelog_with(&mut writer).unwrap();
908 | /// ```
909 | pub fn write_changelog_with(&self, writer: &mut W) -> Result<()>
910 | where
911 | W: FormatWriter,
912 | {
913 | debug!("Writing changelog from writer");
914 | let sm = SectionMap::from_commits(self.get_commits()?);
915 |
916 | writer.write_changelog(self, &sm)
917 | }
918 | }
919 |
--------------------------------------------------------------------------------