gitcc_cli/
commit.rs

1//! `commit` command
2
3use std::env;
4
5use clap::Parser;
6use dialoguer::{theme::ColorfulTheme, Confirm, Editor, Input, Select};
7use gitcc_core::{Config, ConvcoMessage, StatusShow, StringExt};
8
9use crate::{error, info, new_line, success, warn};
10
11/// Commit command arguments
12#[derive(Debug, Parser)]
13pub struct CommitArgs {}
14
15/// Executes the command `commit`
16pub fn run(_args: CommitArgs) -> anyhow::Result<()> {
17    // load the config
18    let cwd = env::current_dir()?;
19    let config = Config::load_from_fs(&cwd)?;
20    let config = if let Some(cfg) = config {
21        cfg
22    } else {
23        info!("using default config");
24        Config::default()
25    };
26
27    // Checks that the repo is clean
28    let status = gitcc_core::git_status(&cwd, StatusShow::Workdir)?;
29    if !status.is_empty() {
30        warn!("repo is dirty:");
31        for (file, _) in status {
32            eprintln!("\t{file}");
33        }
34        match Confirm::with_theme(&ColorfulTheme::default())
35            .with_prompt("continue ?")
36            .report(true)
37            .default(false)
38            .interact()?
39        {
40            false => {
41                error!("aborted");
42                return Ok(());
43            }
44            true => {}
45        }
46    }
47
48    // write the commit
49    let msg = open_dialogue(&config)?;
50    // eprintln!("{:#?}", commit);
51
52    // git commit
53    let commit = gitcc_core::commit_changes(&cwd, &msg.to_string())?;
54    new_line!();
55    success!(format!("new commit with id {}", commit.id));
56
57    Ok(())
58}
59
60/// Asks the user to enter the commit info
61fn open_dialogue(config: &Config) -> anyhow::Result<ConvcoMessage> {
62    // > type
63    let r#type = {
64        let commit_types: Vec<_> = config
65            .commit
66            .types
67            .iter()
68            .map(|(k, v)| format!("{}: {}", k, v))
69            .collect();
70        let commit_types_keys: Vec<_> = config.commit.types.keys().map(|k| k.to_string()).collect();
71        let i = Select::with_theme(&ColorfulTheme::default())
72            .items(&commit_types)
73            .clear(true)
74            .default(0)
75            .report(true)
76            .with_prompt("Commit type")
77            .interact()?;
78
79        commit_types_keys[i].clone()
80    };
81
82    // > scope
83    let scope = {
84        let scope: String = Input::with_theme(&ColorfulTheme::default())
85            .with_prompt("Commit scope")
86            .report(true)
87            .allow_empty(true)
88            .interact_text()?;
89        if scope.is_empty() {
90            None
91        } else {
92            Some(scope.to_lowercase())
93        }
94    };
95
96    // > Short description
97    let desc = Input::<String>::with_theme(&ColorfulTheme::default())
98        .with_prompt("Commit description")
99        .report(true)
100        .interact()?
101        .trim()
102        .to_lowercase_first();
103
104    let mut msg = ConvcoMessage {
105        r#type,
106        scope,
107        is_breaking: false,
108        desc,
109        body: None,
110        footer: None,
111    };
112
113    // > breaking changes
114    // dev note: 'Confirm' does not remove the blinking cursor
115    match Confirm::with_theme(&ColorfulTheme::default())
116        .with_prompt("Breaking change ?")
117        .report(true)
118        .default(false)
119        .interact()?
120    {
121        false => {}
122        true => {
123            let breaking_change_desc = Input::<String>::with_theme(&ColorfulTheme::default())
124                .with_prompt("Breaking change description".to_string())
125                .report(true)
126                .allow_empty(true)
127                .interact_text()?;
128            msg.add_breaking_change(&breaking_change_desc);
129        }
130    }
131
132    // > body
133    match Confirm::with_theme(&ColorfulTheme::default())
134        .with_prompt("Commit message body ?")
135        .report(true)
136        .default(false)
137        .interact()?
138    {
139        false => {}
140        true => {
141            let text = Editor::new()
142                .require_save(true)
143                .trim_newlines(true)
144                .edit("")?;
145            msg.body = text;
146        }
147    }
148
149    // > footer
150    'footer_notes: loop {
151        match Confirm::with_theme(&ColorfulTheme::default())
152            .with_prompt("Add footer note ?")
153            .report(true)
154            .default(false)
155            .interact()?
156        {
157            false => break 'footer_notes,
158            true => {
159                let key = Input::<String>::with_theme(&ColorfulTheme::default())
160                    .with_prompt("Key ?".to_string())
161                    .report(true)
162                    .allow_empty(false)
163                    .interact_text()?;
164                let value = Input::<String>::with_theme(&ColorfulTheme::default())
165                    .with_prompt("Value ?".to_string())
166                    .report(true)
167                    .allow_empty(false)
168                    .interact_text()?;
169                msg.add_footer_note(&key, &value);
170            }
171        }
172    }
173
174    Ok(msg)
175}