1use std::process::{self, Command};
2
3use dialoguer::{theme::ColorfulTheme, Editor, FuzzySelect, Input};
4
5#[derive(Debug)]
6struct Message {
7 commit_type: String,
8 scope: String,
9 description: String,
10 body: String,
11 breaking_change: String,
12}
13
14impl Message {
15 fn format(self) -> String {
16 let mut message = String::new();
17 message.push_str(&self.commit_type);
18 if self.scope.len() != 0 {
19 message.push_str(format!("({})", self.scope).as_str());
20 }
21 message.push_str(": ");
22 message.push_str(&self.description);
23 if self.body.len() != 0 {
24 message.push_str("\n\n");
25 message.push_str(&self.body);
26 }
27 if self.breaking_change.len() != 0 {
28 message.push_str("\n\n");
29 message.push_str(format!("BREAKING CHANGE: {}", self.breaking_change).as_str());
30 }
31 message
32 }
33}
34
35#[cfg(test)]
36mod tests {
37 use super::*;
38
39 #[test]
40 fn format_type_and_description() {
41 let message = Message {
42 commit_type: String::from("feat"),
43 scope: String::from(""),
44 description: String::from("add api"),
45 body: String::from(""),
46 breaking_change: String::from(""),
47 };
48 assert_eq!(message.format(), "feat: add api")
49 }
50
51 #[test]
52 fn format_with_scope() {
53 let message = Message {
54 commit_type: String::from("feat"),
55 scope: String::from("cli"),
56 description: String::from("add api"),
57 body: String::from(""),
58 breaking_change: String::from(""),
59 };
60 assert_eq!(message.format(), "feat(cli): add api")
61 }
62
63 #[test]
64 fn format_with_body() {
65 let message = Message {
66 commit_type: String::from("feat"),
67 scope: String::from(""),
68 description: String::from("add api"),
69 body: String::from("This is a body"),
70 breaking_change: String::from(""),
71 };
72 assert_eq!(
73 message.format(),
74 r#"feat: add api
75
76This is a body"#
77 )
78 }
79
80 #[test]
81 fn format_with_scope_and_body() {
82 let message = Message {
83 commit_type: String::from("feat"),
84 scope: String::from("cli"),
85 description: String::from("add api"),
86 body: String::from("This is a body"),
87 breaking_change: String::from(""),
88 };
89 assert_eq!(
90 message.format(),
91 r#"feat(cli): add api
92
93This is a body"#
94 )
95 }
96
97 #[test]
98 fn format_with_scope_and_body_and_breaking_change() {
99 let message = Message {
100 commit_type: String::from("feat"),
101 scope: String::from("cli"),
102 description: String::from("add api"),
103 body: String::from("This is a body"),
104 breaking_change: String::from("remove api"),
105 };
106 assert_eq!(
107 message.format(),
108 r#"feat(cli): add api
109
110This is a body
111
112BREAKING CHANGE: remove api"#
113 )
114 }
115}
116
117fn select_commit_type() -> String {
118 let selections = vec![
120 "feat", "fix", "build", "chore", "ci", "docs", "perf", "refactor", "revert", "style",
121 "test",
122 ];
123
124 let selection = FuzzySelect::with_theme(&ColorfulTheme::default())
125 .with_prompt("Select the type of change that you're committing:")
126 .default(0)
127 .items(&selections[..])
128 .interact()
129 .unwrap();
130
131 selections[selection].to_string()
132}
133
134fn write_scope() -> String {
135 Input::with_theme(&ColorfulTheme::default())
136 .with_prompt("Write the commit scope (optional):")
137 .allow_empty(true)
138 .interact_text()
139 .unwrap()
140}
141
142fn write_description() -> String {
143 Input::with_theme(&ColorfulTheme::default())
144 .with_prompt("Write a short description:")
145 .interact_text()
146 .unwrap()
147}
148
149fn write_body() -> String {
150 Input::with_theme(&ColorfulTheme::default())
151 .with_prompt("Write a detail description (optional):")
152 .allow_empty(true)
153 .interact_text()
154 .unwrap()
155}
156
157fn write_breaking_change() -> String {
158 Input::with_theme(&ColorfulTheme::default())
159 .with_prompt("Write a breaking change (optional):")
160 .allow_empty(true)
161 .interact_text()
162 .unwrap()
163}
164
165fn create_message() -> Message {
166 let commit_type = select_commit_type();
167 let scope = write_scope();
168 let description = write_description();
169 let body = write_body();
170 let breaking_change = write_breaking_change();
171 let message = Message {
172 commit_type,
173 scope,
174 description,
175 body,
176 breaking_change,
177 };
178 message
179}
180
181fn commit(message: &str) {
182 Command::new("git")
183 .arg("commit")
184 .arg("-m")
185 .arg(format!("{}", message))
186 .status()
187 .expect("commit failed");
188}
189
190pub fn run() {
191 let message = create_message();
192 let formatted_message = message.format();
193
194 let selections = vec!["Commit with it", "Continue with editor", "Cancel"];
195 let selection = FuzzySelect::with_theme(&ColorfulTheme::default())
196 .with_prompt(format!("Are you OK this message?\n{}", formatted_message))
197 .default(0)
198 .items(&selections[..])
199 .interact()
200 .unwrap();
201 match selection {
202 0 => {
204 commit(&formatted_message);
205 }
206 1 => {
208 if let Some(rv) = Editor::new().edit(&formatted_message).unwrap() {
209 commit(&rv);
210 } else {
211 process::exit(1);
212 }
213 }
214 2 => {
216 process::exit(1);
217 }
218 _ => unreachable!(),
219 }
220}