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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
use std::{
    path::{Path, PathBuf},
    str::FromStr,
};

use anyhow::{anyhow, Result};
use clap::{Args, Parser};
use serde::Deserialize;

use crate::git;

/// Styles from <https://github.com/rust-lang/cargo/blob/master/src/cargo/util/style.rs>
mod style {
    use anstyle::*;
    use clap::builder::Styles;

    const HEADER: Style = AnsiColor::Green.on_default().effects(Effects::BOLD);
    const USAGE: Style = AnsiColor::Green.on_default().effects(Effects::BOLD);
    const LITERAL: Style = AnsiColor::Cyan.on_default().effects(Effects::BOLD);
    const PLACEHOLDER: Style = AnsiColor::Cyan.on_default();
    const ERROR: Style = AnsiColor::Red.on_default().effects(Effects::BOLD);
    const VALID: Style = AnsiColor::Cyan.on_default().effects(Effects::BOLD);
    const INVALID: Style = AnsiColor::Yellow.on_default().effects(Effects::BOLD);

    pub const STYLES: Styles = {
        Styles::styled()
            .header(HEADER)
            .usage(USAGE)
            .literal(LITERAL)
            .placeholder(PLACEHOLDER)
            .error(ERROR)
            .valid(VALID)
            .invalid(INVALID)
            .error(ERROR)
    };
}

mod heading {
    pub const GIT_PARAMETERS: &str = "Git Parameters";
    pub const TEMPLATE_SELECTION: &str = "Template Selection";
    pub const OUTPUT_PARAMETERS: &str = "Output Parameters";
}

#[derive(Parser)]
#[command(
    name = "cargo generate",
    bin_name = "cargo",
    arg_required_else_help(true),
    version,
    about,
    next_line_help(false),
    styles(style::STYLES)
)]
pub enum Cli {
    #[command(name = "generate", visible_alias = "gen")]
    Generate(GenerateArgs),
}

#[derive(Clone, Debug, Args)]
#[command(arg_required_else_help(true), version, about)]
pub struct GenerateArgs {
    #[command(flatten)]
    pub template_path: TemplatePath,

