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)]
99 pub verbose: bool,
100
101 #[arg(long="values-file", value_parser, alias="template-values-file", value_name="FILE", help_heading = heading::OUTPUT_PARAMETERS)]
104 pub template_values_file: Option<String>,
105
106 #[arg(long, short, requires("name"), action)]
109 pub silent: bool,
110
111 #[arg(short, long, value_parser)]
114 pub config: Option<PathBuf>,
115
116 #[arg(long, value_parser, help_heading = heading::OUTPUT_PARAMETERS)]
118 pub vcs: Option<Vcs>,
119
120 #[arg(long, conflicts_with = "bin", action, help_heading = heading::OUTPUT_PARAMETERS)]
122 pub lib: bool,
123
124 #[arg(long, conflicts_with = "lib", action, help_heading = heading::OUTPUT_PARAMETERS)]
126 pub bin: bool,
127
128 #[arg(short = 'i', long = "identity", value_parser, value_name="IDENTITY", help_heading = heading::GIT_PARAMETERS)]
130 pub ssh_identity: Option<PathBuf>,
131
132 #[arg(long = "gitconfig", value_parser, value_name="GITCONFIG_FILE", help_heading = heading::GIT_PARAMETERS)]
134 pub gitconfig: Option<PathBuf>,
135
136 #[arg(long, short, number_of_values = 1, value_parser, help_heading = heading::OUTPUT_PARAMETERS)]
138 pub define: Vec<String>,
139
140 #[arg(long, action, help_heading = heading::OUTPUT_PARAMETERS)]
143 pub init: bool,
144
145 #[arg(long, value_parser, value_name="PATH", help_heading = heading::OUTPUT_PARAMETERS)]
147 pub destination: Option<PathBuf>,
148
149 #[arg(long, action, help_heading = heading::OUTPUT_PARAMETERS)]
151 pub force_git_init: bool,
152
153 #[arg(short, long, action, help_heading = heading::OUTPUT_PARAMETERS)]
157 pub allow_commands: bool,
158
159 #[arg(short, long, action, help_heading = heading::OUTPUT_PARAMETERS)]
161 pub overwrite: bool,
162
163 #[arg(long, action, help_heading = heading::GIT_PARAMETERS)]
165 pub skip_submodules: bool,
166
167 #[arg(skip)]
169 pub other_args: Option<Vec<String>>,
170}
171
172impl Default for GenerateArgs {
173 fn default() -> Self {
174 Self {
175 template_path: TemplatePath::default(),
176 list_favorites: false,
177 name: None,
178 force: false,
179 verbose: false,
180 template_values_file: None,
181 silent: false,
182 config: None,
183 vcs: None,
184 lib: true,
185 bin: false,
186 ssh_identity: None,
187 gitconfig: None,
188 define: Vec::default(),
189 init: false,
190 destination: None,
191 force_git_init: false,
192 allow_commands: false,
193 overwrite: false,
194 skip_submodules: false,
195 other_args: None,
196 }
197 }
198}
199
200#[derive(Default, Debug, Clone, Args)]
201pub struct TemplatePath {
202 #[arg(required_unless_present_any(&["SpecificPath"]))]
205 pub auto_path: Option<String>,
206
207 #[arg()]
209 pub subfolder: Option<String>,
210
211 #[arg(long, action, group("SpecificPath"))]
217 pub test: bool,
218
219 #[arg(short, long, group("SpecificPath"), help_heading = heading::TEMPLATE_SELECTION)]
226 pub git: Option<String>,
227
228 #[arg(short, long, conflicts_with_all = ["revision", "tag"], help_heading = heading::GIT_PARAMETERS)]
230 pub branch: Option<String>,
231
232 #[arg(short, long, conflicts_with_all = ["revision", "branch"], help_heading = heading::GIT_PARAMETERS)]
234 pub tag: Option<String>,
235
236 #[arg(short, long, conflicts_with_all = ["tag", "branch"], alias = "rev", help_heading = heading::GIT_PARAMETERS)]
238 pub revision: Option<String>,
239
240 #[arg(short, long, group("SpecificPath"), help_heading = heading::TEMPLATE_SELECTION)]
242 pub path: Option<String>,
243
244 #[arg(long, group("SpecificPath"), help_heading = heading::TEMPLATE_SELECTION)]
247 pub favorite: Option<String>,
248}
249
250impl TemplatePath {
251 pub fn any_path(&self) -> &str {
255 self.git
256 .as_ref()
257 .or(self.path.as_ref())
258 .or(self.favorite.as_ref())
259 .or(self.auto_path.as_ref())
260 .unwrap()
261 }
262
263 pub const fn git(&self) -> Option<&(impl AsRef<str> + '_)> {
264 self.git.as_ref()
265 }
266
267 pub const fn branch(&self) -> Option<&(impl AsRef<str> + '_)> {
268 self.branch.as_ref()
269 }
270
271 pub const fn tag(&self) -> Option<&(impl AsRef<str> + '_)> {
272 self.tag.as_ref()
273 }
274
275 pub const fn revision(&self) -> Option<&(impl AsRef<str> + '_)> {
276 self.revision.as_ref()
277 }
278
279 pub const fn path(&self) -> Option<&(impl AsRef<str> + '_)> {
280 self.path.as_ref()
281 }
282
283 pub const fn favorite(&self) -> Option<&(impl AsRef<str> + '_)> {
284 self.favorite.as_ref()
285 }
286
287 pub const fn auto_path(&self) -> Option<&(impl AsRef<str> + '_)> {
288 self.auto_path.as_ref()
289 }
290
291 pub const fn subfolder(&self) -> Option<&(impl AsRef<str> + '_)> {
292 if self.git.is_some() || self.path.is_some() || self.favorite.is_some() {
293 self.auto_path.as_ref()
294 } else {
295 self.subfolder.as_ref()
296 }
297 }
298}
299
300#[derive(Debug, Parser, Clone, Copy, PartialEq, Eq, Deserialize)]
301pub enum Vcs {
302 None,
303 Git,
304}
305
306impl FromStr for Vcs {
307 type Err = anyhow::Error;
308
309 fn from_str(s: &str) -> Result<Self, Self::Err> {
310 match s.to_uppercase().as_str() {
311 "NONE" => Ok(Self::None),
312 "GIT" => Ok(Self::Git),
313 _ => Err(anyhow!("Must be one of 'git' or 'none'")),
314 }
315 }
316}
317
318impl Vcs {
319 pub fn initialize(&self, project_dir: &Path, branch: Option<&str>, force: bool) -> Result<()> {
320 match self {
321 Self::None => Ok(()),
322 Self::Git => git::init(project_dir, branch, force)
323 .map(|_| ())
324 .map_err(anyhow::Error::from),
325 }
326 }
327
328 pub const fn is_none(&self) -> bool {
329 matches!(self, Self::None)
330 }
331}
332
333#[cfg(test)]
334mod cli_tests {
335 use super::*;
336
337 #[test]
338 fn test_cli() {
339 use clap::CommandFactory;
340 Cli::command().debug_assert()
341 }
342}