use std::{path::PathBuf, process::Command};
use cargo_metadata::{Metadata, Package, camino::Utf8Path};
use clap::ArgAction;
use miette::{IntoDiagnostic, WrapErr};
use tracing::Level;
use crate::{
Result, diff,
vcs::{self, Status},
};
#[derive(Debug, Clone, Copy, Default, clap::Args)]
pub(crate) struct Verbosity {
#[clap(long, short = 'v', action = ArgAction::Count, global = true)]
verbose: u8,
#[clap(
long,
short = 'q',
action = ArgAction::Count,
global = true,
conflicts_with = "verbose"
)]
quiet: u8,
}
impl From<Verbosity> for Option<Level> {
fn from(verb: Verbosity) -> Self {
let level = i8::try_from(verb.verbose).unwrap_or(i8::MAX)
- i8::try_from(verb.quiet).unwrap_or(i8::MAX);
match level {
i8::MIN..=-3 => None,
-2 => Some(Level::ERROR),
-1 => Some(Level::WARN),
0 => Some(Level::INFO),
1 => Some(Level::DEBUG),
2..=i8::MAX => Some(Level::TRACE),
}
}
}
#[derive(Debug, Clone, Default, clap::Args)]
pub(crate) struct WorkspaceArgs {
#[clap(long, value_name = "PATH")]
manifest_path: Option<PathBuf>,
}
impl WorkspaceArgs {
pub(crate) fn metadata(&self) -> Result<Metadata> {
let mut cmd = cargo_metadata::MetadataCommand::new();
if let Some(path) = &self.manifest_path {
cmd.manifest_path(path);
}
let workspace = cmd
.exec()
.into_diagnostic()
.wrap_err("failed to get package metadata")?;
Ok(workspace)
}
}
#[derive(Debug, Clone, Default, clap::Args)]
pub(crate) struct PackageArgs {
#[clap(long)]
workspace: bool,
#[clap(long, short, value_name = "SPEC")]
package: Option<Vec<String>>,
}
impl PackageArgs {
pub(crate) fn packages<'a>(&self, workspace: &'a Metadata) -> Result<Vec<&'a Package>> {
if self.workspace {
return Ok(workspace.workspace_packages());
}
if let Some(names) = &self.package {
let packages = names
.iter()
.map(|name| {
workspace
.packages
.iter()
.find(|pkg| *pkg.name == *name)
.ok_or_else(|| miette!("package not found: {name}"))
})
.collect();
return packages;
}
let package = workspace
.root_package()
.ok_or_else(|| miette!("no root package found"))?;
Ok(vec![package])
}
}
#[derive(Debug, Clone, Default, clap::Args)]
pub(crate) struct FeatureArgs {
#[clap(long, short = 'F', value_name = "FEATURES")]
features: Vec<String>,
#[clap(long)]
all_features: bool,
#[clap(long)]
no_default_features: bool,
}
impl FeatureArgs {
pub(crate) fn cargo_args(&self) -> impl Iterator<Item = &str> + '_ {
self.all_features
.then_some("--all-features")
.into_iter()
.chain(self.features.iter().flat_map(|f| ["--feature", f]))
.chain(self.no_default_features.then_some("--no-default-features"))
}
}
#[derive(Debug, Clone, Default, clap::Args)]
pub(crate) struct ToolchainArgs {
#[clap(long)]
toolchain: Option<String>,
}
impl ToolchainArgs {
pub(crate) fn cargo_command(&self) -> Command {
if let Some(toolchain) = &self.toolchain {
let mut command = Command::new("rustup");
command.args(["run", toolchain, "cargo"]);
command
} else {
Command::new("cargo")
}
}
}
#[derive(Debug, Clone, Default, clap::Args)]
pub(crate) struct FixArgs {
#[clap(long)]
check: bool,
#[clap(long)]
allow_no_vcs: bool,
#[clap(long)]
allow_dirty: bool,
#[clap(long)]
allow_staged: bool,
}
impl FixArgs {
pub(crate) fn check_update_allowed(
&self,
readme_path: impl AsRef<Utf8Path>,
old_text: &str,
new_text: &str,
) -> Result<()> {
let readme_path = readme_path.as_ref();
if self.check {
bail!(
"README is not synced: {readme_path}\n{}",
diff::diff(old_text, new_text)
);
}
if self.allow_no_vcs {
return Ok(());
}
let vcs = vcs::discover(readme_path)
.wrap_err_with(|| format!("failed to detect VCS for README: {readme_path}"))?
.ok_or_else(|| miette!("no VSC detected for README: {readme_path}"))?;
let workdir = vcs
.workdir()
.ok_or_else(|| miette!("VCS workdir not found for README: {readme_path}"))?;
let path_in_repo = readme_path.strip_prefix(workdir).unwrap();
let status = vcs
.status_file(path_in_repo)
.wrap_err_with(|| format!("failed to get VCS status for README: {readme_path}"))?;
match status {
Status::Dirty => {
if !self.allow_dirty {
bail!("README has uncommitted changes: {readme_path}");
}
}
Status::Staged => {
if !self.allow_dirty && !self.allow_staged {
bail!("README has staged changes: {readme_path}");
}
}
Status::Clean => {}
}
Ok(())
}
}