1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
//! `commit` command

use std::env;

use clap::Parser;
use dialoguer::{theme::ColorfulTheme, Confirm, Editor, Input, Select};
use gitcc_core::{Config, ConvcoMessage, StatusShow, StringExt};

use crate::{error, info, new_line, success, warn};

/// Commit command arguments
#[derive(Debug, Parser)]
pub struct CommitArgs {}

/// Executes the command `commit`
pub fn run(_args: CommitArgs) -> anyhow::Result<()> {
    // load the config
    let cwd = env::current_dir()?;
    let config = Config::load_from_fs(&cwd)?;
    let config = if let Some(cfg) = config {
        cfg
    } else {
        info!("using default config");
        Config::default()
    };

    // Checks that the repo is clean
    let status = gitcc_core::git_status(&cwd, StatusShow::Workdir)?;
    if !status.is_empty() {
        warn!("repo is dirty:");
        for (file, _) in status {
            eprintln!("\t{file}");
        }
        match Confirm::with_theme(&ColorfulTheme::default())
            .with_prompt("continue ?")
            .report(true)
            .default(false)
            .interact()?
        {
            false => {
                error!("aborted");
                return Ok(());
            }
            true => {}
        }
    }

    // write the commit
    let msg = open_dialogue(&config)?;
    // eprintln!("{:#?}", commit);

    // git commit
    let commit = gitcc_core::commit_changes(&cwd, &msg.to_string())?;
    new_line!();
    success!(format!("new commit with id {}", commit.id));

    Ok(())
}

/// Asks the user to enter the commit info
fn open_dialogue(config: &Config) -> anyhow::Result<ConvcoMessage> {
    // > type
    let r#type = {
        let commit_types: Vec<_> = config
            .commit
            .types
            .iter()
            .map(|(k, v)| format!("{}: {}", k, v))
            .collect();
        let commit_types_keys: Vec<_> = config.commit.types.keys().map(|k| k.to_string()).collect();
        let i = Select::with_theme(&ColorfulTheme::default())
            .items(&commit_types)
            .clear(true)
            .default(0)
            .report(true)
            .with_prompt("Commit type")
            .interact()?;

        commit_types_keys[i].clone()
    };

    // > scope
    let scope = {
        let scope: String = Input::with_theme(&ColorfulTheme::default())
            .with_prompt("Commit scope")
            .report(true)
            .allow_empty(true)
            .interact_text()?;
        if scope.is_empty() {
            None
        } else {
            Some(scope.to_lowercase())
        }
    };

    // > Short description
    let desc = Input::<String>::with_theme(&ColorfulTheme::default())
        .with_prompt("Commit description")
        .report(true)
        .interact()?
        .trim()
        .to_lowercase_first();

    let mut msg = ConvcoMessage {
        r#type,
        scope,
        is_breaking: false,
        desc,
        body: None,
        footer: None,
    };

    // > breaking changes
    // dev note: 'Confirm' does not remove the blinking cursor
    match Confirm::with_theme(&ColorfulTheme::default())
        .with_prompt("Breaking change ?")
        .report(true)
        .default(false)
        .interact()?
    {
        false => {}
        true => {
            let breaking_change_desc = Input::<String>::with_theme(&ColorfulTheme::default())
                .with_prompt("Breaking change description".to_string())
                .report(true)
                .allow_empty(true)
                .interact_text()?;
            msg.add_breaking_change(&breaking_change_desc);
        }
    }

    // > body
    match Confirm::with_theme(&ColorfulTheme::default())
        .with_prompt("Commit message body ?")
        .report(true)
        .default(false)
        .interact()?
    {
        false => {}
        true => {
            let text = Editor::new()
                .require_save(true)
                .trim_newlines(true)
                .edit("")?;
            msg.body = text;
        }
    }

    // > footer
    'footer_notes: loop {
        match Confirm::with_theme(&ColorfulTheme::default())
            .with_prompt("Add footer note ?")
            .report(true)
            .default(false)
            .interact()?
        {
            false => break 'footer_notes,
            true => {
                let key = Input::<String>::with_theme(&ColorfulTheme::default())
                    .with_prompt("Key ?".to_string())
                    .report(true)
                    .allow_empty(false)
                    .interact_text()?;
                let value = Input::<String>::with_theme(&ColorfulTheme::default())
                    .with_prompt("Value ?".to_string())
                    .report(true)
                    .allow_empty(false)
                    .interact_text()?;
                msg.add_footer_note(&key, &value);
            }
        }
    }

    Ok(msg)
}