git_cc/
lib.rs

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    // conventional commit type
119    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        // Commit with it
203        0 => {
204            commit(&formatted_message);
205        }
206        // Continue with editor
207        1 => {
208            if let Some(rv) = Editor::new().edit(&formatted_message).unwrap() {
209                commit(&rv);
210            } else {
211                process::exit(1);
212            }
213        }
214        // Cancel
215        2 => {
216            process::exit(1);
217        }
218        _ => unreachable!(),
219    }
220}