    /// List defined favorite templates from the config
    #[arg(
        long,
        action,
        group("SpecificPath"),
        conflicts_with_all(&[
            "git", "path", "subfolder", "branch",
            "name",
            "force",
            "silent",
            "vcs",
            "lib",
            "bin",
            "define",
            "init",
            "template_values_file",
            "ssh_identity",
            "test",
        ])
    )]
    pub list_favorites: bool,

    /// Directory to create / project name; if the name isn't in kebab-case, it will be converted
    /// to kebab-case unless `--force` is given.
    #[arg(long, short, value_parser, help_heading = heading::OUTPUT_PARAMETERS)]
    pub name: Option<String>,

    /// Don't convert the project name to kebab-case before creating the directory. Note that cargo
    /// generate won't overwrite an existing directory, even if `--force` is given.
    #[arg(long, short, action, help_heading = heading::OUTPUT_PARAMETERS)]
    pub force: bool,

    /// Enables more verbose output.
    #[arg(long, short, action)]
    pub verbose: bool,

    /// Pass template values through a file. Values should be in the format `key=value`, one per
    /// line
    #[arg(long="values-file", value_parser, alias="template-values-file", value_name="FILE", help_heading = heading::OUTPUT_PARAMETERS)]
    pub template_values_file: Option<String>,

    /// If silent mode is set all variables will be extracted from the template_values_file. If a
    /// value is missing the project generation will fail
    #[arg(long, short, requires("name"), action)]
    pub silent: bool,

    /// Use specific configuration file. Defaults to $CARGO_HOME/cargo-generate or
    /// $HOME/.cargo/cargo-generate
    #[arg(short, long, value_parser)]
    pub config: Option<PathBuf>,

    /// Specify the VCS used to initialize the generated template.
    #[arg(long, value_parser, help_heading = heading::OUTPUT_PARAMETERS)]
    pub vcs: Option<Vcs>,

    /// Populates template variable `crate_type` with value `"lib"`
    #[arg(long, conflicts_with = "bin", action, help_heading = heading::OUTPUT_PARAMETERS)]
    pub lib: bool,

    /// Populates a template variable `crate_type` with value `"bin"`
    #[arg(long, conflicts_with = "lib", action, help_heading = heading::OUTPUT_PARAMETERS)]
    pub bin: bool,

    /// Use a different ssh identity
    #[arg(short = 'i', long = "identity", value_parser, value_name="IDENTITY", help_heading = heading::GIT_PARAMETERS)]
    pub ssh_identity: Option<PathBuf>,

    /// Define a value for use during template expansion. E.g `--define foo=bar`
    #[arg(long, short, number_of_values = 1, value_parser, help_heading = heading::OUTPUT_PARAMETERS)]
    pub define: Vec<String>,

    /// Generate the template directly into the current dir. No subfolder will be created and no vcs
    /// is initialized.
    #[arg(long, action, help_heading = heading::OUTPUT_PARAMETERS)]
    pub init: bool,

    /// Generate the template directly at the given path.
    #[arg(long, value_parser, value_name="PATH", help_heading = heading::OUTPUT_PARAMETERS)]
    pub destination: Option<PathBuf>,

    /// Will enforce a fresh git init on the generated project
    #[arg(long, action, help_heading = heading::OUTPUT_PARAMETERS)]
    pub force_git_init: bool,

    /// Allows running system commands without being prompted. Warning: Setting this flag will
    /// enable the template to run arbitrary system commands without user confirmation. Use at your
    /// own risk and be sure to review the template code beforehand.
    #[arg(short, long, action, help_heading = heading::OUTPUT_PARAMETERS)]
    pub allow_commands: bool,

    /// Allow the template to overwrite existing files in the destination.
    #[arg(short, long, action, help_heading = heading::OUTPUT_PARAMETERS)]
    pub overwrite: bool,

    /// Skip downloading git submodules (if there are any)
    #[arg(long, action, help_heading = heading::GIT_PARAMETERS)]
    pub skip_submodules: bool,

    /// All args after "--" on the command line.
    #[arg(skip)]
    pub other_args: Option<Vec<String>>,
}

impl Default for GenerateArgs {
    fn default() -> Self {
        Self {
            template_path: TemplatePath::default(),
            list_favorites: false,
            name: None,
            force: false,
            verbose: false,
            template_values_file: None,
            silent: false,
            config: None,
            vcs: None,
            lib: true,
            bin: false,
            ssh_identity: None,
            define: Vec::default(),
            init: false,
            destination: None,
            force_git_init: false,
            allow_commands: false,
            overwrite: false,
            skip_submodules: false,
            other_args: None,
        }
    }
}

#[derive(Default, Debug, Clone, Args)]
pub struct TemplatePath {
    /// Auto attempt to use as either `--git` or `--favorite`. If either is specified explicitly,
    /// use as subfolder.
    #[arg(required_unless_present_any(&["SpecificPath"]))]
    pub auto_path: Option<String>,

    /// Specifies the subfolder within the template repository to be used as the actual template.
    #[arg()]
    pub subfolder: Option<String>,

    /// Expand $CWD as a template, then run `cargo test` on the expansion (set
    /// $CARGO_GENERATE_TEST_CMD to override test command).
    ///
    /// Any arguments given after the `--test` argument, will be used as arguments for the test
    /// command.
    #[arg(long, action, group("SpecificPath"))]
    pub test: bool,

    /// Git repository to clone template from. Can be a URL (like
    /// `https://github.com/rust-cli/cli-template`), a path (relative or absolute), or an
    /// `owner/repo` abbreviated GitHub URL (like `rust-cli/cli-template`).
    ///
    /// Note that cargo generate will first attempt to interpret the `owner/repo` form as a
    /// relative path and only try a GitHub URL if the local path doesn't exist.
    #[arg(short, long, group("SpecificPath"), help_heading = heading::TEMPLATE_SELECTION)]
    pub git: Option<String>,

