use anyhow::{anyhow, bail, Context, Error, Result};
use std::{
env::current_dir,
fs,
path::{Path, PathBuf},
str::FromStr,
};
use structopt::StructOpt;
use toml_edit::{Document, Item, Value};
enum PatchTarget {
Crates,
Git(String),
Custom(String),
}
enum PointTo {
Path,
GitBranch { repository: String, branch: String },
GitCommit { repository: String, commit: String },
}
impl PointTo {
fn from_cli(
point_to_git: Option<String>,
point_to_git_branch: Option<String>,
point_to_git_commit: Option<String>,
) -> Result<Self> {
if let Some(repository) = point_to_git {
if let Some(branch) = point_to_git_branch {
Ok(Self::GitBranch { repository, branch })
} else if let Some(commit) = point_to_git_commit {
Ok(Self::GitCommit { repository, commit })
} else {
bail!("`--point-to-git-branch` or `--point-to-git-commit` are required when `--point-to-git` is passed!");
}
} else {
Ok(Self::Path)
}
}
}
impl PatchTarget {
fn as_str(&self) -> &str {
match self {
Self::Crates => "crates-io",
Self::Git(url) => url,
Self::Custom(custom) => custom,
}
}
}
#[derive(Debug, StructOpt)]
pub struct Patch {
#[structopt(long)]
path: Option<PathBuf>,
#[structopt(long)]
crates_to_patch: PathBuf,
#[structopt(long)]
point_to_git: Option<String>,
#[structopt(
long,
conflicts_with_all = &[ "point-to-git-commit" ],
requires_all = &[ "point-to-git" ],
)]
point_to_git_branch: Option<String>,
#[structopt(
long,
conflicts_with_all = &[ "point-to-git-branch" ],
requires_all = &[ "point-to-git" ],
)]
point_to_git_commit: Option<String>,
#[structopt(
long,
conflicts_with_all = &[ "crates", "substrate", "cumulus", "polkadot", "beefy" ]
)]
target: Option<String>,
#[structopt(
long,
short = "s",
conflicts_with_all = &[ "target", "polkadot", "cumulus", "crates", "beefy" ]
)]
substrate: bool,
#[structopt(
long,
short = "p",
conflicts_with_all = &[ "target", "substrate", "cumulus", "crates", "beefy" ]
)]
polkadot: bool,
#[structopt(
long,
short = "c",
conflicts_with_all = &[ "target", "substrate", "polkadot", "crates", "beefy" ]
)]
cumulus: bool,
#[structopt(
long,
short = "b",
conflicts_with_all = &[ "target", "substrate", "cumulus", "crates", "polkadot" ]
)]
beefy: bool,
#[structopt(
long,
conflicts_with_all = &[ "target", "substrate", "polkadot", "cumulus", "beefy" ]
)]
crates: bool,
}
impl Patch {
pub fn run(self) -> Result<()> {
let patch_target = self.patch_target()?;
let path = self
.path
.map(|p| {
if !p.exists() {
bail!("Given --path=`{}` does not exist!", p.display());
} else {
Ok(p)
}
})
.unwrap_or_else(|| {
current_dir().with_context(|| anyhow!("Working directory is invalid."))
})?;
let cargo_toml_to_patch = workspace_root_package(&path)?;
let point_to = PointTo::from_cli(
self.point_to_git,
self.point_to_git_branch,
self.point_to_git_commit,
)?;
add_patches_for_packages(
&cargo_toml_to_patch,
&patch_target,
workspace_packages(&self.crates_to_patch)?,
point_to,
)
}
fn patch_target(&self) -> Result<PatchTarget> {
if let Some(ref custom) = self.target {
Ok(PatchTarget::Custom(custom.clone()))
} else if self.substrate {
Ok(PatchTarget::Git(
"https://github.com/paritytech/substrate".into(),
))
} else if self.polkadot {
Ok(PatchTarget::Git(
"https://github.com/paritytech/polkadot".into(),
))
} else if self.cumulus {
Ok(PatchTarget::Git(
"https://github.com/paritytech/cumulus".into(),
))
} else if self.beefy {
Ok(PatchTarget::Git(
"https://github.com/paritytech/parity-bridges-gadget".into(),
))
} else if self.crates {
Ok(PatchTarget::Crates)
} else {
bail!("You need to pass `--target`, `--substrate`, `--polkadot`, `--cumulus`, `--beefy` or `--crates`!");
}
}
}
fn workspace_root_package(path: &Path) -> Result<PathBuf> {
if path.ends_with("Cargo.toml") {
return Ok(path.into());
}
let metadata = cargo_metadata::MetadataCommand::new()
.current_dir(path)
.exec()
.with_context(|| "Failed to get cargo metadata for workspace")?;
Ok(metadata.workspace_root.join("Cargo.toml").into())
}
fn workspace_packages(workspace: &Path) -> Result<impl Iterator<Item = cargo_metadata::Package>> {
let metadata = cargo_metadata::MetadataCommand::new()
.current_dir(workspace)
.exec()
.with_context(|| "Failed to get cargo metadata for workspace.")?;
Ok(metadata
.workspace_members
.clone()
.into_iter()
.map(move |p| metadata[&p].clone()))
}
fn add_patches_for_packages(
cargo_toml: &Path,
patch_target: &PatchTarget,
mut packages: impl Iterator<Item = cargo_metadata::Package>,
point_to: PointTo,
) -> Result<()> {
let content = fs::read_to_string(cargo_toml)
.with_context(|| anyhow!("Failed to read manifest at {}", cargo_toml.display()))?;
let mut doc = Document::from_str(&content).context("Failed to parse Cargo.toml")?;
let patch_table = doc
.as_table_mut()
.entry("patch")
.or_insert(Item::Table(Default::default()))
.as_table_mut()
.ok_or_else(|| anyhow!("Patch table isn't a toml table!"))?;
patch_table.set_implicit(true);
let patch_target_table = patch_table
.entry(patch_target.as_str())
.or_insert(Item::Table(Default::default()))
.as_table_mut()
.ok_or_else(|| anyhow!("Patch target table isn't a toml table!"))?;
packages.try_for_each(|mut p| {
log::info!("Adding patch for `{}`.", p.name);
let patch = patch_target_table
.entry(&p.name)
.or_insert(Item::Value(Value::InlineTable(Default::default())))
.as_inline_table_mut()
.ok_or_else(|| anyhow!("Patch entry for `{}` isn't an inline table!", p.name))?;
if p.manifest_path.ends_with("Cargo.toml") {
p.manifest_path.pop();
}
let path: PathBuf = p.manifest_path.into();
match &point_to {
PointTo::Path => {
*patch.get_or_insert("path", "") =
Value::from(path.display().to_string()).decorated(" ", " ");
}
PointTo::GitBranch { repository, branch } => {
*patch.get_or_insert("git", "") =
Value::from(repository.clone()).decorated(" ", " ");
*patch.get_or_insert("branch", "") =
Value::from(branch.clone()).decorated(" ", " ");
}
PointTo::GitCommit { repository, commit } => {
*patch.get_or_insert("git", "") =
Value::from(repository.clone()).decorated(" ", " ");
*patch.get_or_insert("rev", "") = Value::from(commit.clone()).decorated(" ", " ");
}
}
Ok::<_, Error>(())
})?;
fs::write(cargo_toml, doc.to_string())
.with_context(|| anyhow!("Failed to write manifest to {}", cargo_toml.display()))
}