1use std::{
2 path::{Path, PathBuf},
3 str::FromStr,
4};
5
6use anyhow::{anyhow, Result};
7use clap::{Args, Parser};
8use serde::Deserialize;
9
10use crate::git;
11
12mod style {
14 use anstyle::*;
15 use clap::builder::Styles;
16
17 const HEADER: Style = AnsiColor::Green.on_default().effects(Effects::BOLD);
18 const USAGE: Style = AnsiColor::Green.on_default().effects(Effects::BOLD);
19 const LITERAL: Style = AnsiColor::Cyan.on_default().effects(Effects::BOLD);
20 const PLACEHOLDER: Style = AnsiColor::Cyan.on_default();
21 const ERROR: Style = AnsiColor::Red.on_default().effects(Effects::BOLD);
22 const VALID: Style = AnsiColor::Cyan.on_default().effects(Effects::BOLD);
23 const INVALID: Style = AnsiColor::Yellow.on_default().effects(Effects::BOLD);
24
25 pub const STYLES: Styles = {
26 Styles::styled()
27 .header(HEADER)
28 .usage(USAGE)
29 .literal(LITERAL)
30 .placeholder(PLACEHOLDER)
31 .error(ERROR)
32 .valid(VALID)
33 .invalid(INVALID)
34 .error(ERROR)
35 };
36}
37
38mod heading {
39 pub const GIT_PARAMETERS: &str = "Git Parameters";
40 pub const TEMPLATE_SELECTION: &str = "Template Selection";
41 pub const OUTPUT_PARAMETERS: &str = "Output Parameters";
42}
43
44#[derive(Parser)]
45#[command(
46 name = "cargo generate",
47 bin_name = "cargo",
48 arg_required_else_help(true),
49 version,
50 about,
51 next_line_help(false),
52 styles(style::STYLES)
53)]
54pub enum Cli {
55 #[command(name = "generate", visible_alias = "gen")]
56 Generate(GenerateArgs),
57}
58
59#[derive(Clone, Debug, Args)]
60#[command(arg_required_else_help(true), version, about)]
61pub struct GenerateArgs {
62 #[command(flatten)]
63 pub template_path: TemplatePath,
64
65 #[arg(
67 long,
68 action,
69 group("SpecificPath"),
70 conflicts_with_all(&[
71 "git", "path", "subfolder", "branch",
72 "name",
73 "force",
74 "silent",
75 "vcs",
76 "lib",
77 "bin",
78 "define",
79 "init",
80 "template_values_file",
81 "ssh_identity",
82 "test",
83 ])
84 )]
85 pub list_favorites: bool,
86
87 #[arg(long, short, value_parser, help_heading = heading::OUTPUT_PARAMETERS)]
90 pub name: Option<String>,
91
92 #[arg(long, short, action, help_heading = heading::OUTPUT_PARAMETERS)]
95 pub force: bool,
96
97 #[arg(long, short, action, conflicts_with = "quiet")]
99 pub verbose: bool,
100
101 #[arg(
104 long,
105 short,
106 action,
107 conflicts_with = "verbose",
108 requires = "continue_on_error"
109 )]
110 pub quiet: bool,
111
112 #[arg(long, action)]
114 pub continue_on_error: bool,
115
116 #[arg(long="values-file", value_parser, alias="template-values-file", value_name="FILE", help_heading = heading::OUTPUT_PARAMETERS)]
119 pub template_values_file: Option<String>,
120
121 #[arg(long, short, requires("name"), action)]
124 pub silent: bool,
125
126 #[arg(short, long, value_parser)]
129 pub config: Option<PathBuf>,
130
131 #[arg(long, value_parser, help_heading = heading::OUTPUT_PARAMETERS)]
133 pub vcs: Option<Vcs>,
134
135 #[arg(long, conflicts_with = "bin", action, help_heading = heading::OUTPUT_PARAMETERS)]
137 pub lib: bool,
138
139 #[arg(long, conflicts_with = "lib", action, help_heading = heading::OUTPUT_PARAMETERS)]
141 pub bin: bool,
142
143 #[arg(short = 'i', long = "identity", value_parser, value_name="IDENTITY", help_heading = heading::GIT_PARAMETERS)]
145 pub ssh_identity: Option<PathBuf>,
146
147 #[arg(long = "gitconfig", value_parser, value_name="GITCONFIG_FILE", help_heading = heading::GIT_PARAMETERS)]
149 pub gitconfig: Option<PathBuf>,
150
151 #[arg(long, short, number_of_values = 1, value_parser, help_heading = heading::OUTPUT_PARAMETERS)]
153 pub define: Vec<String>,
154
155 #[arg(long, action, help_heading = heading::OUTPUT_PARAMETERS)]
158 pub init: bool,
159
160 #[arg(long, value_parser, value_name="PATH", help_heading = heading::OUTPUT_PARAMETERS)]
162 pub destination: Option<PathBuf>,
163
164 #[arg(long, action, help_heading = heading::OUTPUT_PARAMETERS)]
166 pub force_git_init: bool,
167
168 #[arg(short, long, action, help_heading = heading::OUTPUT_PARAMETERS)]
172 pub allow_commands: bool,
173
174 #[arg(short, long, action, help_heading = heading::OUTPUT_PARAMETERS)]
176 pub overwrite: bool,
177
178 #[arg(long, action, help_heading = heading::GIT_PARAMETERS)]
180 pub skip_submodules: bool,
181
182 #[arg(skip)]
184 pub other_args: Option<Vec<String>>,
185}
186
187impl Default for GenerateArgs {
188 fn default() -> Self {
189 Self {
190 template_path: TemplatePath::default(),
191 list_favorites: false,
192 name: None,
193 force: false,
194 verbose: false,
195 quiet: false,
196 continue_on_error: false,
197 template_values_file: None,
198 silent: false,
199 config: None,
200 vcs: None,
201 lib: true,
202 bin: false,
203 ssh_identity: None,
204 gitconfig: None,
205 define: Vec::default(),
206 init: false,
207 destination: None,
208 force_git_init: false,
209 allow_commands: false,
210 overwrite: false,
211 skip_submodules: false,
212 other_args: None,
213 }
214 }
215}
216
217#[derive(Default, Debug, Clone, Args)]
218pub struct TemplatePath {
219 #[arg(required_unless_present_any(&["SpecificPath"]))]
222 pub auto_path: Option<String>,
223
224 #[arg()]
226 pub subfolder: Option<String>,
227
228 #[arg(long, action, group("SpecificPath"))]
235 pub test: bool,
236
237 #[arg(short, long, group("SpecificPath"), help_heading = heading::TEMPLATE_SELECTION)]
244 pub git: Option<String>,
245
246 #[arg(short, long, conflicts_with_all = ["revision", "tag"], help_heading = heading::GIT_PARAMETERS)]
248 pub branch: Option<String>,
249
250 #[arg(short, long, conflicts_with_all = ["revision", "branch"], help_heading = heading::GIT_PARAMETERS)]
252 pub tag: Option<String>,
253
254 #[arg(short, long, conflicts_with_all = ["tag", "branch"], alias = "rev", help_heading = heading::GIT_PARAMETERS)]
256 pub revision: Option<String>,
257
258 #[arg(short, long, group("SpecificPath"), help_heading = heading::TEMPLATE_SELECTION)]
260 pub path: Option<String>,
261
262 #[arg(long, group("SpecificPath"), help_heading = heading::TEMPLATE_SELECTION)]
265 pub favorite: Option<String>,
266}
267
268impl TemplatePath {
269 pub fn any_path(&self) -> &str {
273 self.git
274 .as_ref()
275 .or(self.path.as_ref())
276 .or(self.favorite.as_ref())
277 .or(self.auto_path.as_ref())
278 .unwrap()
279 }
280
281 pub const fn git(&self) -> Option<&(impl AsRef<str> + '_)> {
282 self.git.as_ref()
283 }
284
285 pub const fn branch(&self) -> Option<&(impl AsRef<str> + '_)> {
286 self.branch.as_ref()
287 }
288
289 pub const fn tag(&self) -> Option<&(impl AsRef<str> + '_)> {
290 self.tag.as_ref()
291 }
292
293 pub const fn revision(&self) -> Option<&(impl AsRef<str> + '_)> {
294 self.revision.as_ref()
295 }
296
297 pub const fn path(&self) -> Option<&(impl AsRef<str> + '_)> {
298 self.path.as_ref()
299 }
300
301 pub const fn favorite(&self) -> Option<&(impl AsRef<str> + '_)> {
302 self.favorite.as_ref()
303 }
304
305 pub const fn auto_path(&self) -> Option<&(impl AsRef<str> + '_)> {
306 self.auto_path.as_ref()
307 }
308
309 pub const fn subfolder(&self) -> Option<&(impl AsRef<str> + '_)> {
310 if self.git.is_some() || self.path.is_some() || self.favorite.is_some() {
311 self.auto_path.as_ref()
312 } else {
313 self.subfolder.as_ref()
314 }
315 }
316}
317
318#[derive(Debug, Parser, Clone, Copy, PartialEq, Eq, Deserialize)]
319pub enum Vcs {
320 None,
321 Git,
322}
323
324impl FromStr for Vcs {
325 type Err = anyhow::Error;
326
327 fn from_str(s: &str) -> Result<Self, Self::Err> {
328 match s.to_uppercase().as_str() {
329 "NONE" => Ok(Self::None),
330 "GIT" => Ok(Self::Git),
331 _ => Err(anyhow!("Must be one of 'git' or 'none'")),
332 }
333 }
334}
335
336impl Vcs {
337 pub fn initialize(&self, project_dir: &Path, branch: Option<&str>, force: bool) -> Result<()> {
338 match self {
339 Self::None => Ok(()),
340 Self::Git => git::init(project_dir, branch, force)
341 .map(|_| ())
342 .map_err(anyhow::Error::from),
343 }
344 }
345
346 pub const fn is_none(&self) -> bool {
347 matches!(self, Self::None)
348 }
349}
350
351#[cfg(test)]
352mod cli_tests {
353 use super::*;
354
355 #[test]
356 fn test_cli() {
357 use clap::CommandFactory;
358 Cli::command().debug_assert()
359 }
360}