    /// Branch to use when installing from git
    #[arg(short, long, conflicts_with_all = ["revision", "tag"], help_heading = heading::GIT_PARAMETERS)]
    pub branch: Option<String>,

    /// Tag to use when installing from git
    #[arg(short, long, conflicts_with_all = ["revision", "branch"], help_heading = heading::GIT_PARAMETERS)]
    pub tag: Option<String>,

    /// Git revision to use when installing from git (e.g. a commit hash)
    #[arg(short, long, conflicts_with_all = ["tag", "branch"], alias = "rev", help_heading = heading::GIT_PARAMETERS)]
    pub revision: Option<String>,

    /// Local path to copy the template from. Can not be specified together with --git.
    #[arg(short, long, group("SpecificPath"), help_heading = heading::TEMPLATE_SELECTION)]
    pub path: Option<String>,

    /// Generate a favorite template as defined in the config. In case the favorite is undefined,
    /// use in place of the `--git` option, otherwise specifies the subfolder
    #[arg(long, group("SpecificPath"), help_heading = heading::TEMPLATE_SELECTION)]
    pub favorite: Option<String>,
}

impl TemplatePath {
    /// # Panics
    /// Will panic if no path to a template has been set at all,
    /// which is never if Clap is initialized properly.
    pub fn any_path(&self) -> &str {
        self.git
            .as_ref()
            .or(self.path.as_ref())
            .or(self.favorite.as_ref())
            .or(self.auto_path.as_ref())
            .unwrap()
    }

    pub const fn git(&self) -> Option<&(impl AsRef<str> + '_)> {
        self.git.as_ref()
    }

    pub const fn branch(&self) -> Option<&(impl AsRef<str> + '_)> {
        self.branch.as_ref()
    }

    pub const fn tag(&self) -> Option<&(impl AsRef<str> + '_)> {
        self.tag.as_ref()
    }

    pub const fn revision(&self) -> Option<&(impl AsRef<str> + '_)> {
        self.revision.as_ref()
    }

    pub const fn path(&self) -> Option<&(impl AsRef<str> + '_)> {
        self.path.as_ref()
    }

    pub const fn favorite(&self) -> Option<&(impl AsRef<str> + '_)> {
        self.favorite.as_ref()
    }

    pub const fn auto_path(&self) -> Option<&(impl AsRef<str> + '_)> {
        self.auto_path.as_ref()
    }

    pub const fn subfolder(&self) -> Option<&(impl AsRef<str> + '_)> {
        if self.git.is_some() || self.path.is_some() || self.favorite.is_some() {
            self.auto_path.as_ref()
        } else {
            self.subfolder.as_ref()
        }
    }
}

#[derive(Debug, Parser, Clone, Copy, PartialEq, Eq, Deserialize)]
pub enum Vcs {
    None,
    Git,
}

impl FromStr for Vcs {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_uppercase().as_str() {
            "NONE" => Ok(Self::None),
            "GIT" => Ok(Self::Git),
            _ => Err(anyhow!("Must be one of 'git' or 'none'")),
        }
    }
}

impl Vcs {
    pub fn initialize(&self, project_dir: &Path, branch: Option<&str>, force: bool) -> Result<()> {
        match self {
            Self::None => Ok(()),
            Self::Git => git::init(project_dir, branch, force)
                .map(|_| ())
                .map_err(anyhow::Error::from),
        }
    }

    pub const fn is_none(&self) -> bool {
        matches!(self, Self::None)
    }
}

#[cfg(test)]
mod cli_tests {
    use super::*;

    #[test]
    fn test_cli() {
        use clap::CommandFactory;
        Cli::command().debug_assert()
    }
}