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;
19static 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 #[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 #[arg(short, long, help_heading = CARGO_HEADER)]
50 pub cargo_publish: bool,
51
52 #[arg(short = 'Q', long, default_value = Suppress::default())]
54 pub suppress: Suppress,
55
56 #[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 #[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 #[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(); 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 }
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}