git_revise/revise/
template.rs

1use std::fmt::Formatter;
2
3use colored::Colorize;
4use tera::{Context, Tera};
5use tokio::task;
6
7use super::prompts::{
8    commit_ai, commit_body, commit_breaking, commit_confirm, commit_issue,
9    commit_scope, commit_subject, commit_translate, commit_type,
10};
11use crate::{
12    ai::{gemini::Gemini, AI},
13    config,
14    error::ReviseResult,
15    git::GitUtils,
16    revise::prompts::Inquire,
17    AICommand, ReviseCommands,
18};
19
20#[derive(Debug, Default, Clone)]
21pub struct Template {
22    pub commit_type: commit_type::Part,
23    pub commit_scope: commit_scope::Part,
24    pub commit_subject: commit_subject::Part,
25    pub commit_body: commit_body::Part,
26    pub commit_breaking: commit_breaking::Part,
27    pub commit_issue: commit_issue::Part,
28}
29
30impl Template {
31    pub async fn run(&mut self, cmd: &ReviseCommands) -> ReviseResult<String> {
32        if cmd.ai.is_some() {
33            self.run_action(cmd).await?;
34        } else {
35            self.run_default()?;
36        }
37        let mut confirm = commit_confirm::Part::new(self.clone());
38        confirm.inquire()?;
39        Ok(confirm.ans.unwrap())
40    }
41
42    pub fn run_default(&mut self) -> ReviseResult<()> {
43        self.commit_type.inquire()?;
44        self.commit_scope.inquire()?;
45        self.commit_subject.inquire()?;
46        self.commit_body.inquire()?;
47        self.commit_breaking.inquire()?;
48        self.commit_issue.inquire()?;
49        Ok(())
50    }
51
52    pub async fn run_action(
53        &mut self,
54        cmd: &ReviseCommands,
55    ) -> ReviseResult<()> {
56        let cfg = config::get_config();
57        let Some(key) = cfg.api_key.get("gemini_key") else {
58            return Err(anyhow::anyhow!("API key not found"));
59        };
60
61        let gemini = Gemini::new(key);
62
63        let mut s = match cmd.ai.clone().unwrap() {
64            AICommand::Translate(s) => s,
65            AICommand::Generate => GitUtils::new().diff(&cmd.excludes)?,
66        };
67        // Fix: If the diff is empty, not directly ask user to input
68        if s.is_empty() {
69            let mut translate = commit_translate::Part::new();
70            translate.inquire()?;
71            s = match translate.ans {
72                Some(s) => s,
73                None => {
74                    return Err(anyhow::anyhow!("Translate message is empty"));
75                }
76            };
77        }
78        let handle =
79            task::spawn(async move { gemini.generate_response(&s).await });
80        self.commit_type.inquire()?;
81        self.commit_scope.inquire()?;
82        self.commit_breaking.inquire()?;
83        self.commit_issue.inquire()?;
84        let res = handle.await??;
85        let mut ai = commit_ai::Part::new(res.keys().cloned().collect());
86        ai.inquire()?;
87        let ai_ans = res.get(&ai.ans.clone().unwrap()).unwrap();
88        self.commit_subject.ans = Some(ai_ans.message.clone());
89        self.commit_body.ans = Some(ai_ans.body.clone());
90        Ok(())
91    }
92
93    pub fn get_ctype(&self) -> String {
94        self.commit_type.ans.clone().unwrap()
95    }
96    pub fn get_cicon(&self) -> String {
97        config::get_config().get_emoji(&self.get_ctype()).unwrap()
98    }
99    pub fn get_cscope(&self) -> Option<String> {
100        self.commit_scope.ans.clone()
101    }
102    pub fn get_csubject(&self) -> String {
103        self.commit_subject.ans.clone().unwrap()
104    }
105    pub fn get_cbody(&self) -> Option<String> {
106        self.commit_body.ans.clone()
107    }
108    pub fn get_cbreaking(&self) -> Option<String> {
109        self.commit_breaking.ans.clone()
110    }
111    pub fn get_cissue(&self) -> Option<String> {
112        self.commit_issue.ans.clone()
113    }
114
115    pub fn template(&self, color: bool) -> String {
116        let cfg = config::get_config();
117        let mut tera = Tera::default();
118        tera.add_raw_template("template", &cfg.template).unwrap();
119        let mut ctx = Context::new();
120        if color {
121            ctx.insert("commit_type", &self.get_ctype().green().to_string());
122            ctx.insert("commit_icon", &self.get_cicon());
123            match self.get_cscope() {
124                Some(scope) => {
125                    ctx.insert("commit_scope", &format!("{}", scope.yellow()));
126                }
127                None => {
128                    ctx.insert("commit_scope", &Option::<String>::None);
129                }
130            }
131            ctx.insert(
132                "commit_subject",
133                &self.get_csubject().bright_cyan().to_string(),
134            );
135            match self.get_cbody() {
136                Some(body) => {
137                    ctx.insert("commit_body", &body);
138                }
139                None => {
140                    ctx.insert("commit_body", &Option::<String>::None);
141                }
142            }
143            if let Some(breaking) = self.get_cbreaking() {
144                ctx.insert(
145                    "commit_breaking",
146                    &format!(
147                        "{}: {}",
148                        "BREAKING CHANGE".bright_red(),
149                        breaking.purple()
150                    ),
151                );
152                ctx.insert(
153                    "commit_breaking_symbol",
154                    &"!".bright_red().to_string(),
155                );
156            } else {
157                ctx.insert("commit_breaking", &Option::<String>::None);
158                ctx.insert("commit_breaking_symbol", &Option::<String>::None);
159            }
160            match self.get_cissue() {
161                Some(issue) => {
162                    ctx.insert("commit_issue", &format!("{}", issue.blue()));
163                }
164                None => {
165                    ctx.insert("commit_issue", &Option::<String>::None);
166                }
167            }
168        } else {
169            ctx.insert("commit_type", &self.get_ctype());
170            ctx.insert("commit_icon", &self.get_cicon());
171            ctx.insert("commit_scope", &self.get_cscope());
172            ctx.insert("commit_subject", &self.get_csubject());
173            ctx.insert("commit_body", &self.get_cbody());
174            if let Some(breaking) = self.get_cbreaking() {
175                ctx.insert(
176                    "commit_breaking",
177                    &format!("{}: {}", "BREAKING CHANGE", breaking),
178                );
179                ctx.insert("commit_breaking_symbol", &"!".to_string());
180            } else {
181                ctx.insert("commit_breaking", &Option::<String>::None);
182                ctx.insert("commit_breaking_symbol", &Option::<String>::None);
183            }
184            ctx.insert("commit_issue", &self.get_cissue());
185        }
186
187        tera.render("template", &ctx).unwrap()
188    }
189}
190
191impl std::fmt::Display for Template {
192    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
193        let msg = self.template(false);
194        write!(f, "{msg}")
195    }
196}
197
198#[ignore]
199#[test]
200fn test_template() {
201    config::initialize_config().unwrap_or_else(|e| {
202        eprintln!("Load config err: {e}");
203        std::process::exit(exitcode::CONFIG);
204    });
205
206    let t = Template {
207        commit_type: commit_type::Part {
208            ans: Some("feat".to_string()),
209            ..Default::default()
210        },
211        commit_scope: commit_scope::Part {
212            ans: Some("scope".to_string()),
213            ..Default::default()
214        },
215        commit_subject: commit_subject::Part {
216            ans: Some("add a new feature".to_string()),
217            ..Default::default()
218        },
219        commit_body: commit_body::Part {
220            ans: Some("add a new feature with a body".to_string()),
221            ..Default::default()
222        },
223        commit_breaking: commit_breaking::Part {
224            ans: Some("breaking change".to_string()),
225            ..Default::default()
226        },
227        commit_issue: commit_issue::Part {
228            ans: Some("#34".to_string()),
229            ..Default::default()
230        },
231    };
232    let s = t.template(true);
233    println!("{s}");
234    println!("{t}");
235}