├── 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 | --------------------------------------------------------------------------------