From 726af4eef27778017d701541d4c79b7b34cb8a8b Mon Sep 17 00:00:00 2001 From: Kevin K Date: Sat, 25 Apr 2015 13:05:07 -0400 Subject: [PATCH] feat(config): support .clog.toml configuration file --- Cargo.lock | 18 +++++- Cargo.toml | 1 + src/clogconfig.rs | 128 +++++++++++++++++++++++++++++++++++++++++ src/common.rs | 9 +-- src/format_util.rs | 4 -- src/git.rs | 33 +++++------ src/git.rs.orig | 110 +++++++++++++++++++++++++++++++++++ src/log_writer.rs | 31 +++++----- src/macros.rs | 12 ++-- src/main.rs | 74 +++++++----------------- src/section_builder.rs | 38 ++++-------- 11 files changed, 324 insertions(+), 134 deletions(-) create mode 100644 src/clogconfig.rs delete mode 100644 src/format_util.rs create mode 100644 src/git.rs.orig 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..a5f03a7 --- /dev/null +++ b/src/clogconfig.rs @@ -0,0 +1,128 @@ +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_f = match File::open(cfg_file){ + Ok(f) => f, + Err(e) => return Err(Box::new(e)), + }; + + 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))) + } + }; + + let from = if matches.is_present("from-latest-tag") || clog_table.lookup("from-latest-tag").unwrap_or(&Value::Boolean(false)).as_bool().unwrap() { + 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 => match clog_table.lookup("repository") { + Some(val) => val.as_str().unwrap_or("").to_owned(), + None => "".to_owned() + } + }; + + let subtitle = match matches.value_of("subtitle") { + Some(title) => title.to_owned(), + None => match clog_table.lookup("subtitle") { + Some(val) => val.as_str().unwrap_or("").to_owned(), + None => "".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/git.rs.orig b/src/git.rs.orig new file mode 100644 index 0000000..704c3d6 --- /dev/null +++ b/src/git.rs.orig @@ -0,0 +1,110 @@ +use std::process::Command; +use common:: { LogEntry }; +use common::CommitType; +use std::borrow::ToOwned; + +use semver; + + +#[derive(Debug)] +pub struct LogReaderConfig { + pub grep: String, + pub format: String, + pub from: Option, + pub to: String +} + +pub fn get_latest_tag () -> String { + let output = Command::new("git") + .arg("rev-list") + .arg("--tags") + .arg("--max-count=1") + .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)); + + String::from_utf8_lossy(&output.stdout).into_owned() +} + +pub fn get_log_entries (config:LogReaderConfig) -> Vec{ + + let range = match config.from { + Some(ref from) => format!("{}..{}", from, config.to), + None => "HEAD".to_owned() + }; + + 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(&range) + .output().unwrap_or_else(|e| panic!("Failed to run 'git log' with error: {}", e)); + + String::from_utf8_lossy(&output.stdout) + .split("\n==END==\n") + .map(|commit_str| { parse_raw_commit(commit_str) }) + .filter(| entry| entry.commit_type != CommitType::Unknown) + .collect() +} + + +fn parse_raw_commit(commit_str:&str) -> LogEntry { + let mut lines = commit_str.split('\n'); + + let hash = lines.next().unwrap_or("").to_owned(); + + let commit_pattern = regex!(r"^(.*)\((.*)\):(.*)"); + let (subject, component, commit_type) = + match lines.next().and_then(|s| commit_pattern.captures(s)) { + Some(caps) => { +<<<<<<< HEAD + 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(); +>>>>>>> 7f1d12e... feat(aliases): implement aliases for commit types + let component = caps.at(2); + let subject = caps.at(3); + (subject, component, commit_type) + }, + None => (Some(""), Some(""), CommitType::Unknown) + }; + let closes_pattern = regex!(r"(?:Closes|Fixes|Resolves)\s((?:#(\d+)(?:,\s)?)+)"); + let closes = lines.filter_map(|line| closes_pattern.captures(line)) + .map(|caps| caps.at(2).unwrap().to_owned()) + .collect(); + + LogEntry { + hash: hash, + subject: subject.unwrap().to_owned(), + component: component.unwrap().to_owned(), + closes: closes, + breaks: vec![], + commit_type: commit_type + } +} 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 }