1use std::process::exit;
2
3use console::Term;
4use serde::{Deserialize, Serialize};
5use tracing::{info, warn};
6use url::Url;
7
8use crate::git::has_staged_changes;
9use crate::{
10 git, recovery, EmojiFormat, Error, GitmojiConfig, Result, EXIT_CANNOT_UPDATE, EXIT_NO_CONFIG,
11};
12
13mod commit;
14pub use self::commit::*;
15
16mod config;
17pub use self::config::*;
18
19#[cfg(feature = "hook")]
20mod hook;
21
22mod list;
23use self::list::print_gitmojis;
24
25mod search;
26use self::search::filter;
27
28mod update;
29use self::update::update_gitmojis;
30
31async fn get_config_or_stop() -> GitmojiConfig {
32 match read_config_or_fail().await {
33 Ok(config) => config,
34 Err(err) => {
35 warn!("Oops, cannot read config because {err}");
36 eprintln!(
37 "⚠️ No configuration found, try run `gitmoji init` to fetch a configuration"
38 );
39 exit(EXIT_NO_CONFIG)
40 }
41 }
42}
43
44async fn update_config_or_stop(config: GitmojiConfig) -> GitmojiConfig {
45 let url = config.update_url().to_string();
46 match update_gitmojis(config).await {
47 Ok(config) => config,
48 Err(err) => {
49 warn!("Oops, cannot update the config because {err}");
50 eprintln!("⚠️ Configuration not updated, maybe check the update url '{url}'");
51 exit(EXIT_CANNOT_UPDATE)
52 }
53 }
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct CommitTitleDescription {
58 pub title: String,
59 pub description: Option<String>,
60}
61
62#[tracing::instrument(skip(term))]
63async fn ask_commit_title_description(
64 config: &GitmojiConfig,
65 term: &Term,
66) -> Result<CommitTitleDescription> {
67 let CommitParams {
68 gitmoji,
69 scope,
70 title,
71 description,
72 } = get_commit_params(config, term)?;
73
74 let gitmoji = match config.format() {
75 EmojiFormat::UseCode => gitmoji.code(),
76 EmojiFormat::UseEmoji => gitmoji.emoji(),
77 };
78
79 let title = scope.map_or_else(
80 || format!("{gitmoji} {title}"),
81 |scope| format!("{gitmoji} {scope}{title}"),
82 );
83
84 let result = CommitTitleDescription { title, description };
85 Ok(result)
86}
87
88#[tracing::instrument(skip(term))]
90pub async fn commit(all: bool, amend: bool, extra_args: &[String], term: &Term) -> Result<()> {
91 let config = get_config_or_stop().await;
92
93 if !amend && !has_staged_changes().await? {
94 eprintln!("No change to commit");
95 return Ok(());
96 }
97
98 let commit = if let Ok(Some(recovered)) = recovery::read() {
99 if recovery::ask(term, &recovered).unwrap_or(false) {
100 recovered
101 } else {
102 ask_commit_title_description(&config, term).await?
103 }
104 } else {
105 ask_commit_title_description(&config, term).await?
106 };
107
108 let all = all || config.auto_add();
110
111 let status = git::commit(
113 all,
114 amend,
115 config.signed(),
116 &commit.title,
117 commit.description.as_deref(),
118 extra_args,
119 )
120 .await?;
121
122 if status.success() {
123 if let Err(err) = recovery::clear() {
124 warn!("{err}");
125 }
126 Ok(())
127 } else {
128 if let Err(err) = recovery::write(&commit) {
129 warn!("{err}");
130 }
131 Err(Error::FailToCommit)
132 }
133}
134
135#[tracing::instrument(skip(term))]
140pub async fn config(default: bool, term: &Term) -> Result<()> {
141 let config = if default {
142 GitmojiConfig::default()
143 } else {
144 create_config(term)?
145 };
146 info!("Loading gitmojis from {}", config.update_url());
147 update_config_or_stop(config).await;
148
149 Ok(())
150}
151
152#[tracing::instrument]
154pub async fn search(text: &str) -> Result<()> {
155 let config = get_config_or_stop().await;
156 let result = filter(config.gitmojis(), text);
157 print_gitmojis(&result);
158 Ok(())
159}
160
161#[tracing::instrument]
163pub async fn list() -> Result<()> {
164 let config = get_config_or_stop().await;
165 print_gitmojis(config.gitmojis());
166 Ok(())
167}
168
169#[tracing::instrument]
171pub async fn update_config(url: Option<Url>) -> Result<()> {
172 let mut config = read_config_or_default().await;
173 if let Some(url) = url {
174 config.set_update_url(url);
175 }
176 let result = update_config_or_stop(config).await;
177 print_gitmojis(result.gitmojis());
178
179 Ok(())
180}
181
182#[cfg(feature = "hook")]
184#[tracing::instrument]
185pub async fn create_hook() -> Result<()> {
186 hook::create().await
187}
188
189#[tracing::instrument]
191#[cfg(feature = "hook")]
192pub async fn remove_hook() -> Result<()> {
193 hook::remove().await
194}
195
196#[cfg(all(feature = "hook", unix))]
198fn open_tty_term() -> std::io::Result<Term> {
199 use std::fs::OpenOptions;
200
201 let tty_read = OpenOptions::new().read(true).open("/dev/tty")?;
202 let tty_write = OpenOptions::new().write(true).open("/dev/tty")?;
203
204 Ok(Term::read_write_pair(tty_read, tty_write))
205}
206
207#[cfg(feature = "hook")]
209#[tracing::instrument(skip(_term))]
210pub async fn apply_hook(
211 dest: std::path::PathBuf,
212 source: Option<String>,
213 _term: &Term,
214) -> Result<()> {
215 use tokio::io::AsyncWriteExt;
216
217 let term = open_tty_term().map_err(|e| Error::Hook {
220 cause: format!("Cannot open /dev/tty: {e}"),
221 })?;
222
223 let config = get_config_or_stop().await;
224
225 let CommitTitleDescription { title, description } =
226 ask_commit_title_description(&config, &term).await?;
227
228 info!("Write commit message to {dest:?} with source: {source:?}");
229 let contents = tokio::fs::read(&dest).await.unwrap_or_default();
230 let mut file = tokio::fs::File::create(&dest).await?;
231
232 file.write_all(title.as_bytes()).await?;
233 file.write_all(b"\n\n").await?;
234
235 if let Some(description) = description {
236 file.write_all(description.as_bytes()).await?;
237 file.write_all(b"\n").await?;
238 }
239 file.write_all(&contents).await?;
240
241 Ok(())
242}