Skip to main content

cargo_uv/
cli.rs

1mod action;
2mod git_ops;
3mod manifest;
4mod suppress;
5mod workspace;
6
7pub use crate::cli::{
8    action::Action, git_ops::GitOps, manifest::Manifest, suppress::Suppress, workspace::Workspace,
9};
10use std::{ops::Deref, path::PathBuf};
11
12use crate::{GitBuilder, Result};
13use cargo_metadata::Metadata;
14use miette::IntoDiagnostic;
15use semver::Version;
16use tracing::{Level, debug, instrument};
17
18use crate::current_span;
19// use clap::ValueHint;
20
21static GIT_HEADER: &str = "Git";
22static CARGO_HEADER: &str = "Cargo";
23static WORKSPACE_HEADER: &str = "Package Selection";
24
25pub const CLAP_STYLING: clap::builder::styling::Styles = clap::builder::styling::Styles::styled()
26    .header(clap_cargo::style::HEADER)
27    .usage(clap_cargo::style::USAGE)
28    .literal(clap_cargo::style::LITERAL)
29    .placeholder(clap_cargo::style::PLACEHOLDER)
30    .error(clap_cargo::style::ERROR)
31    .valid(clap_cargo::style::VALID)
32    .invalid(clap_cargo::style::INVALID);
33
34#[derive(clap::Parser, Debug)]
35#[command(about, long_about=None, version)]
36#[command(styles=CLAP_STYLING)]
37pub struct Cli {
38    /// Action to affect the package version.
39    #[arg(default_value_t = Action::default())]
40    pub action: Action,
41
42    #[arg(long, help="Sets the pre-release segment for the new version.", value_parser = semver::Prerelease::new)]
43    pub pre: Option<semver::Prerelease>,
44
45    #[arg(long, help = "Sets the build metadata for the new version.")]
46    pub build: Option<semver::BuildMetadata>,
47
48    /// Runs the `cargo publish`
49    #[arg(short, long, help_heading = CARGO_HEADER)]
50    pub cargo_publish: bool,
51
52    /// What to suppress from stdout
53    #[arg(short = 'Q', long, default_value = Suppress::default())]
54    pub suppress: Suppress,
55
56    /// adds 'no_verify' to cargo publish command.
57    #[arg(long, help_heading = CARGO_HEADER)]
58    pub no_verify: bool,
59
60    #[arg(short = 'n', long, help = "Allows program to work in a dirty repo.")]
61    pub allow_dirty: bool,
62
63    #[command(flatten)]
64    pub git_ops: GitOps,
65
66    /// All commands run as if they run in the the directory of the Cargo.toml set.
67    #[command(flatten)]
68    pub manifest: Manifest,
69
70    #[command(flatten)]
71    pub workspace: Workspace,
72
73    #[arg(short, long, help = "Bypass version bump checks.")]
74    pub force_version: bool,
75
76    #[arg(short, long, help = "Allows git tag to occur in a dirty repo.")]
77    pub dry_run: bool,
78
79    #[command(flatten)]
80    pub color: colorchoice_clap::Color,
81
82    #[command(flatten)]
83    pub verbosity: clap_verbosity_flag::Verbosity,
84
85    /// New version to set. Ignored if action isn't set.
86    #[arg(value_parser = Version::parse)]
87    pub set_version: Option<Version>,
88
89    #[arg(skip)]
90    metadata: Option<Metadata>,
91}
92
93impl Cli {
94    pub fn root_dir(&self) -> Result<PathBuf> {
95        let root = match self.manifest.manifest_path.clone() {
96            Some(p) => p
97                .canonicalize()
98                .into_diagnostic()?
99                .parent()
100                .map(|p| p.to_path_buf())
101                .ok_or_else(|| {
102                    miette::miette!("Failed to canonicaliaze correctly: {}", &p.display())
103                })?,
104            None => PathBuf::from("."),
105        };
106        tracing::info!("Root: {}", &root.display());
107        Ok(root)
108    }
109
110    #[instrument(skip_all, fields(root_cargo_file), name = "Cli::get_metadata")]
111    pub fn get_metadata<'m>(&'m mut self) -> Result<&'m Metadata> {
112        if let Some(ref m) = self.metadata {
113            Ok(m)
114        } else {
115            self.refresh_metadata()?;
116            let cargo_file = self
117                .metadata
118                .as_ref()
119                .unwrap()
120                .workspace_root
121                .join("Cargo.toml")
122                .to_string();
123            current_span!().record("root_cargo_file", cargo_file);
124            tracing::info!("Package metadata found.");
125            self.metadata
126                .as_ref()
127                .ok_or_else(|| miette::miette!("Failed to get metadata somehow..."))
128        }
129    }
130
131    #[instrument(skip_all, fields(root_cargo_file), name = "Cli::refresh_metadata")]
132    pub fn refresh_metadata(&mut self) -> Result<()> {
133        let mut cmd = self.manifest.metadata();
134        cmd.no_deps(); // Confirmed does have an impact on performance.
135        self.metadata = Some(cmd.exec().into_diagnostic()?);
136        Ok(())
137    }
138
139    #[instrument(skip_all, fields(self.verbosity), name ="Cli::tracing_level")]
140    pub fn tracing_level(&self) -> Option<Level> {
141        self.verbosity.tracing_level()
142    }
143
144    #[instrument(skip_all, fields(self.action), name ="Cli::action")]
145    pub fn action(&self) -> Action {
146        let action = self.action;
147        tracing::debug!("Action: {}", action);
148        action
149    }
150
151    #[instrument(skip_all, fields(self.allow_dirty), name ="Cli::allow_dirty")]
152    pub fn allow_dirty(&self) -> bool {
153        tracing::debug!("allow_dirty");
154        self.allow_dirty
155    }
156
157    #[instrument(skip_all, fields(self.allow_dirty, count), name ="Cli::try_allow_dirty")]
158    pub fn try_allow_dirty(&self) -> Result<()> {
159        if self.allow_dirty {
160            return Ok(());
161        }
162        let git = GitBuilder::new().root_directory(self.root_dir()?).build();
163        let files: crate::GitFiles = git.dirty_files()?;
164        let count = files.len();
165
166        if count != 0 {
167            miette::bail!(
168                help = "Use '--allow-dirty' to avoid this check.",
169                "{} file/s in the working directory contain changes that were not yet committed into git.{}",
170                count,
171                files
172            )
173        } else {
174            Ok(())
175        }
176    }
177
178    #[instrument(skip_all, fields(self.dry_run), name ="Cli::dry_run")]
179    pub fn dry_run(&self) -> bool {
180        self.dry_run
181    }
182
183    #[instrument(skip_all, fields(message), name = "Cli::git_message")]
184    pub fn git_message(&self) -> Option<String> {
185        let msg = self.git_ops.message.clone();
186        current_span!().record("message", &msg);
187        tracing::debug!("Fetching the git message if available.");
188        msg
189    }
190
191    #[instrument(skip_all, fields(self.force_version), name ="Cli::force_version")]
192    pub fn force_version(&self) -> bool {
193        tracing::debug!("Checking if forcing version.");
194        self.force_version
195    }
196
197    #[instrument(skip_all, fields(git_tag), name = "Cli::git_tag")]
198    pub fn git_tag(&self) -> bool {
199        let tag = self.git_ops.git_tag;
200        current_span!().record("git_tag", tag);
201        debug!("Checking for git tag flag...");
202        tag
203    }
204
205    #[instrument(skip_all, fields(git_push), name = "Cli::git_push")]
206    pub fn git_push(&self) -> bool {
207        let push = self.git_ops.git_push;
208        current_span!().record("git_push", push);
209        debug!("Checking for git push flag...");
210        push
211    }
212
213    #[instrument(skip_all, fields(cargo_publish), name = "Cli::cargo_publish")]
214    pub fn cargo_publish(&self) -> bool {
215        let publish = self.cargo_publish;
216        current_span!().record("cargo_publish", publish);
217        debug!("Checking for cargo publish flag...");
218        publish
219    }
220
221    pub fn no_verify(&self) -> bool {
222        self.no_verify
223    }
224
225    // /// Partition workspace members into those selected and those excluded.
226    // ///
227    // /// Notes:
228    // /// - Requires the features `cargo_metadata`.
229    // /// - Requires not calling `MetadataCommand::no_deps`
230    // pub fn partition_packages<'p>(
231    //     &'p mut self,
232    // ) -> Result<(
233    //     Vec<&'p cargo_metadata::Package>,
234    //     Vec<&'p cargo_metadata::Package>,
235    // )> {
236    //     self.refresh_metadata()?;
237    //     match self.metadata() {
238    //         Some(meta) => Ok(self.workspace.partition_packages(meta)),
239    //         None => bail!("Metadata not fetched."),
240    //     }
241    // }
242}
243
244impl Deref for Cli {
245    type Target = Workspace;
246
247    fn deref(&self) -> &Workspace {
248        &self.workspace
249    }
250}
251
252impl Cli {
253    pub fn metadata(&self) -> Option<&Metadata> {
254        self.metadata.as_ref()
255    }
256}