From 329e119a0326e54cdf4d669b58f835ebb111d47f Mon Sep 17 00:00:00 2001 From: Kevin K Date: Thu, 23 Apr 2015 19:25:23 -0400 Subject: [PATCH 1/5] feat(clog): auto increment version with --major, --minor, or --patch Only compatible with semver at the moment Closes #19 --- Cargo.lock | 18 ++++++++++++------ Cargo.toml | 1 + src/git.rs | 18 +++++++++++++++--- src/main.rs | 41 +++++++++++++++++++++++++++++++++++------ 4 files changed, 63 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3b22e15..97b2b5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,14 +2,15 @@ name = "clog" version = "0.3.2" dependencies = [ - "clap 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)", + "clap 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", + "semver 0.1.19 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "clap" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -24,12 +25,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "regex" -version = "0.1.28" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "semver" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "time" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "gcc 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index 9e0741e..ac5d740 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,3 +13,4 @@ description = "A conventional changelog for the rest of us" regex = "*" time = "*" clap = "*" +semver = "*" diff --git a/src/git.rs b/src/git.rs index ffc8fa7..02b468e 100644 --- a/src/git.rs +++ b/src/git.rs @@ -3,6 +3,8 @@ use common:: { LogEntry }; use common::CommitType; use std::borrow::ToOwned; +use semver; + #[derive(Debug)] pub struct LogReaderConfig { pub grep: String, @@ -16,17 +18,27 @@ pub fn get_latest_tag () -> String { .arg("rev-list") .arg("--tags") .arg("--max-count=1") - .output().unwrap_or_else(|e| panic!("Failed to run git rev-list with error: {}",e)); + .output().unwrap_or_else(|e| panic!("Failed to run 'git rev-list' with error: {}",e)); let buf = String::from_utf8_lossy(&output.stdout); buf.trim_matches('\n').to_owned() } +pub fn get_latest_tag_ver () -> Result { + let output = Command::new("git") + .arg("describe") + .arg("--tags") + .arg("--abbrev=0") + .output().unwrap_or_else(|e| panic!("Failed to run 'git describe' with error: {}",e)); + + semver::Version::parse(&String::from_utf8_lossy(&output.stdout)[..]) +} + pub fn get_last_commit () -> String { let output = Command::new("git") .arg("rev-parse") .arg("HEAD") - .output().unwrap_or_else(|e| panic!("Failed to run git rev-parse with error: {}", e)); + .output().unwrap_or_else(|e| panic!("Failed to run 'git rev-parse' with error: {}", e)); String::from_utf8_lossy(&output.stdout).into_owned() } @@ -44,7 +56,7 @@ pub fn get_log_entries (config:LogReaderConfig) -> Vec{ .arg(&format!("--grep={}",config.grep)) .arg(&format!("--format={}", "%H%n%s%n%b%n==END==")) .arg(&range) - .output().unwrap_or_else(|e| panic!("Failed to run git log with error: {}", e)); + .output().unwrap_or_else(|e| panic!("Failed to run 'git log' with error: {}", e)); String::from_utf8_lossy(&output.stdout) .split("\n==END==\n") diff --git a/src/main.rs b/src/main.rs index 4aad62c..c201f25 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ extern crate regex; extern crate time; +extern crate semver; #[macro_use] extern crate clap; @@ -32,14 +33,18 @@ fn main () { .version(&crate_version!()[..]) .about("a conventional changelog for the rest of us") .args_from_usage("-r --repository=[repository] 'e.g. https://github.com/thoughtram/clog' - --setversion=[setversion] 'e.g. 1.0.1' --from=[from] 'e.g. 12a8546' + --major 'Increment major version by one (Sets minor and patch to 0)' + --minor 'Increment minor version by one (Sets patch to 0)' + --patch 'Increment patch version by one' --subtitle=[subtitle] 'e.g. crazy-release-title' --to=[to] 'e.g. 8057684 (Defaults to HEAD when omitted)'") // Because --from-latest-tag can't be used with --from, we add it seperately so we can // specify a .mutually_excludes() .arg(Arg::from_usage("--from-latest-tag 'use latest tag as start (instead of --from)'") .mutually_excludes("from")) + .arg(Arg::from_usage("--setversion=[setversion] 'e.g. 1.0.1'") + .mutually_excludes_all(vec!["major", "minor", "patch"])) .get_matches(); let start_nsec = time::get_time().nsec; @@ -51,6 +56,34 @@ fn main () { to: matches.value_of("to").unwrap_or("").to_owned() }; + // compute version early, so we can exit on error + let version = { + // less typing later... + let (major, minor, patch) = (matches.is_present("major"), matches.is_present("minor"), matches.is_present("patch")); + if matches.is_present("setversion") { + matches.value_of("setversion").unwrap().to_owned() + } else if major || minor || patch { + match git::get_latest_tag_ver() { + Ok(ref mut v) => { + // if-else may be quicker, but it's longer mentally, and this isn't slow + match (major, minor, patch) { + (true,_,_) => { v.major += 1; v.minor = 0; v.patch = 0; }, + (_,true,_) => { v.minor += 1; v.patch = 0; }, + (_,_,true) => { v.patch += 1; }, + _ => unreachable!() + } + format!("{}", v) + }, + Err(e) => { + println!("Error parsing latest version: {}\nTry setting the version manually with --setversion=[version]", e ); + std::process::exit(1); + } + } + } else { + format_util::get_short_hash(&git::get_last_commit()[..]).to_owned() + } + }; + let commits = git::get_log_entries(log_reader_config); let sections = section_builder::build_sections(commits.clone()); @@ -62,11 +95,7 @@ fn main () { let mut file = File::create(&Path::new("changelog.md")).ok().unwrap(); let mut writer = LogWriter::new(&mut file, LogWriterOptions { repository_link: matches.value_of("repository").unwrap_or(""), - version: if matches.is_present("setversion") { - matches.value_of("setversion").unwrap().to_owned() - } else { - format_util::get_short_hash(&git::get_last_commit()[..]).to_owned() - }, + version: version, subtitle: matches.value_of("subtitle").unwrap_or("").to_owned() }); From 1a3fff95e1a9e22cbbe775256481d4b84945eb9a Mon Sep 17 00:00:00 2001 From: Kevin K Date: Thu, 23 Apr 2015 20:01:42 -0400 Subject: [PATCH 2/5] chore(.gitignore): add temp files to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index b2d0b50..d5c842c 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ #Others .DS_Store + +# Temporary files +.*~ From 8545a687de2399e1c80ef61ee52790f7b165909c Mon Sep 17 00:00:00 2001 From: Kevin K Date: Thu, 23 Apr 2015 21:20:50 -0400 Subject: [PATCH 3/5] docs(README.md): update readme with new usage --- README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 67cb3e5..bb53115 100644 --- a/README.md +++ b/README.md @@ -14,16 +14,20 @@ USAGE: clog [FLAGS] [OPTIONS] FLAGS: - --from-latest-tag uses the latest tag as starting point (ignores other --from parameters) + --from-latest-tag use latest tag as start (instead of --from) + -h, --help Prints help information + --major Increment major version by one (Sets minor and patch to 0) + --minor Increment minor version by one (Sets patch to 0) + --patch Increment patch version by one -v, --version Prints version information - -h, --help Prints this message OPTIONS: - --subtitle=subtitle e.g. crazy-release-title + --from=from e.g. 12a8546 -r, --repository=repository e.g. https://github.com/thoughtram/clog --setversion=setversion e.g. 1.0.1 + --subtitle=subtitle e.g. crazy-release-title --to=to e.g. 8057684 (Defaults to HEAD when omitted) - --from=from e.g. 12a8546 + ``` Try it! From 44f7d49389cfae40ad09033c6deaf54852d75d70 Mon Sep 17 00:00:00 2001 From: Kevin K Date: Fri, 24 Apr 2015 00:09:53 -0400 Subject: [PATCH 4/5] feat(aliases): implement aliases for commit types This is a decent change. Now the CommitType enum is auto-generated via a macro. This macro also implements two helper methods, and one Trait. The helper methods return a Vec<'static str> of all "aliases" for specified variant if called like CommitType::Variant.aliases(). Or if called on the enum itself there the method returns ALL aliases such as CommitType::all_aliases(). The macro also implments the std::str::FromStr trait so you can parse from a string to a known enum type automatically. Say we defined a "feat" or "ft" alias of variant "Feature", if we had a string of "ft" or "feature" we could call some_str.parse::().unwrap() to get a CommitType::Feature, or if it failed to parse we get a CommitType::Unknown Closes #3 --- src/common.rs | 16 +++++++++++----- src/git.rs | 10 +++++----- src/macros.rs | 46 ++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 15 +++++++-------- 4 files changed, 69 insertions(+), 18 deletions(-) create mode 100644 src/macros.rs diff --git a/src/common.rs b/src/common.rs index 0c999f8..13685ea 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,11 +1,17 @@ use std::fmt; use std::collections::HashMap; -#[derive(Debug, PartialEq, Clone)] -pub enum CommitType { - Feature, - Fix, - Unknown +// Creates an enum where the poritions inside the '(' and ')' act as aliases for that +// commit type. The only one you MUST specify is 'Unknown ()' +// +// Later you can call CommitType::Fix.aliases() to get all the aliases as a Vec<'statci str> +commit_type_enum!{ + #[derive(Debug, PartialEq, Clone)] + pub enum CommitType { + Feature ( feat, ft ), + Fix ( fix, fx), + Unknown () + } } #[derive(Clone)] diff --git a/src/git.rs b/src/git.rs index 02b468e..2e0dc06 100644 --- a/src/git.rs +++ b/src/git.rs @@ -5,6 +5,7 @@ use std::borrow::ToOwned; use semver; + #[derive(Debug)] pub struct LogReaderConfig { pub grep: String, @@ -75,11 +76,10 @@ fn parse_raw_commit(commit_str:&str) -> LogEntry { let (subject, component, commit_type) = match lines.next().and_then(|s| commit_pattern.captures(s)) { Some(caps) => { - let commit_type = match caps.at(1) { - Some("feat") => CommitType::Feature, - Some("fix") => CommitType::Fix, - _ => CommitType::Unknown - }; + // The macro that made the CommitType automatically implements std::str::FromStr + // with all aliases or falls back to CommitType::Unknown on failure so we can + // call unwrap(). + let commit_type = caps.at(1).unwrap_or("").parse::().unwrap(); let component = caps.at(2); let subject = caps.at(3); (subject, component, commit_type) diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..d6982e3 --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,46 @@ +// regex cheat thanks to https://github.com/BurntSushi +macro_rules! regex( + ($s:expr) => (::regex::Regex::new($s).unwrap()); +); + +// A macro creating an entry types, and their aliases +// +// This is a little hacky, because it expects an Unknown () variant +// +// TODO: de-dup with recursive calls +macro_rules! commit_type_enum { + (#[derive($($d:ident),+)] pub enum $e:ident { $($v:ident ( $($a:ident),* ) ),+ }) => { + #[derive($($d,)+)] + pub enum $e { + $($v,)+ + } + + impl $e { + #[allow(dead_code)] + pub fn aliases(&self) -> Vec<&'static str> { + match *self { + $($e::$v => vec![ + $( stringify!($a) ),* + ],)+ + } + } + #[allow(dead_code)] + pub fn all_aliases() -> Vec<&'static str> { + vec![ + $( $( stringify!($a),)* )+ + ] + } + } + impl ::std::str::FromStr for $e { + type Err = $e; + + #[allow(dead_code)] + fn from_str(s: &str) -> Result { + match s { + $(stringify!($v) $( | stringify!($a) )* => Ok($e::$v),)+ + _ => Err($e::Unknown) + } + } + } + }; +} diff --git a/src/main.rs b/src/main.rs index c201f25..539065f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,20 +7,19 @@ extern crate semver; #[macro_use] extern crate clap; -use git::LogReaderConfig; -use log_writer::{ LogWriter, LogWriterOptions }; use std::fs::File; use std::io::Read; use std::path::Path; use std::borrow::ToOwned; -use clap::{App, Arg}; +use common::CommitType; +use git::LogReaderConfig; +use log_writer::{ LogWriter, LogWriterOptions }; -// regex cheat thanks to https://github.com/BurntSushi -macro_rules! regex( - ($s:expr) => (::regex::Regex::new($s).unwrap()); -); +use clap::{App, Arg}; +#[macro_use] +mod macros; mod common; mod git; mod log_writer; @@ -50,7 +49,7 @@ fn main () { let start_nsec = time::get_time().nsec; let log_reader_config = LogReaderConfig { - grep: "^feat|^fix|BREAKING'".to_owned(), + grep: format!("{}BREAKING'", CommitType::all_aliases().iter().fold(String::new(),|acc, al| acc + &format!("^{}|", al)[..])), format: "%H%n%s%n%b%n==END==".to_owned(), from: if matches.is_present("from-latest-tag") { Some(git::get_latest_tag()) } else { matches.value_of("from").map(|v| v.to_owned()) }, to: matches.value_of("to").unwrap_or("").to_owned() From bb3072b80416fb3c874845360e9d46704cd53c79 Mon Sep 17 00:00:00 2001 From: Kevin K Date: Sat, 25 Apr 2015 13:05:07 -0400 Subject: [PATCH 5/5] feat(config): support .clog.toml configuration file --- Cargo.lock | 18 +++++- Cargo.toml | 1 + src/clogconfig.rs | 132 +++++++++++++++++++++++++++++++++++++++++ src/common.rs | 9 +-- src/format_util.rs | 4 -- src/git.rs | 33 ++++------- src/log_writer.rs | 31 ++++------ src/macros.rs | 12 ++-- src/main.rs | 74 +++++++---------------- src/section_builder.rs | 38 ++++-------- 10 files changed, 218 insertions(+), 134 deletions(-) create mode 100644 src/clogconfig.rs delete mode 100644 src/format_util.rs diff --git a/Cargo.lock b/Cargo.lock index 97b2b5b..636acfd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,9 +3,10 @@ name = "clog" version = "0.3.2" dependencies = [ "clap 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.1.30 (registry+https://github.com/rust-lang/crates.io-index)", "semver 0.1.19 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", + "toml 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -25,7 +26,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "regex" -version = "0.1.29" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "rustc-serialize" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -42,3 +48,11 @@ dependencies = [ "libc 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "toml" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rustc-serialize 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)", +] + diff --git a/Cargo.toml b/Cargo.toml index ac5d740..85cde24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,4 @@ regex = "*" time = "*" clap = "*" semver = "*" +toml = "*" diff --git a/src/clogconfig.rs b/src/clogconfig.rs new file mode 100644 index 0000000..4039b35 --- /dev/null +++ b/src/clogconfig.rs @@ -0,0 +1,132 @@ +use std::fs::File; +use std::io::Read; +use std::path::Path; +use std::borrow::ToOwned; +use std::fmt::Display; +use std::env; + +use clap::ArgMatches; +use toml::{Value, Parser}; + +use git; +use common::CommitType; +use CLOG_CONFIG_FILE; + +pub struct ClogConfig { + pub grep: String, + pub format: String, + pub repo: String, + pub version: String, + pub subtitle: String, + pub from: String, + pub to: String, +} + +pub type ConfigResult = Result>; + +impl ClogConfig { + pub fn from_matches(matches: &ArgMatches) -> ConfigResult { + // compute version early, so we can exit on error + let version = { + // less typing later... + let (major, minor, patch) = (matches.is_present("major"), matches.is_present("minor"), matches.is_present("patch")); + if matches.is_present("setversion") { + matches.value_of("setversion").unwrap().to_owned() + } else if major || minor || patch { + match git::get_latest_tag_ver() { + Ok(ref mut v) => { + // if-else may be quicker, but it's longer mentally, and this isn't slow + match (major, minor, patch) { + (true,_,_) => { v.major += 1; v.minor = 0; v.patch = 0; }, + (_,true,_) => { v.minor += 1; v.patch = 0; }, + (_,_,true) => { v.patch += 1; }, + _ => unreachable!() + } + format!("v{}", v) + }, + Err(e) => { + return Err(Box::new(format!("Error: {}\n\n\tEnsure the tag format follows Semantic Versioning such as N.N.N\n\tor set the version manually with --setversion " , e ))); + } + } + } else { + // Use short hash + (&git::get_last_commit()[0..8]).to_owned() + } + }; + + let cwd = match env::current_dir() { + Ok(d) => d, + Err(e) => return Err(Box::new(e)), + }; + + let cfg_file = Path::new(&cwd).join(CLOG_CONFIG_FILE); + let mut toml_from_latest = None; + let mut toml_repo = None; + let mut toml_subtitle = None; + + if let Ok(ref mut toml_f) = File::open(cfg_file){ + let mut toml_s = String::with_capacity(100); + + if let Err(e) = toml_f.read_to_string(&mut toml_s) { + return Err(Box::new(e)) + } + + toml_s.shrink_to_fit(); + + let mut toml = Parser::new(&toml_s[..]); + + let toml_table = match toml.parse() { + Some(table) => table, + None => { + return Err(Box::new(format!("Error parsing file {}\n\nPlease check the format or specify the options manually", CLOG_CONFIG_FILE))) + } + }; + + let clog_table = match toml_table.get("clog") { + Some(table) => table, + None => { + return Err(Box::new(format!("Error parsing file {}\n\nPlease check the format or specify the options manually", CLOG_CONFIG_FILE))) + } + }; + + toml_from_latest = clog_table.lookup("from-latest-tag").unwrap_or(&Value::Boolean(false)).as_bool(); + toml_repo = match clog_table.lookup("repository") { + Some(val) => Some(val.as_str().unwrap_or("").to_owned()), + None => Some("".to_owned()) + }; + toml_subtitle = match clog_table.lookup("subtitle") { + Some(val) => Some(val.as_str().unwrap_or("").to_owned()), + None => Some("".to_owned()) + }; + }; + + let from = if matches.is_present("from-latest-tag") || toml_from_latest.unwrap_or(false) { + git::get_latest_tag() + } else if let Some(from) = matches.value_of("from") { + from.to_owned() + } else { + "".to_owned() + }; + + let repo = match matches.value_of("repository") { + Some(repo) => repo.to_owned(), + None => toml_repo.unwrap_or("".to_owned()) + }; + + let subtitle = match matches.value_of("subtitle") { + Some(title) => title.to_owned(), + None => toml_subtitle.unwrap_or("".to_owned()) + }; + + Ok(ClogConfig{ + grep: format!("{}BREAKING'", CommitType::all_aliases().iter().fold(String::new(),|acc, al| acc + &format!("^{}|", al)[..])), + format: "%H%n%s%n%b%n==END==".to_owned(), + repo: repo, + version: version, + subtitle: subtitle, + from: from, + to: matches.value_of("to").unwrap_or("HEAD").to_owned(), + }) + } + +} \ No newline at end of file diff --git a/src/common.rs b/src/common.rs index 13685ea..53fedea 100644 --- a/src/common.rs +++ b/src/common.rs @@ -2,15 +2,16 @@ use std::fmt; use std::collections::HashMap; // Creates an enum where the poritions inside the '(' and ')' act as aliases for that -// commit type. The only one you MUST specify is 'Unknown ()' +// commit type. This macro auto-generates an "Unknown" variant for failures, no need to specify // -// Later you can call CommitType::Fix.aliases() to get all the aliases as a Vec<'statci str> +// Later you can call CommitType::Fix.aliases() to get all the aliases as a Vec<'static str> +// or CommitType::all_aliases() to get a Vec<'static str> of all aliases +// This macro also implements std::str::FromStr to allow things like "feat".parse(); commit_type_enum!{ #[derive(Debug, PartialEq, Clone)] pub enum CommitType { Feature ( feat, ft ), - Fix ( fix, fx), - Unknown () + Fix ( fix, fx ) } } diff --git a/src/format_util.rs b/src/format_util.rs deleted file mode 100644 index 27e2ad5..0000000 --- a/src/format_util.rs +++ /dev/null @@ -1,4 +0,0 @@ - -pub fn get_short_hash(hash: &str) -> &str { - &hash[0..8] -} \ No newline at end of file diff --git a/src/git.rs b/src/git.rs index 2e0dc06..1599609 100644 --- a/src/git.rs +++ b/src/git.rs @@ -1,20 +1,12 @@ use std::process::Command; -use common:: { LogEntry }; -use common::CommitType; use std::borrow::ToOwned; use semver; +use clogconfig::ClogConfig; +use common::{ LogEntry, CommitType }; -#[derive(Debug)] -pub struct LogReaderConfig { - pub grep: String, - pub format: String, - pub from: Option, - pub to: String -} - -pub fn get_latest_tag () -> String { +pub fn get_latest_tag() -> String { let output = Command::new("git") .arg("rev-list") .arg("--tags") @@ -25,17 +17,18 @@ pub fn get_latest_tag () -> String { buf.trim_matches('\n').to_owned() } -pub fn get_latest_tag_ver () -> Result { +pub fn get_latest_tag_ver() -> Result { let output = Command::new("git") .arg("describe") .arg("--tags") .arg("--abbrev=0") .output().unwrap_or_else(|e| panic!("Failed to run 'git describe' with error: {}",e)); - semver::Version::parse(&String::from_utf8_lossy(&output.stdout)[..]) + let v_string = String::from_utf8_lossy(&output.stdout); + semver::Version::parse(&v_string[..].trim_left_matches(|c| c == 'v' || c == 'V')) } -pub fn get_last_commit () -> String { +pub fn get_last_commit() -> String { let output = Command::new("git") .arg("rev-parse") .arg("HEAD") @@ -44,18 +37,18 @@ pub fn get_last_commit () -> String { String::from_utf8_lossy(&output.stdout).into_owned() } -pub fn get_log_entries (config:LogReaderConfig) -> Vec{ +pub fn get_log_entries(config: &ClogConfig) -> Vec{ - let range = match config.from { - Some(ref from) => format!("{}..{}", from, config.to), - None => "HEAD".to_owned() + let range = match &config.from[..] { + "" => "HEAD".to_owned(), + _ => format!("{}..{}", config.from, config.to) }; let output = Command::new("git") .arg("log") .arg("-E") - .arg(&format!("--grep={}",config.grep)) - .arg(&format!("--format={}", "%H%n%s%n%b%n==END==")) + .arg(&format!("--grep={}", config.grep)) + .arg(&format!("--format={}", config.format)) .arg(&range) .output().unwrap_or_else(|e| panic!("Failed to run 'git log' with error: {}", e)); diff --git a/src/log_writer.rs b/src/log_writer.rs index feca9d6..3b065c7 100644 --- a/src/log_writer.rs +++ b/src/log_writer.rs @@ -1,26 +1,21 @@ use std::collections::HashMap; use std::io::{Write, Result}; -use time; -use format_util; -use common::{ LogEntry }; use std::borrow::ToOwned; -pub struct LogWriter<'a, 'lwo> { - writer: &'a mut (Write + 'a), - options: LogWriterOptions<'lwo> -} +use time; -pub struct LogWriterOptions<'a> { - pub repository_link: &'a str, - pub version: String, - pub subtitle: String -} +use common::LogEntry; +use clogconfig::ClogConfig; -impl<'a, 'lwo> LogWriter<'a, 'lwo> { +pub struct LogWriter<'a, 'cc> { + writer: &'a mut (Write + 'a), + options: &'cc ClogConfig +} - fn commit_link(hash: &String, options: &LogWriterOptions) -> String { - let short_hash = format_util::get_short_hash(&hash[..]); - match &options.repository_link[..] { +impl<'a, 'cc> LogWriter<'a, 'cc> { + fn commit_link(hash: &String, options: &ClogConfig) -> String { + let short_hash = &hash[0..8]; + match &options.repo[..] { "" => format!("({})", short_hash), link => format!("[{}]({}/commit/{})", short_hash, link, hash) @@ -28,7 +23,7 @@ impl<'a, 'lwo> LogWriter<'a, 'lwo> { } fn issue_link(&self, issue: &String) -> String { - match &self.options.repository_link[..] { + match &self.options.repo[..] { "" => format!("(#{})", issue), link => format!("[#{}]({}/issues/{})", issue, link, issue) } @@ -97,7 +92,7 @@ impl<'a, 'lwo> LogWriter<'a, 'lwo> { write!(self.writer, "{}", content) } - pub fn new(writer: &'a mut T, options: LogWriterOptions<'lwo>) -> LogWriter<'a, 'lwo> + pub fn new(writer: &'a mut T, options: &'cc ClogConfig) -> LogWriter<'a, 'cc> where T: Write + Send { LogWriter { writer: writer, diff --git a/src/macros.rs b/src/macros.rs index d6982e3..04899ac 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -4,24 +4,22 @@ macro_rules! regex( ); // A macro creating an entry types, and their aliases -// -// This is a little hacky, because it expects an Unknown () variant -// -// TODO: de-dup with recursive calls macro_rules! commit_type_enum { (#[derive($($d:ident),+)] pub enum $e:ident { $($v:ident ( $($a:ident),* ) ),+ }) => { #[derive($($d,)+)] pub enum $e { + Unknown, $($v,)+ } impl $e { #[allow(dead_code)] - pub fn aliases(&self) -> Vec<&'static str> { + pub fn aliases(&self) -> Option> { match *self { - $($e::$v => vec![ + $e::Unknown => None, + $($e::$v => Some(vec![ $( stringify!($a) ),* - ],)+ + ]),)+ } } #[allow(dead_code)] diff --git a/src/main.rs b/src/main.rs index 539065f..0204dda 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ extern crate regex; extern crate time; extern crate semver; +extern crate toml; #[macro_use] extern crate clap; @@ -10,26 +11,28 @@ extern crate clap; use std::fs::File; use std::io::Read; use std::path::Path; -use std::borrow::ToOwned; - -use common::CommitType; -use git::LogReaderConfig; -use log_writer::{ LogWriter, LogWriterOptions }; use clap::{App, Arg}; +use log_writer::LogWriter; +use clogconfig::ClogConfig; + #[macro_use] mod macros; mod common; mod git; mod log_writer; mod section_builder; -mod format_util; +mod clogconfig; + +// for now the clog configuration file is .clog.toml (perhaps change to use definable +// in a future version...) +const CLOG_CONFIG_FILE: &'static str = ".clog.toml"; fn main () { let matches = App::new("clog") // Pull version from Cargo.toml - .version(&crate_version!()[..]) + .version(&format!("v{}", crate_version!())[..]) .about("a conventional changelog for the rest of us") .args_from_usage("-r --repository=[repository] 'e.g. https://github.com/thoughtram/clog' --from=[from] 'e.g. 12a8546' @@ -42,66 +45,31 @@ fn main () { // specify a .mutually_excludes() .arg(Arg::from_usage("--from-latest-tag 'use latest tag as start (instead of --from)'") .mutually_excludes("from")) + // Since --setversion shouldn't be used with any of the --major, --minor, or --match, we + // set those as exclusions .arg(Arg::from_usage("--setversion=[setversion] 'e.g. 1.0.1'") .mutually_excludes_all(vec!["major", "minor", "patch"])) .get_matches(); let start_nsec = time::get_time().nsec; - let log_reader_config = LogReaderConfig { - grep: format!("{}BREAKING'", CommitType::all_aliases().iter().fold(String::new(),|acc, al| acc + &format!("^{}|", al)[..])), - format: "%H%n%s%n%b%n==END==".to_owned(), - from: if matches.is_present("from-latest-tag") { Some(git::get_latest_tag()) } else { matches.value_of("from").map(|v| v.to_owned()) }, - to: matches.value_of("to").unwrap_or("").to_owned() - }; - - // compute version early, so we can exit on error - let version = { - // less typing later... - let (major, minor, patch) = (matches.is_present("major"), matches.is_present("minor"), matches.is_present("patch")); - if matches.is_present("setversion") { - matches.value_of("setversion").unwrap().to_owned() - } else if major || minor || patch { - match git::get_latest_tag_ver() { - Ok(ref mut v) => { - // if-else may be quicker, but it's longer mentally, and this isn't slow - match (major, minor, patch) { - (true,_,_) => { v.major += 1; v.minor = 0; v.patch = 0; }, - (_,true,_) => { v.minor += 1; v.patch = 0; }, - (_,_,true) => { v.patch += 1; }, - _ => unreachable!() - } - format!("{}", v) - }, - Err(e) => { - println!("Error parsing latest version: {}\nTry setting the version manually with --setversion=[version]", e ); - std::process::exit(1); - } - } - } else { - format_util::get_short_hash(&git::get_last_commit()[..]).to_owned() - } - }; - - let commits = git::get_log_entries(log_reader_config); - - let sections = section_builder::build_sections(commits.clone()); + let clog_config = ClogConfig::from_matches(&matches).unwrap_or_else(|e| { println!("{}",e); std::process::exit(1); }); + + let commits = git::get_log_entries(&clog_config); + + let sections = section_builder::build_sections(commits); let mut contents = String::new(); File::open(&Path::new("changelog.md")).map(|mut f| f.read_to_string(&mut contents).ok()).ok(); let mut file = File::create(&Path::new("changelog.md")).ok().unwrap(); - let mut writer = LogWriter::new(&mut file, LogWriterOptions { - repository_link: matches.value_of("repository").unwrap_or(""), - version: version, - subtitle: matches.value_of("subtitle").unwrap_or("").to_owned() - }); + let mut writer = LogWriter::new(&mut file, &clog_config); writer.write_header().ok().expect("failed to write header"); - writer.write_section("Bug Fixes", §ions.fixes).ok().expect("failed to write bugfixes");; - writer.write_section("Features", §ions.features).ok().expect("failed to write features");; - writer.write(&contents[..]).ok().expect("failed to write contents");; + writer.write_section("Bug Fixes", §ions.fixes).ok().expect("failed to write bugfixes"); + writer.write_section("Features", §ions.features).ok().expect("failed to write features"); + writer.write(&contents[..]).ok().expect("failed to write contents"); let end_nsec = time::get_time().nsec; let elapsed_mssec = (end_nsec - start_nsec) / 1000000; diff --git a/src/section_builder.rs b/src/section_builder.rs index 8edf358..b5be0a8 100644 --- a/src/section_builder.rs +++ b/src/section_builder.rs @@ -1,5 +1,4 @@ use std::collections::HashMap; -use std::collections::hash_map::Entry:: { Occupied, Vacant }; use common::{ LogEntry, SectionMap }; use common::CommitType::{ Feature, Fix }; @@ -10,36 +9,23 @@ pub fn build_sections(log_entries: Vec) -> SectionMap { breaks: HashMap::new() }; - for entry in log_entries.into_iter() { + // see https://github.com/rust-lang/rfcs/issues/353 + // sections.features + // .find_or_insert(entry.component.clone(), Vec::new()) + // .push(entry.clone()); + log_entries.into_iter().map(|entry| { match entry.commit_type { Feature => { - let feature = match sections.features.entry(entry.component.clone()) { - Vacant(v) => v.insert(Vec::new()), - Occupied(o) => o.into_mut() - }; - - feature.push(entry.clone()); - - // see https://github.com/rust-lang/rfcs/issues/353 - /* sections.features - .find_or_insert(entry.component.clone(), Vec::new()) - .push(entry.clone());*/ + let feature = sections.features.entry(entry.component.clone()).or_insert(vec![]); + feature.push(entry); }, - Fix => { - let fix = match sections.fixes.entry(entry.component.clone()) { - Vacant(v) => v.insert(Vec::new()), - Occupied(o) => o.into_mut() - }; - - fix.push(entry.clone()); - - /* sections.fixes - .find_or_insert(entry.component.clone(), Vec::new()) - .push(entry.clone());*/ + Fix => { + let fix = sections.fixes.entry(entry.component.clone()).or_insert(vec![]); + fix.push(entry); }, - _ => {} + _ => (), } - } + }).collect::>(); sections }