XAB427Y43CDIGVHXNEOZC4J4KX5PRFJIX6NJADQR6LP6OWHPNC2AC
HFXK2C7HC4PTDDDAYOKGYLPESYOXWVLLZ622H3KYUSI24DIELU7QC
WLLL2ADLZCFVCKAFSF3HWIICTDPAK3NTEYK3ZAGSP5YEPSKNU4KQC
CUZW3MMNXRGZEPWDI6M6QIAZPEQHA35KMXO7VWTPYBLXVLT7CMOAC
CHJDF2CR5BZF5FQN6ZQQKU6DS4UXTDYMPPCCCBOWR6DGWNW3QR4QC
5P5MR7NKRHWVAJFHTYHLYAPSWHM5CWOVERMRRAWABP4J7JM5YKHAC
DMZZMIA7AL72S6YU5LB2PS5V5WNALGKPCGX2SIGXWTKFHRXL7ALQC
2TXT5LGDNIJNMM4EORWKP35BGREM3RPDQ7IUEXZFKQ4N6LGNPLWQC
VUSECHH6JNFVRXXS5ZP7NBGO3BQWBJZCWRJCYF2SKOGZLKRYLK4QC
4LFON6GEZVZPITBA7AZCNARXHUSRHWGFYQMR2X6JEVBGHKHECVTAC
4FCEKFETVRRY5ZNAO44GFUCEIXYCM3J2MRTUQ7CUYZ2ZHANWI2MAC
27L7IZBULKDP76DFPUBKKJHLP7XIXN3KFKSMYS35TU6Y574LUUYQC
HXCFU6ZFK2G33HC3VX2PAQTL3UJK4BG472O4G6T5YF3ZOQKIE4HQC
M3X5J5GHHZXBPTFXDCHWQKQPWUVRUH6RJNGVWIA44B57J4T3O75AC
KPQC63IAT2RE3DTDMWE7Z6N5DL4RRWB7QCONH4EB36RTQ7V3DULQC
name = "console"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"encode_unicode 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.74 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 1.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
"terminal_size 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)",
"termios 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-width 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "dialoguer"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"console 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
]
[[package]]
name = "getrandom"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.74 (registry+https://github.com/rust-lang/crates.io-index)",
"wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)",
name = "rand"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.74 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rand_chacha"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"ppv-lite86 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rand_core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rand_hc"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "redox_syscall"
version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
]
[[package]]
name = "tempfile"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.74 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.57 (registry+https://github.com/rust-lang/crates.io-index)",
"remove_dir_all 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
name = "terminal_size"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.74 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "termios"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.74 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
"checksum console 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8c0994e656bba7b922d8dd1245db90672ffb701e684e45be58f20719d69abc5a"
"checksum dialoguer 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f4aa86af7b19b40ef9cbef761ed411a49f0afa06b7b6dcd3dfe2f96a3c546138"
"checksum rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
"checksum rand_chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
"checksum rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
"checksum rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
"checksum redox_syscall 0.1.57 (registry+https://github.com/rust-lang/crates.io-index)" = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
"checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
"checksum terminal_size 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "9a14cd9f8c72704232f0bfc8455c0e861f0ad4eb60cc9ec8a170e231414c1e13"
"checksum termios 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6f0fcee7b24a25675de40d5bb4de6e41b0df07bc9856295e7e2b3a3600c400c2"
fn read_single_line(prompt: &str) -> Result<String, Error> {
let mut out = io::stdout();
write!(out, "{}", prompt)?;
out.flush()?;
let mut value = String::new();
io::stdin().read_line(&mut value)?;
// remove the newline
Ok(value.trim().to_owned())
fn read_single_line(
theme: &impl dialoguer::theme::Theme,
prompt: &str,
default: &str,
) -> Result<String, Error> {
Ok(dialoguer::Input::with_theme(theme)
.with_prompt(prompt)
.default(default.to_string())
.allow_empty(true)
.interact()?)
fn read_multi_line(prompt: &str) -> Result<String, Error> {
let mut out = io::stdout();
writeln!(out, "{}", prompt)?;
writeln!(out, "Press CTRL+D to stop")?;
out.flush()?;
let mut value = String::new();
io::stdin().read_to_string(&mut value)?;
// remove the newlines
Ok(value.trim().to_owned())
fn make_commit_message(
Dialog {
r#type,
scope,
description,
body,
breaking_change,
issues,
}: &Dialog,
breaking: bool,
parser: &CommitParser,
) -> Result<String, Error> {
let mut msg = r#type.to_string();
if !scope.is_empty() {
msg.push('(');
msg.push_str(scope.as_str());
msg.push(')');
}
if breaking || !breaking_change.is_empty() {
msg.push('!');
}
msg.push_str(": ");
msg.push_str(description.as_str());
if !body.is_empty() {
msg.push_str("\n\n");
msg.push_str(body.as_str())
}
if !breaking_change.is_empty() {
msg.push_str("\n\n");
msg.push_str(format!("BREAKING CHANGE: {}", breaking_change).as_str());
}
if !issues.is_empty() {
msg.push_str("\n\n");
msg.push_str(format!("Refs: {}", issues).as_str());
}
// validate by parsing
Ok(parser.parse(msg.as_str()).map(|_| msg)?)
fn type_as_string(&self) -> &str {
if self.build {
"build"
} else if self.chore {
"chore"
} else if self.ci {
"ci"
} else if self.docs {
"docs"
} else if self.feat {
"feat"
} else if self.fix {
"fix"
} else if self.perf {
"perf"
} else if self.refactor {
"refactor"
} else if self.style {
"style"
} else if self.test {
"test"
} else {
unreachable!()
}
}
fn commit(
&self,
scope: String,
description: String,
body: String,
breaking_change: String,
issues: String,
parser: CommitParser,
) -> Result<ExitStatus, Error> {
let mut msg = self.type_as_string().to_owned();
if !scope.is_empty() {
msg.push('(');
msg.push_str(scope.as_str());
msg.push(')');
}
if self.breaking || !breaking_change.is_empty() {
msg.push('!');
}
msg.push_str(": ");
msg.push_str(description.as_str());
if !body.is_empty() {
msg.push_str("\n\n");
msg.push_str(body.as_str())
}
if !breaking_change.is_empty() {
msg.push_str("\n\n");
msg.push_str(format!("BREAKING CHANGE: {}", breaking_change).as_str());
}
if !issues.is_empty() {
msg.push_str("\n\n");
msg.push_str(format!("Refs: {}", issues).as_str());
}
// validate by parsing
parser
.parse(msg.as_str())
.expect("Matches conventional commit");
fn commit(&self, msg: String) -> Result<ExitStatus, Error> {
fn read_scope(
theme: &impl dialoguer::theme::Theme,
default: &str,
scope_regex: Regex,
) -> Result<String, Error> {
let result: String = dialoguer::Input::with_theme(theme)
.with_prompt("scope")
.validate_with(move |input: &str| match scope_regex.is_match(input) {
true => Ok(()),
false => {
if input.is_empty() {
Ok(())
} else {
Err(format!("scope does not match regex {:?}", scope_regex))
}
}
})
.default(default.to_string())
.allow_empty(true)
.interact()?;
Ok(result)
}
fn read_description(
theme: &impl dialoguer::theme::Theme,
default: String,
) -> Result<String, Error> {
let result: String = dialoguer::Input::with_theme(theme)
.with_prompt("description")
.validate_with(|input: &str| {
if input.len() < 10 {
Err("Description needs a length of at least 10 characters")
} else {
Ok(())
}
})
.default(default)
.allow_empty(false)
.interact()?;
Ok(result)
}
fn read_body(default: &str) -> Result<String, Error> {
let prompt = if default.is_empty() {
"# Enter a commit message body"
} else {
default
};
Ok(dialoguer::Editor::new()
.require_save(true)
.edit(prompt)?
.unwrap_or_default()
.lines()
.filter(|line| !line.starts_with('#'))
.collect::<Vec<&str>>()
.join("\n")
.trim()
.to_owned())
}
#[derive(Default)]
struct Dialog {
r#type: String,
scope: String,
description: String,
body: String,
breaking_change: String,
issues: String,
}
impl Dialog {
fn select_type(
theme: &impl dialoguer::theme::Theme,
selected: &str,
types: &[Type],
) -> Result<String, Error> {
let index = dialoguer::Select::with_theme(theme)
.with_prompt("type")
.items(types)
.default(types.iter().position(|t| t.r#type == selected).unwrap_or(0))
.interact()?;
Ok(r#types[index].r#type.clone())
}
// Prompt all
fn wizard(
config: &Config,
parser: CommitParser,
r#type: Option<String>,
breaking: bool,
) -> Result<String, Error> {
let mut dialog = Self::default();
let theme = &dialoguer::theme::ColorfulTheme::default();
let types = config.types.as_slice();
let scope_regex = Regex::new(config.scope_regex.as_str()).expect("valid scope regex");
loop {
// type
let current_type = dialog.r#type.as_str();
match (r#type.as_ref(), current_type) {
(Some(t), "") if t != "" => dialog.r#type = t.to_owned(),
(_, t) => {
dialog.r#type = Self::select_type(theme, t, types)?;
}
}
// scope
dialog.scope = read_scope(theme, dialog.scope.as_ref(), scope_regex.clone())?;
// description
dialog.description = read_description(theme, dialog.description)?;
// body
dialog.body = read_body(dialog.body.as_str())?;
// breaking change
dialog.breaking_change = read_single_line(
theme,
"optional BREAKING change",
dialog.breaking_change.as_str(),
)?;
// issues
dialog.issues =
read_single_line(theme, "issues (e.g. #2, #8)", dialog.issues.as_str())?;
// finally make message
match make_commit_message(&dialog, breaking, &parser) {
Ok(msg) => {
if dialoguer::Confirm::with_theme(theme)
.with_prompt(format!("\nConfirm commit message:\n\n{}\n", msg))
.interact()?
{
break Ok(msg);
}
}
Err(error) => {
println!("{}", error);
}
}
}
}
}
let scope = read_single_line("optional scope: ")?;
let description = read_single_line("description: ")?;
let body = read_multi_line("optional body:")?;
let breaking_change = read_single_line("optional BREAKING CHANGE: ")?;
let issues = read_single_line("optional issues (e.g. #2, #8): ")?;
let r#type = match (
self.feat,
self.fix,
self.build,
self.chore,
self.ci,
self.docs,
self.style,
self.refactor,
self.perf,
self.test,
) {
(true, false, false, false, false, false, false, false, false, false) => {
Some("feat".to_string())
}
(false, true, false, false, false, false, false, false, false, false) => {
Some("fix".to_string())
}
(false, false, true, false, false, false, false, false, false, false) => {
Some("build".to_string())
}
(false, false, false, true, false, false, false, false, false, false) => {
Some("chore".to_string())
}
(false, false, false, false, true, false, false, false, false, false) => {
Some("ci".to_string())
}
(false, false, false, false, false, true, false, false, false, false) => {
Some("docs".to_string())
}
(false, false, false, false, false, false, true, false, false, false) => {
Some("style".to_string())
}
(false, false, false, false, false, false, false, true, false, false) => {
Some("refactor".to_string())
}
(false, false, false, false, false, false, false, false, true, false) => {
Some("perf".to_string())
}
(false, false, false, false, false, false, false, false, false, true) => {
Some("test".to_string())
}
_ => None,
};
#[derive(Debug)]
pub enum ParseError {
NoType,
NoDescription,
EmptyCommitMessage,
InvalidFirstLine,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParseError::NoType => write!(f, "missing type"),
ParseError::NoDescription => write!(f, "missing description"),
ParseError::EmptyCommitMessage => write!(f, "empty commit message"),
ParseError::InvalidFirstLine => write!(
f,
"first line doesn't match `<type>[optional scope]: <description>`"
),
}
}
}
impl std::error::Error for ParseError {}
Type {
r#type: "build".into(),
section: "Other".into(),
hidden: true,
},
Type {
r#type: "chore".into(),
section: "Other".into(),
hidden: true,
},
Type {
r#type: "ci".into(),
section: "Other".into(),
hidden: true,
},
Type {
r#type: "docs".into(),
section: "Documentation".into(),
hidden: true,
},
Type {
r#type: "style".into(),
section: "Other".into(),
hidden: true,
},
Type {
r#type: "refactor".into(),
section: "Other".into(),
hidden: true,
},
Type {
r#type: "perf".into(),
section: "Other".into(),
hidden: true,
},
Type {
r#type: "test".into(),
section: "Other".into(),
hidden: true,
},