use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use std::process::Command;
use semver::Version;
use serde::Deserialize;
use crate::Result;
use crate::manifest::Manifest;
#[derive(Debug)]
pub struct Member {
pub name: String,
pub version: Version,
}
#[derive(Debug)]
pub struct Workspace {
root: PathBuf,
members: Vec<Member>,
targets: Vec<Manifest>,
}
impl Workspace {
pub fn discover() -> Result<Self> {
let metadata = cargo_metadata()?;
let root = metadata.workspace_root;
let root_path = root_manifest(&root);
let mut members = Vec::new();
let mut targets: Vec<Manifest> = Vec::new();
let mut written: BTreeSet<PathBuf> = BTreeSet::new();
for package in metadata.packages {
let version = Version::parse(&package.version)
.map_err(|err| format!("{}: {err}", package.name))?;
members.push(Member {
name: package.name,
version,
});
let manifest = Manifest::load(&package.manifest_path)?;
if manifest.has_version() && written.insert(package.manifest_path) {
targets.push(manifest);
}
}
if !written.contains(&root_path) {
let root_manifest = Manifest::load(&root_path)?;
if root_manifest.has_version() {
targets.push(root_manifest);
}
}
if targets.is_empty() {
return Err(format!("{} declares no version to move", root.display()));
}
Ok(Self {
root,
members,
targets,
})
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn members(&self) -> &[Member] {
&self.members
}
pub fn member_names(&self) -> BTreeSet<String> {
self.members
.iter()
.map(|member| member.name.clone())
.collect()
}
pub fn target_count(&self) -> usize {
self.targets.len()
}
pub fn target_mut(&mut self, index: usize) -> &mut Manifest {
&mut self.targets[index]
}
pub fn shared_version(&self) -> Result<Version> {
let mut sorted = self
.members
.iter()
.map(|member| member.version.clone())
.collect::<Vec<_>>();
sorted.sort();
sorted.dedup();
match sorted.len() {
1 => Ok(sorted.pop().expect("one version")),
0 => Err(String::from("this workspace has no crates")),
_ => Err(format!(
"the crates do not share a version ({}); use `semver-sync set <VERSION>`",
sorted
.iter()
.map(Version::to_string)
.collect::<Vec<_>>()
.join(", ")
)),
}
}
pub fn snapshot(&self) -> Result<Vec<(PathBuf, Vec<u8>)>> {
self.targets
.iter()
.map(|target| {
std::fs::read(target.path())
.map(|bytes| (target.path().to_path_buf(), bytes))
.map_err(|err| format!("cannot read {}: {err}", target.path().display()))
})
.collect()
}
pub fn unchanged_since(&self, before: &[(PathBuf, Vec<u8>)]) -> Result<()> {
for (path, bytes) in before {
let after =
std::fs::read(path).map_err(|err| format!("cannot read {path:?}: {err}"))?;
if &after != bytes {
return Err(format!(
"a dry run rewrote {path:?}; the manifests were not left alone"
));
}
}
Ok(())
}
pub fn refresh_lock(&self) -> Result<()> {
let output = Command::new("cargo")
.args([
"metadata",
"--format-version",
"1",
"--manifest-path",
&root_manifest(&self.root).to_string_lossy(),
])
.output()
.map_err(|err| format!("cannot run cargo metadata: {err}"))?;
if !output.status.success() {
return Err(format!(
"the workspace does not resolve after the bump\n{}\n compare the manifests: git diff -- '*Cargo.toml'",
cargo_error(&output.stderr)
));
}
Ok(())
}
}
pub fn repo_root() -> Result<PathBuf> {
Ok(cargo_metadata()?.workspace_root)
}
fn cargo_metadata() -> Result<Metadata> {
let output = Command::new("cargo")
.args(["metadata", "--no-deps", "--format-version", "1"])
.output()
.map_err(|err| format!("cannot run cargo metadata: {err}"))?;
if !output.status.success() {
return Err(cargo_error(&output.stderr));
}
serde_json::from_slice(&output.stdout)
.map_err(|err| format!("cannot read cargo metadata: {err}"))
}
fn root_manifest(root: &Path) -> PathBuf {
root.join("Cargo.toml")
}
fn cargo_error(stderr: &[u8]) -> String {
let text = String::from_utf8_lossy(stderr);
let trimmed = text.trim();
if trimmed.is_empty() {
return String::from("cargo failed without a message");
}
trimmed.to_string()
}
#[derive(Deserialize)]
struct Metadata {
workspace_root: PathBuf,
packages: Vec<Package>,
}
#[derive(Deserialize)]
struct Package {
name: String,
version: String,
manifest_path: PathBuf,
}