use breezyshim::tree::{Tree, WorkingTree};
use std::path::Path;
use std::process::Command;
#[derive(Debug)]
pub enum Error {
BrzError(breezyshim::error::Error),
CratesIoError(crates_io_api::Error),
VersionError(String),
Other(String),
}
impl From<breezyshim::error::Error> for Error {
fn from(e: breezyshim::error::Error) -> Self {
Error::BrzError(e)
}
}
impl From<crates_io_api::Error> for Error {
fn from(e: crates_io_api::Error) -> Self {
Error::CratesIoError(e)
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match &self {
Error::BrzError(e) => write!(f, "TreeError: {}", e),
Error::CratesIoError(e) => write!(f, "CratesIoError: {}", e),
Error::VersionError(e) => write!(f, "VersionError: {}", e),
Error::Other(e) => write!(f, "Other: {}", e),
}
}
}
impl std::error::Error for Error {}
pub fn get_owned_crates(user: &str) -> Result<Vec<url::Url>, Error> {
let client =
crates_io_api::SyncClient::new(crate::USER_AGENT, std::time::Duration::from_millis(1000))
.map_err(|e| Error::Other(format!("Unable to create crates.io client: {}", e)))?;
let user = client.user(user)?;
let query = crates_io_api::CratesQueryBuilder::new().user_id(user.id);
let owned_crates = client.crates(query.build())?;
Ok(owned_crates
.crates
.into_iter()
.filter_map(|c| c.repository)
.map(|r| url::Url::parse(r.as_str()).unwrap())
.collect::<Vec<url::Url>>())
}
pub fn publish(tree: &dyn WorkingTree, subpath: &Path) -> Result<(), Error> {
Command::new("cargo")
.arg("publish")
.current_dir(tree.abspath(subpath)?)
.spawn()
.map_err(|e| Error::Other(format!("Unable to spawn cargo publish: {}", e)))?
.wait()
.map_err(|e| Error::Other(format!("Unable to wait for cargo publish: {}", e)))?;
Ok(())
}
pub fn update_version_in_toml(
parsed_toml: &mut toml_edit::DocumentMut,
new_version: &str,
) -> Result<(), Error> {
if let Some(version) = parsed_toml
.get_mut("package")
.and_then(|p| p.get_mut("version"))
{
if version.get("workspace").is_none() {
*version = toml_edit::value(new_version);
return Ok(());
}
} else if parsed_toml
.get("workspace")
.and_then(|w| w.get("package"))
.and_then(|p| p.get("version"))
.is_some()
{
log::info!("No package.version found, but workspace.package.version exists");
} else {
return Err(Error::Other(
"Unable to find package in Cargo.toml".to_string(),
));
}
if let Some(version) = parsed_toml
.get_mut("workspace")
.and_then(|w| w.get_mut("package"))
.and_then(|p| p.get_mut("version"))
{
*version = toml_edit::value(new_version);
} else {
return Err(Error::Other(
"Unable to find workspace in Cargo.toml".to_string(),
));
}
Ok(())
}
pub fn update_version(tree: &dyn WorkingTree, new_version: &str) -> Result<(), Error> {
let cargo_toml_contents = tree.get_file_text(Path::new("Cargo.toml"))?;
let mut parsed_toml: toml_edit::DocumentMut =
String::from_utf8_lossy(cargo_toml_contents.as_slice())
.parse()
.map_err(|e| Error::Other(format!("Unable to parse Cargo.toml: {}", e)))?;
update_version_in_toml(&mut parsed_toml, new_version)?;
let updated_cargo_toml = parsed_toml.to_string();
tree.put_file_bytes_non_atomic(Path::new("Cargo.toml"), updated_cargo_toml.as_bytes())?;
if tree.has_filename(Path::new("Cargo.lock")) {
Command::new("cargo")
.arg("update")
.arg("-w")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::inherit())
.current_dir(tree.basedir())
.spawn()
.map_err(|e| Error::Other(format!("Unable to spawn cargo update: {}", e)))?
.wait()
.map_err(|e| Error::Other(format!("Unable to wait for cargo update: {}", e)))?;
}
Ok(())
}
pub fn find_version_in_toml(cargo_toml_contents: &str) -> Result<crate::version::Version, Error> {
let parsed_toml: toml_edit::DocumentMut = cargo_toml_contents
.parse()
.map_err(|e| Error::Other(format!("Unable to parse Cargo.toml: {}", e)))?;
let version = parsed_toml
.as_table()
.get("package")
.and_then(|p| p.as_table())
.and_then(|t| t.get("version"))
.ok_or_else(|| Error::Other("Unable to find version in Cargo.toml".to_string()))?;
let version_str = if let Some(v) = version.as_str() {
v
} else if version
.get("workspace")
.and_then(|b| b.as_bool())
.unwrap_or(false)
{
parsed_toml
.get("workspace")
.and_then(|t| t.get("package"))
.and_then(|t| t.get("version"))
.and_then(|v| v.as_str())
.ok_or_else(|| {
Error::Other("Unable to find workspace.package version in Cargo.toml".to_string())
})?
} else {
return Err(Error::Other(
"Unable to parse version in Cargo.toml".to_string(),
));
};
version_str
.parse()
.map_err(|e| Error::VersionError(format!("Unable to parse version: {}", e)))
}
pub fn find_version(tree: &dyn Tree) -> Result<crate::version::Version, Error> {
let cargo_toml_contents = tree.get_file_text(Path::new("Cargo.toml"))?;
find_version_in_toml(
std::str::from_utf8(cargo_toml_contents.as_slice())
.map_err(|e| Error::Other(format!("Unable to parse Cargo.toml as UTF-8: {}", e)))?,
)
}
#[cfg(test)]
mod tests {
#[test]
fn test_find_version_in_toml() {
let text = "[package]\nversion = \"0.1.0\"\n";
let version = super::find_version_in_toml(text).unwrap();
assert_eq!(version, "0.1.0".parse().unwrap());
let text = "[package]\nversion = { workspace = true }\n[workspace]\npackage = { version = \"0.2.0\" }\n";
let version = super::find_version_in_toml(text).unwrap();
assert_eq!(version, "0.2.0".parse().unwrap());
}
#[test]
fn test_find_version_in_toml_error() {
let text = "[package]\nversion = 0.1.0\n";
let version = super::find_version_in_toml(text);
assert!(version.is_err());
let text = "[package]\nversion = { workspace = true }\n[workspace]\npackage = { version = 0.2.0 }\n";
let version = super::find_version_in_toml(text);
assert!(version.is_err());
}
#[test]
fn test_update_version_in_toml() {
let text = "[package]\nversion = \"0.1.0\"\n";
let mut parsed_toml: toml_edit::DocumentMut = text.parse().unwrap();
super::update_version_in_toml(&mut parsed_toml, "0.2.0").unwrap();
assert_eq!(parsed_toml.to_string(), "[package]\nversion = \"0.2.0\"\n");
let text = "[package]\nversion = \"0.1.0\"\n[dependencies.test]\nversion = \"0.3.0\"\n";
let mut parsed_toml: toml_edit::DocumentMut = text.parse().unwrap();
super::update_version_in_toml(&mut parsed_toml, "0.2.0").unwrap();
assert_eq!(
parsed_toml.to_string(),
"[package]\nversion = \"0.2.0\"\n[dependencies.test]\nversion = \"0.3.0\"\n"
);
let text = "[package]\nversion = { workspace = true }\n[workspace]\npackage = { version = \"0.1.0\" }\n";
let mut parsed_toml: toml_edit::DocumentMut = text.parse().unwrap();
super::update_version_in_toml(&mut parsed_toml, "0.2.0").unwrap();
assert_eq!(
parsed_toml.to_string(),
"[package]\nversion = { workspace = true }\n[workspace]\npackage = { version = \"0.2.0\" }\n"
);
let text = "[workspace]\npackage = { version = \"0.1.0\" }\n";
let mut parsed_toml: toml_edit::DocumentMut = text.parse().unwrap();
super::update_version_in_toml(&mut parsed_toml, "0.2.0").unwrap();
assert_eq!(
parsed_toml.to_string(),
"[workspace]\npackage = { version = \"0.2.0\" }\n"
);
}
#[test]
fn test_update_version_in_toml_invalid() {
let text = "";
let mut parsed_toml: toml_edit::DocumentMut = text.parse().unwrap();
let result = super::update_version_in_toml(&mut parsed_toml, "0.2.0");
assert!(result.is_err());
}
}