use std::path::Path;
use std::path::PathBuf;
use cargo_edit::{LocalManifest, shell_status, shell_warn, upgrade_requirement};
use clap::Args;
use crate::errors::CargoResult;
use crate::version::BumpLevel;
use crate::version::TargetVersion;
#[derive(Debug, Args)]
#[command(version)]
#[command(group = clap::ArgGroup::new("ver").multiple(false))]
pub(crate) struct VersionArgs {
#[arg(group = "ver")]
target: Option<semver::Version>,
#[arg(long, group = "ver")]
bump: Option<BumpLevel>,
#[arg(short, long)]
pub metadata: Option<String>,
#[arg(long, value_name = "PATH")]
manifest_path: Option<PathBuf>,
#[arg(
long = "package",
short = 'p',
value_name = "PKGID",
conflicts_with = "all",
conflicts_with = "workspace"
)]
pkgid: Vec<String>,
#[arg(
long,
help = "[deprecated in favor of `--workspace`]",
conflicts_with = "workspace",
conflicts_with = "pkgid"
)]
all: bool,
#[arg(long, conflicts_with = "all", conflicts_with = "pkgid")]
workspace: bool,
#[arg(long, short = 'n')]
dry_run: bool,
#[arg(long)]
exclude: Vec<String>,
#[arg(long)]
offline: bool,
#[arg(long)]
locked: bool,
#[command(flatten)]
verbose: clap_verbosity_flag::Verbosity,
#[arg(short = 'Z', value_name = "FLAG", global = true, value_enum)]
unstable_features: Vec<UnstableOptions>,
}
impl VersionArgs {
pub(crate) fn exec(self) -> CargoResult<()> {
exec(self)
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, clap::ValueEnum)]
enum UnstableOptions {}
fn exec(args: VersionArgs) -> CargoResult<()> {
env_logger::Builder::from_env("CARGO_LOG")
.filter_level(args.verbose.log_level_filter())
.init();
let VersionArgs {
target,
bump,
metadata,
manifest_path,
pkgid,
all,
dry_run,
workspace,
exclude,
locked,
offline,
verbose: _,
unstable_features: _,
} = args;
let target = match (target, bump) {
(None, None) => TargetVersion::Relative(BumpLevel::Release),
(None, Some(level)) => TargetVersion::Relative(level),
(Some(version), None) => TargetVersion::Absolute(version),
(Some(_), Some(_)) => unreachable!("clap groups should prevent this"),
};
let ws_metadata = resolve_ws(manifest_path.as_deref(), locked, offline)?;
let root_manifest_path = ws_metadata.workspace_root.as_std_path().join("Cargo.toml");
let workspace_members = find_ws_members(&ws_metadata);
if all {
shell_warn("The flag `--all` has been deprecated in favor of `--workspace`")?;
}
let workspace = workspace || all || pkgid.is_empty();
let mut selected = if workspace {
workspace_members
.iter()
.filter(|p| !exclude.contains(&p.name))
.collect::<Vec<_>>()
} else {
workspace_members
.iter()
.filter(|p| pkgid.contains(&p.name))
.collect::<Vec<_>>()
};
let update_workspace_version;
let mut changed = false;
if workspace && exclude.is_empty() {
update_workspace_version = true;
} else {
let explicit_selected = selected
.iter()
.filter(|p| {
LocalManifest::try_new(Path::new(&p.manifest_path))
.map(|m| m.version_is_inherited())
.unwrap_or(false)
})
.map(|p| p.name.as_str())
.collect::<Vec<_>>();
update_workspace_version = !explicit_selected.is_empty();
if update_workspace_version {
let implicit = workspace_members
.iter()
.filter(|i| !selected.iter().any(|s| i.id == s.id))
.filter(|i| {
LocalManifest::try_new(Path::new(&i.manifest_path))
.map(|m| m.version_is_inherited())
.unwrap_or(false)
})
.collect::<Vec<_>>();
let exclude_implicit = implicit
.iter()
.filter(|p| exclude.contains(&p.name))
.map(|p| p.name.as_str())
.collect::<Vec<_>>();
if !exclude_implicit.is_empty() {
anyhow::bail!(
"Cannot exclude {} package(s) when {} package(s) modify `workspace.package.version`",
exclude_implicit.join(", "),
explicit_selected.join(", ")
);
}
selected.extend(implicit);
}
}
if update_workspace_version {
let mut ws_manifest = LocalManifest::try_new(&root_manifest_path)?;
if let Some(current) = ws_manifest.get_workspace_version()
&& let Some(next) = target.bump(¤t, metadata.as_deref())?
{
shell_status(
"Upgrading",
&format!("workspace version from {current} to {next}"),
)?;
ws_manifest.set_workspace_version(&next);
changed = true;
if !dry_run {
ws_manifest.write()?;
}
}
}
for package in selected {
let current = &package.version;
let next = target.bump(current, metadata.as_deref())?;
if let Some(next) = next {
let mut manifest = LocalManifest::try_new(Path::new(&package.manifest_path))?;
if manifest.version_is_inherited() {
shell_status(
"Upgrading",
&format!(
"{} from {} to {} (inherited from workspace)",
package.name, current, next,
),
)?;
} else {
shell_status(
"Upgrading",
&format!("{} from {} to {}", package.name, current, next),
)?;
manifest.set_package_version(&next);
changed = true;
if !dry_run {
manifest.write()?;
}
}
let crate_root =
dunce::canonicalize(package.manifest_path.parent().expect("at least a parent"))?;
update_dependents(
&crate_root,
&next,
&root_manifest_path,
&workspace_members,
dry_run,
)?;
}
}
if changed {
resolve_ws(manifest_path.as_deref(), locked, offline)?;
}
if dry_run {
shell_warn("aborting set-version due to dry run")?;
}
Ok(())
}
fn update_dependents(
crate_root: &Path,
next: &semver::Version,
root_manifest_path: &Path,
workspace_members: &[cargo_metadata::Package],
dry_run: bool,
) -> CargoResult<()> {
{
update_dependent(crate_root, next, root_manifest_path, "workspace", dry_run)?;
}
for member in workspace_members.iter() {
update_dependent(
crate_root,
next,
member.manifest_path.as_std_path(),
&member.name,
dry_run,
)?;
}
Ok(())
}
fn is_relevant(d: &dyn toml_edit::TableLike, dep_crate_root: &Path, crate_root: &Path) -> bool {
if !d.contains_key("version") {
return false;
}
match d
.get("path")
.and_then(|i| i.as_str())
.and_then(|relpath| dunce::canonicalize(dep_crate_root.join(relpath)).ok())
{
Some(dep_path) => dep_path == crate_root,
None => false,
}
}
fn update_dependent(
crate_root: &Path,
next: &semver::Version,
manifest_path: &Path,
name: &str,
dry_run: bool,
) -> CargoResult<()> {
let mut dep_manifest = LocalManifest::try_new(manifest_path)?;
let mut changed = false;
let dep_crate_root = dep_manifest
.path
.parent()
.expect("at least a parent")
.to_owned();
for dep in dep_manifest
.get_dependency_tables_mut()
.flat_map(|t| t.iter_mut().filter_map(|(_, d)| d.as_table_like_mut()))
.filter(|d| is_relevant(*d, &dep_crate_root, crate_root))
{
let old_req = dep
.get("version")
.expect("filter ensures this")
.as_str()
.unwrap_or("*");
if let Some(new_req) = upgrade_requirement(old_req, next)? {
shell_status(
"Updating",
&format!("{name}'s dependency from {old_req} to {new_req}"),
)?;
dep.insert("version", toml_edit::value(new_req));
changed = true;
}
}
if changed && !dry_run {
dep_manifest.write()?;
}
Ok(())
}
fn resolve_ws(
manifest_path: Option<&Path>,
locked: bool,
offline: bool,
) -> CargoResult<cargo_metadata::Metadata> {
let mut cmd = cargo_metadata::MetadataCommand::new();
if let Some(manifest_path) = manifest_path {
cmd.manifest_path(manifest_path);
}
cmd.features(cargo_metadata::CargoOpt::AllFeatures);
let mut other = Vec::new();
if locked {
other.push("--locked".to_owned());
}
if offline {
other.push("--offline".to_owned());
}
cmd.other_options(other);
let ws = cmd.exec()?;
Ok(ws)
}
fn find_ws_members(ws: &cargo_metadata::Metadata) -> Vec<cargo_metadata::Package> {
let workspace_members: std::collections::HashSet<_> = ws.workspace_members.iter().collect();
ws.packages
.iter()
.filter(|p| workspace_members.contains(&p.id))
.cloned()
.collect()
}