aboutsummaryrefslogtreecommitdiff
path: root/development/tools/cargo-workspace2/src
diff options
context:
space:
mode:
Diffstat (limited to 'development/tools/cargo-workspace2/src')
-rw-r--r--development/tools/cargo-workspace2/src/bin/cargo-ws2.rs42
-rw-r--r--development/tools/cargo-workspace2/src/cargo/deps.rs75
-rw-r--r--development/tools/cargo-workspace2/src/cargo/error.rs42
-rw-r--r--development/tools/cargo-workspace2/src/cargo/gen.rs34
-rw-r--r--development/tools/cargo-workspace2/src/cargo/mod.rs25
-rw-r--r--development/tools/cargo-workspace2/src/cargo/parser.rs69
-rw-r--r--development/tools/cargo-workspace2/src/cli.rs91
-rw-r--r--development/tools/cargo-workspace2/src/lib.rs64
-rw-r--r--development/tools/cargo-workspace2/src/models/_crate.rs112
-rw-r--r--development/tools/cargo-workspace2/src/models/cargo.rs132
-rw-r--r--development/tools/cargo-workspace2/src/models/graph.rs78
-rw-r--r--development/tools/cargo-workspace2/src/models/mod.rs50
-rw-r--r--development/tools/cargo-workspace2/src/models/publish.rs20
-rw-r--r--development/tools/cargo-workspace2/src/ops/error.rs28
-rw-r--r--development/tools/cargo-workspace2/src/ops/executor.rs33
-rw-r--r--development/tools/cargo-workspace2/src/ops/mod.rs106
-rw-r--r--development/tools/cargo-workspace2/src/ops/parser.rs112
-rw-r--r--development/tools/cargo-workspace2/src/ops/publish/exec.rs117
-rw-r--r--development/tools/cargo-workspace2/src/ops/publish/mod.rs132
-rw-r--r--development/tools/cargo-workspace2/src/query/executor.rs83
-rw-r--r--development/tools/cargo-workspace2/src/query/mod.rs280
21 files changed, 1725 insertions, 0 deletions
diff --git a/development/tools/cargo-workspace2/src/bin/cargo-ws2.rs b/development/tools/cargo-workspace2/src/bin/cargo-ws2.rs
new file mode 100644
index 000000000000..8de5bc60f6a4
--- /dev/null
+++ b/development/tools/cargo-workspace2/src/bin/cargo-ws2.rs
@@ -0,0 +1,42 @@
+// extern crate cargo_ws_release;
+// #[macro_use]
+// extern crate clap;
+// extern crate toml;
+// extern crate toml_edit;
+
+// use cargo_ws_release::data_models::level::*;
+// use cargo_ws_release::do_batch_release;
+// use clap::{App, Arg};
+// use std::fs::File;
+// use std::process;
+
+use cargo_workspace2 as ws2;
+
+use std::env::{args, current_dir};
+use ws2::cli::{self, CmdSet};
+use ws2::models::{CargoWorkspace, CrateId, Workspace};
+use ws2::{ops::Op, query::Query};
+
+fn main() {
+ let CmdSet { debug, line } = cli::parse_env_args();
+ let (q, rest) = Query::parse(line.into_iter());
+
+ let path = current_dir().unwrap();
+ let ws = Workspace::process(match CargoWorkspace::open(path) {
+ Ok(c) => c,
+ Err(e) => {
+ eprintln!("An error occured: {}", e);
+ std::process::exit(1);
+ }
+ });
+
+ let set = ws.query(q);
+ let op = Op::parse(rest);
+
+ mutate(ws, op, set);
+}
+
+/// Consume a workspace to mutate it
+fn mutate(mut ws: Workspace, op: Op, set: Vec<CrateId>) {
+ ws.execute(op, set);
+}
diff --git a/development/tools/cargo-workspace2/src/cargo/deps.rs b/development/tools/cargo-workspace2/src/cargo/deps.rs
new file mode 100644
index 000000000000..d40170ccd841
--- /dev/null
+++ b/development/tools/cargo-workspace2/src/cargo/deps.rs
@@ -0,0 +1,75 @@
+use super::v_to_s;
+use toml_edit::InlineTable;
+
+/// An intra-workspace dependency
+///
+/// In a Cargo.toml file these are expressed as paths, and sometimes
+/// also as versions.
+///
+/// ```toml
+/// [dependencies]
+/// my-other-crate = { version = "0.1.0", path = "../other-crate" }
+/// ```
+#[derive(Debug, Clone)]
+pub struct Dependency {
+ pub name: String,
+ pub alias: Option<String>,
+ pub version: Option<String>,
+ pub path: Option<String>,
+}
+
+impl Dependency {
+ pub(crate) fn parse(n: String, t: &InlineTable) -> Option<Self> {
+ let v = t.get("version").map(|s| v_to_s(s));
+ let p = t.get("path").map(|s| v_to_s(s));
+
+ // If a `package` key is present, set it as the name, and set
+ // the `n` as the alias. When we look for keys later, the
+ // alias has precedence over the actual name, but this way
+ // `name` is always the actual crate name which is important
+ // for dependency resolution.
+ let (alias, name) = match t
+ .get("package")
+ .map(|s| v_to_s(s).replace("\"", "").trim().to_string())
+ {
+ Some(alias) => (Some(n), alias),
+ None => (None, n),
+ };
+
+ match (v, p) {
+ (version @ Some(_), path @ Some(_)) => Some(Self {
+ name,
+ alias,
+ version,
+ path,
+ }),
+ (version @ Some(_), None) => Some(Self {
+ name,
+ alias,
+ version,
+ path: None,
+ }),
+ (None, path @ Some(_)) => Some(Self {
+ name,
+ alias,
+ version: None,
+ path,
+ }),
+ (None, None) => None,
+ }
+ }
+
+ /// Check if the dependency has a provided version
+ pub fn has_version(&self) -> bool {
+ self.version.is_some()
+ }
+
+ /// Check if the dependency has a provided path
+ pub fn has_path(&self) -> bool {
+ self.path.is_some()
+ }
+
+ pub fn alias(&self) -> Option<String> {
+ self.alias.clone()
+ }
+}
diff --git a/development/tools/cargo-workspace2/src/cargo/error.rs b/development/tools/cargo-workspace2/src/cargo/error.rs
new file mode 100644
index 000000000000..cf4b7e3d1671
--- /dev/null
+++ b/development/tools/cargo-workspace2/src/cargo/error.rs
@@ -0,0 +1,42 @@
+use std::{fmt, io};
+use toml_edit::TomlError;
+
+/// Errors occured while interacting with Cargo.toml files
+#[derive(Debug)]
+pub enum CargoError {
+ /// Failed to read or write a file
+ Io,
+ /// Error parsing Cargo.toml file
+ Parsing,
+ /// Provided Cargo.toml was no workspace
+ NoWorkspace,
+ /// Provided Cargo.toml had no dependencies
+ NoDependencies,
+}
+
+impl fmt::Display for CargoError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "{}",
+ match self {
+ Self::Io => "General I/O error",
+ Self::Parsing => "Parsing error",
+ Self::NoWorkspace => "Selected crate root is not a workspace!",
+ Self::NoDependencies => "No dependencies found!",
+ }
+ )
+ }
+}
+
+impl From<io::Error> for CargoError {
+ fn from(_: io::Error) -> Self {
+ Self::Io
+ }
+}
+
+impl From<TomlError> for CargoError {
+ fn from(_: TomlError) -> Self {
+ Self::Parsing
+ }
+}
diff --git a/development/tools/cargo-workspace2/src/cargo/gen.rs b/development/tools/cargo-workspace2/src/cargo/gen.rs
new file mode 100644
index 000000000000..4ba891e15835
--- /dev/null
+++ b/development/tools/cargo-workspace2/src/cargo/gen.rs
@@ -0,0 +1,34 @@
+//! Generate toml data
+
+use super::{CargoError, Result};
+use std::{fs::File, io::Write, path::PathBuf};
+use toml_edit::{value, Document, Item, Value};
+
+/// Sync a document back into it's Cargo.toml
+pub(crate) fn sync(doc: &mut Document, path: PathBuf) -> Result<()> {
+ if !path.exists() {
+ return Err(CargoError::Io);
+ }
+
+ let mut f = File::create(path)?;
+ f.write_all(doc.to_string().as_bytes())?;
+ Ok(())
+}
+
+/// Takes a mutable document, dependency alias or name, and version
+pub(crate) fn update_dependency(doc: &mut Document, dep: &String, ver: &String) {
+ match doc.as_table_mut().entry("dependencies") {
+ Item::Table(ref mut t) => match t.entry(dep.as_str()) {
+ Item::Value(Value::InlineTable(ref mut t)) => {
+ if let Some(v) = t.get_mut("version") {
+ *v = Value::from(ver.clone());
+ }
+ return;
+ }
+ _ => {}
+ },
+ _ => {}
+ }
+
+ // eprintln!("Invalid dependency format!");
+}
diff --git a/development/tools/cargo-workspace2/src/cargo/mod.rs b/development/tools/cargo-workspace2/src/cargo/mod.rs
new file mode 100644
index 000000000000..deb54563d6ba
--- /dev/null
+++ b/development/tools/cargo-workspace2/src/cargo/mod.rs
@@ -0,0 +1,25 @@
+//! A set of models directly related to `Cargo.toml` files
+
+mod deps;
+pub use deps::Dependency;
+
+mod error;
+pub use error::CargoError;
+
+mod parser;
+pub(crate) use parser::{get_members, parse_dependencies, parse_root_toml, parse_toml};
+
+mod gen;
+pub(crate) use gen::{sync, update_dependency};
+
+pub(crate) type Result<T> = std::result::Result<T, CargoError>;
+
+use toml_edit::Value;
+
+/// Turn a toml Value into a String, panic if it fails
+pub(self) fn v_to_s(v: &Value) -> String {
+ match v {
+ Value::String(s) => s.to_string(),
+ _ => unreachable!(),
+ }
+}
diff --git a/development/tools/cargo-workspace2/src/cargo/parser.rs b/development/tools/cargo-workspace2/src/cargo/parser.rs
new file mode 100644
index 000000000000..c3e67e2785fb
--- /dev/null
+++ b/development/tools/cargo-workspace2/src/cargo/parser.rs
@@ -0,0 +1,69 @@
+use crate::cargo::{CargoError, Dependency, Result};
+use std::{fs::File, io::Read, path::PathBuf};
+use toml_edit::{Document, Item, Value};
+
+/// Parse the root entry of a cargo workspace into a document
+pub(crate) fn parse_root_toml(p: PathBuf) -> Result<Document> {
+ let mut buf = String::new();
+ File::open(p)?.read_to_string(&mut buf)?;
+ Ok(buf.parse::<Document>()?)
+}
+
+/// For a root-config, get the paths of member crates
+pub(crate) fn get_members(doc: &Document) -> Result<Vec<String>> {
+ match &doc["workspace"]["members"] {
+ Item::Value(Value::Array(arr)) => Ok(arr
+ .iter()
+ .filter_map(|val| match val {
+ Value::String(name) => Some(format!("{}", name)),
+ val => {
+ eprintln!("Ignoring value `{:?}` in List of strings", val);
+ None
+ }
+ })
+ .map(|s| s.replace("\"", "").trim().into())
+ .map(|s: String| {
+ if s.trim().starts_with("#") {
+ s.split("\n").map(ToString::to_string).collect()
+ } else {
+ vec![s]
+ }
+ })
+ // Holy shit this crate is useless! Why would it not just
+ // strip the commented lines, instead of doing some
+ // bullshit like this...
+ .fold(vec![], |mut vec, ovec| {
+ ovec.into_iter().for_each(|i| {
+ if !i.trim().starts_with("#") && i != "" {
+ vec.push(i.trim().to_string());
+ }
+ });
+ vec
+ })),
+ _ => Err(CargoError::NoWorkspace),
+ }
+}
+
+/// Parse a member crate Cargo.toml file
+pub(crate) fn parse_toml(p: PathBuf) -> Result<Document> {
+ let mut buf = String::new();
+ File::open(p)?.read_to_string(&mut buf)?;
+ Ok(buf.parse::<Document>()?)
+}
+
+/// Parse a member crate set of dependencies
+///
+/// When a crate is not a table, it can't be a workspace member
+/// dependency, so is skipped.
+pub(crate) fn parse_dependencies(doc: &Document) -> Vec<Dependency> {
+ match &doc["dependencies"] {
+ Item::Table(ref t) => t
+ .iter()
+ .filter_map(|(n, v)| match v {
+ Item::Value(Value::InlineTable(ref t)) => Dependency::parse(n.to_string(), t),
+ _ => None,
+ })
+ .collect(),
+ _ => vec![],
+ }
+}
diff --git a/development/tools/cargo-workspace2/src/cli.rs b/development/tools/cargo-workspace2/src/cli.rs
new file mode 100644
index 000000000000..afea9e224176
--- /dev/null
+++ b/development/tools/cargo-workspace2/src/cli.rs
@@ -0,0 +1,91 @@
+//! Helpers and utilities to parse the CLI input
+
+use std::env;
+
+pub struct CmdSet {
+ pub debug: bool,
+ pub line: Vec<String>,
+}
+
+fn get_nth(idx: usize) -> Option<String> {
+ env::args().nth(idx).as_ref().map(|s| s.to_owned()).clone()
+}
+
+/// Call this instead of env::args() - it handles !commands too
+pub fn parse_env_args() -> CmdSet {
+ let mut line: Vec<_> = env::args().collect();
+ let mut debug = false;
+
+ if line.len() == 1 {
+ render_help(2);
+ }
+
+ find_bang_commands().into_iter().for_each(|(idx, bang)| {
+ let maybe_next = line.iter().nth(idx + 1).as_ref().map(|s| s.to_owned());
+
+ match bang.trim() {
+ "!help" => match maybe_next {
+ None => render_help(0),
+ Some(cmd) => crate::ops::render_help(cmd.to_string()),
+ },
+ "!version" => render_version(),
+ "!debug" => {
+ debug = true;
+ line.remove(idx);
+ }
+ bang => {
+ if debug {
+ eprintln!("Unrecognised bang command: {}", bang);
+ }
+
+ line.remove(idx);
+ }
+ }
+ });
+
+ CmdSet { line, debug }
+}
+
+/// Get env::args() and look for any string with a `!` in front of it.
+/// If it's not in the set of known bang commands, ignore it.
+fn find_bang_commands() -> Vec<(usize, String)> {
+ env::args()
+ .enumerate()
+ .filter_map(|(idx, s)| {
+ if s.starts_with("!") {
+ Some((idx, s))
+ } else {
+ None
+ }
+ })
+ .collect()
+}
+
+pub(crate) fn render_help(code: i32) -> ! {
+ eprintln!("cargo-ws v{}", env!("CARGO_PKG_VERSION"));
+ eprintln!("An expression language and command executor for cargo workspaces.");
+ eprintln!("Usage: cargo ws2 <QUERY LANG> <COMMAND> [COMMAND ARGS] [!debug]");
+ eprintln!(" cargo ws2 [!version | !debug]");
+ eprintln!(" cargo ws2 !help [COMMAND]");
+ eprintln!("");
+
+ crate::ops::list_commands();
+ eprintln!("");
+
+ eprintln!("Query language examples:\n");
+ eprintln!(" - [ foo bar ]: select crates foo and bar");
+ eprintln!(" - {{ foo < }}: select crates that depend on foo");
+ eprintln!(" - {{ foo < bar &< }}: select crates that depend on foo AND bar");
+ eprintln!(" - {{ foo < bar |< }}: select crates that depend on foo OR bar");
+ eprintln!("\nIf you have any questions, or find bugs, please e-mail me: kookie@spacekookie.de");
+ std::process::exit(code)
+}
+
+fn render_version() -> ! {
+ eprintln!("cargo-ws v{}", env!("CARGO_PKG_VERSION"));
+ eprintln!(
+ "Build with: {}",
+ include_str!(concat!(env!("OUT_DIR"), "/rustc.version"))
+ );
+ std::process::exit(0)
+}
diff --git a/development/tools/cargo-workspace2/src/lib.rs b/development/tools/cargo-workspace2/src/lib.rs
new file mode 100644
index 000000000000..689c3601cd55
--- /dev/null
+++ b/development/tools/cargo-workspace2/src/lib.rs
@@ -0,0 +1,64 @@
+//! cargo-workspace2 is a library to help manage cargo workspaces
+//!
+//! Out of the box the `cargo` workspace experience leaves a lot to be
+//! desired. Managing a repo with many crates in it can get out of
+//! hand quickly. Moreover, other tools that try to solve these
+//! issues often pick _one_ particular usecase of cargo workspaces,
+//! and enforce very strict rules on how to use them.
+//!
+//! This library aims to solve some of the issues of dealing with
+//! workspaces in a way that doesn't enforce a usage mode for the
+//! user.
+//!
+//! This package also publishes a binary (cargo ws2), which is
+//! recommended for most users. In case the binary handles a use-case
+//! you have in a way that you don't like, this library aims to
+//! provide a fallback so that you don't have to re-implement
+//! everything from scratch.
+//!
+//! ## Using this library
+//!
+//! Parsing happens in stages. First you need to use the
+//! [`cargo`](./cargo/index.html) module to parse the actual
+//! `Cargo.toml` files. After that you can use the cargo models in
+//! [`models`](models/index.html) to further process dependencies, and
+//! create a [`DepGraph`](models/struct.DepGraph.html) to resolve queries and make changes.
+
+pub mod cargo;
+pub mod models;
+pub mod ops;
+pub mod query;
+
+#[doc(hidden)]
+pub mod cli;
+
+// extern crate toml;
+// extern crate toml_edit;
+
+// pub use data_models::graph;
+// use data_models::level::Level;
+// use graph::DepGraph;
+// use std::fs::File;
+// pub use utilities::cargo_utils;
+// pub use utilities::utils;
+
+// pub mod data_models;
+// pub mod utilities;
+
+// pub fn do_batch_release(f: File, lvl: &Level) -> DepGraph {
+// let members = cargo_utils::get_members(f);
+// let configs = cargo_utils::batch_load_configs(&members);
+
+// let v = configs
+// .iter()
+// .map(|c| cargo_utils::parse_config(c, &members))
+// .fold(DepGraph::new(), |mut graph, (name, deps)| {
+// graph.add_node(name.clone());
+
+// deps.iter()
+// .fold(graph, |graph, dep| graph.add_dependency(&name, dep))
+// });
+
+// println!("{:#?}", v);
+// v
+// }
diff --git a/development/tools/cargo-workspace2/src/models/_crate.rs b/development/tools/cargo-workspace2/src/models/_crate.rs
new file mode 100644
index 000000000000..68d2baad2bad
--- /dev/null
+++ b/development/tools/cargo-workspace2/src/models/_crate.rs
@@ -0,0 +1,112 @@
+use crate::models::{CargoCrate, CrateId, DepGraph};
+use std::sync::atomic::{AtomicUsize, Ordering};
+use std::{
+ cmp::{self, Eq, Ord, PartialEq, PartialOrd},
+ collections::BTreeSet,
+ path::PathBuf,
+};
+
+static ID_CTR: AtomicUsize = AtomicUsize::new(0);
+
+/// A crate in a cargo workspace
+///
+/// Has a name, path (stored as the offset of the root), and set of
+/// dependencies inside the workspace. To get the dependents of this
+/// crate, query the dependency graph with the set of other crate IDs.
+#[derive(Clone, Debug)]
+pub struct Crate {
+ /// Numeric Id of this crate
+ pub id: CrateId,
+ /// Package name, not the folder name
+ pub name: String,
+ /// Path offset of the workspace root
+ pub cc: CargoCrate,
+ /// List of dependencies this crate has inside this workspace
+ pub dependencies: BTreeSet<CrateId>,
+}
+
+impl PartialEq for Crate {
+ fn eq(&self, other: &Self) -> bool {
+ self.id == other.id
+ }
+}
+
+impl Eq for Crate {}
+
+impl Ord for Crate {
+ fn cmp(&self, other: &Self) -> cmp::Ordering {
+ self.id.cmp(&other.id)
+ }
+}
+
+impl PartialOrd for Crate {
+ fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+/// Increment the monotonicly increasing Id
+fn incr_id() -> usize {
+ ID_CTR.fetch_add(1, Ordering::Relaxed)
+}
+
+impl Crate {
+ pub fn new(cc: CargoCrate) -> Self {
+ Self {
+ id: incr_id(),
+ name: cc.name(),
+ cc,
+ dependencies: BTreeSet::default(),
+ }
+ }
+
+ /// Call this function once all crates have been loaded into scope
+ pub fn process(&mut self, g: &DepGraph) {
+ let deps: Vec<_> = self
+ .cc
+ .dependencies
+ .iter()
+ .filter_map(|d| g.find_crate(&d.name))
+ .collect();
+
+ deps.into_iter().for_each(|cid| self.add_dependency(cid));
+ }
+
+ /// Get the crate name
+ pub fn name(&self) -> &String {
+ &self.name
+ }
+
+ /// Get the crate path
+ pub fn path(&self) -> &PathBuf {
+ &self.cc.path
+ }
+
+ /// Get the current version
+ pub fn version(&self) -> String {
+ self.cc.version()
+ }
+
+ /// Add a dependency of this crate
+ pub fn add_dependency(&mut self, id: CrateId) {
+ self.dependencies.insert(id);
+ }
+
+ /// Check if this crate has a particular dependency
+ pub fn has_dependency(&self, id: CrateId) -> bool {
+ self.dependencies.contains(&id)
+ }
+
+ pub fn change_dependency(&mut self, dep: &String, new_ver: &String) {
+ self.cc.change_dep(dep, new_ver);
+ }
+
+ /// Publish a new version of this crate
+ pub fn publish(&mut self, new_version: String) {
+ self.cc.set_version(new_version);
+ }
+
+ pub fn sync(&mut self) {
+ self.cc.sync();
+ }
+}
diff --git a/development/tools/cargo-workspace2/src/models/cargo.rs b/development/tools/cargo-workspace2/src/models/cargo.rs
new file mode 100644
index 000000000000..b020e82e418d
--- /dev/null
+++ b/development/tools/cargo-workspace2/src/models/cargo.rs
@@ -0,0 +1,132 @@
+use crate::cargo::{self, Dependency, Result};
+use std::{fmt, path::PathBuf};
+use toml_edit::{value, Document, Item, Value};
+
+/// Initial representation of a crate, before being parsed
+#[derive(Clone)]
+pub struct CargoCrate {
+ pub doc: Document,
+ pub path: PathBuf,
+ pub dependencies: Vec<Dependency>,
+}
+
+impl fmt::Debug for CargoCrate {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{}", self.path.as_path().display())
+ }
+}
+
+impl CargoCrate {
+ /// Get the crate name from the inner document
+ pub fn name(&self) -> String {
+ match &self.doc["package"]["name"] {
+ Item::Value(Value::String(ref name)) => {
+ name.to_string().replace("\"", "").as_str().trim().into()
+ }
+ _ => panic!(format!("Invalid Cargo.toml: {:?}", self.path)),
+ }
+ }
+
+ /// Get the current version
+ pub fn version(&self) -> String {
+ match &self.doc["package"]["version"] {
+ Item::Value(Value::String(ref name)) => {
+ name.to_string().replace("\"", "").as_str().trim().into()
+ }
+ _ => panic!(format!("Invalid Cargo.toml: {:?}", self.path)),
+ }
+ }
+
+ /// Find a cargo dependency by name
+ pub fn dep_by_name(&self, name: &String) -> &Dependency {
+ self.dependencies
+ .iter()
+ .find(|c| &c.name == name)
+ .as_ref()
+ .unwrap()
+ }
+
+ pub fn change_dep(&mut self, dep: &String, ver: &String) {
+ let dep = self
+ .dep_by_name(dep)
+ .alias()
+ .unwrap_or(dep.to_string())
+ .clone();
+ cargo::update_dependency(&mut self.doc, &dep, ver);
+ }
+
+ pub fn all_deps_mut(&mut self) -> Vec<&mut Dependency> {
+ self.dependencies.iter_mut().collect()
+ }
+
+ /// Check if this crate depends on a specific version of another
+ pub fn has_version(&self, name: &String) -> bool {
+ self.dep_by_name(name).has_version()
+ }
+
+ /// Check if this crate depends on a specific path of another
+ pub fn has_path(&self, name: &String) -> bool {
+ self.dep_by_name(name).has_version()
+ }
+
+ /// Set a new version for this crate
+ pub fn set_version(&mut self, version: String) {
+ self.doc["package"]["version"] = value(version);
+ }
+
+ /// Sync any changes made to the document to disk
+ pub fn sync(&mut self) {
+ cargo::sync(&mut self.doc, self.path.join("Cargo.toml")).unwrap();
+ }
+}
+
+/// Initial representation of the workspate, before getting parsed
+pub struct CargoWorkspace {
+ pub root: PathBuf,
+ pub crates: Vec<CargoCrate>,
+}
+
+impl CargoWorkspace {
+ /// Open a workspace and parse dependency graph
+ ///
+ /// Point this to the root of the workspace, do the root
+ /// `Cargo.toml` file.
+ pub fn open(p: impl Into<PathBuf>) -> Result<Self> {
+ let path = p.into();
+
+ let root_cfg = cargo::parse_root_toml(path.join("Cargo.toml"))?;
+ let members = cargo::get_members(&root_cfg)?;
+
+ let m_cfg: Vec<_> = members
+ .into_iter()
+ .filter_map(
+ |name| match cargo::parse_toml(path.join(&name).join("Cargo.toml")) {
+ Ok(doc) => Some((
+ PathBuf::new().join(name),
+ cargo::parse_dependencies(&doc),
+ doc,
+ )),
+ Err(e) => {
+ eprintln!(
+ "Error occured while parsing member `{}`/`Cargo.toml`: {:?}",
+ name, e
+ );
+ None
+ }
+ },
+ )
+ .collect();
+
+ Ok(Self {
+ root: path,
+ crates: m_cfg
+ .into_iter()
+ .map(|(path, dependencies, doc)| CargoCrate {
+ path,
+ dependencies,
+ doc,
+ })
+ .collect(),
+ })
+ }
+}
diff --git a/development/tools/cargo-workspace2/src/models/graph.rs b/development/tools/cargo-workspace2/src/models/graph.rs
new file mode 100644
index 000000000000..867c463fb1e3
--- /dev/null
+++ b/development/tools/cargo-workspace2/src/models/graph.rs
@@ -0,0 +1,78 @@
+use crate::models::{CargoCrate, Crate, CrateId};
+use std::collections::{BTreeMap, BTreeSet};
+use std::path::PathBuf;
+
+/// Dependency graph in a workspace
+pub struct DepGraph {
+ /// Mapping of crates in the workspace
+ members: BTreeMap<CrateId, Crate>,
+ /// Map of crates and the members that depend on them
+ dependents: BTreeMap<CrateId, BTreeSet<CrateId>>,
+}
+
+impl DepGraph {
+ /// Create a new, empty dependency graph
+ pub fn new() -> Self {
+ Self {
+ members: Default::default(),
+ dependents: Default::default(),
+ }
+ }
+
+ pub fn add_crate(&mut self, cc: CargoCrate) {
+ let cc = Crate::new(cc);
+ self.members.insert(cc.id, cc);
+ }
+
+ /// Cache the dependents graph for all crates
+ pub fn finalise(&mut self) {
+ let mut members = self.members.clone();
+ members.iter_mut().for_each(|(_, c)| c.process(&self));
+
+ members.iter().for_each(|(id, _crate)| {
+ _crate.dependencies.iter().for_each(|dep_id| {
+ self.dependents.entry(*dep_id).or_default().insert(*id);
+ });
+ });
+ let _ = std::mem::replace(&mut self.members, members);
+ }
+
+ /// Get a crate by ID
+ pub fn get_crate(&self, id: CrateId) -> &Crate {
+ self.members.get(&id).as_ref().unwrap()
+ }
+
+ /// Get mutable access to a crate by ID
+ pub fn mut_crate(&mut self, id: CrateId) -> &mut Crate {
+ self.members.get_mut(&id).unwrap()
+ }
+
+ /// Find a crate via it's name
+ pub fn find_crate(&self, name: &String) -> Option<CrateId> {
+ self.members
+ .iter()
+ .find(|(_, c)| c.name() == name)
+ .map(|(id, _)| *id)
+ }
+
+ /// Find a crate by it's path-offset in the workspace
+ pub fn find_crate_by_path(&self, name: &String) -> Option<CrateId> {
+ self.members
+ .iter()
+ .find(|(_, c)| c.path() == &PathBuf::new().join(name))
+ .map(|(id, _)| *id)
+ }
+
+ /// Get a crate's dependents
+ pub fn get_dependents(&self, id: CrateId) -> Vec<CrateId> {
+ self.dependents
+ .get(&id)
+ .as_ref()
+ .map(|set| set.iter().cloned().collect())
+ .unwrap_or(vec![])
+ }
+
+ pub fn get_all(&self) -> Vec<&Crate> {
+ self.members.iter().map(|(_, c)| c).collect()
+ }
+}
diff --git a/development/tools/cargo-workspace2/src/models/mod.rs b/development/tools/cargo-workspace2/src/models/mod.rs
new file mode 100644
index 000000000000..759b2703f9f0
--- /dev/null
+++ b/development/tools/cargo-workspace2/src/models/mod.rs
@@ -0,0 +1,50 @@
+//! Collection of cargo workspace data models.
+//!
+//! To start parsing types, construct a `CargoWorkspace`, which you
+//! can then modify with commands found in [`ops`](../ops/index.html).
+
+mod cargo;
+pub use cargo::{CargoCrate, CargoWorkspace};
+
+mod _crate;
+pub use _crate::Crate;
+
+mod publish;
+pub use publish::{MutationSet, PubMutation};
+
+mod graph;
+pub use graph::DepGraph;
+
+pub type CrateId = usize;
+
+use crate::{ops::Op, query::Query};
+use std::path::PathBuf;
+
+/// A fully parsed workspace
+pub struct Workspace {
+ pub root: PathBuf,
+ dgraph: DepGraph,
+}
+
+impl Workspace {
+ /// Create a parsed workspace by passing in the stage1 parse data
+ pub fn process(cws: CargoWorkspace) -> Self {
+ let CargoWorkspace { root, crates } = cws;
+
+ let mut dgraph = DepGraph::new();
+ crates.into_iter().for_each(|cc| dgraph.add_crate(cc));
+ dgraph.finalise();
+
+ Self { root, dgraph }
+ }
+
+ /// Execute a query on this workspace to find crate IDs
+ pub fn query(&self, q: Query) -> Vec<CrateId> {
+ q.execute(&self.dgraph)
+ }
+
+ /// Execute an operation on a set of crates this in workspace
+ pub fn execute(&mut self, op: Op, target: Vec<CrateId>) {
+ op.execute(target, self.root.clone(), &mut self.dgraph)
+ }
+}
diff --git a/development/tools/cargo-workspace2/src/models/publish.rs b/development/tools/cargo-workspace2/src/models/publish.rs
new file mode 100644
index 000000000000..11984017d49f
--- /dev/null
+++ b/development/tools/cargo-workspace2/src/models/publish.rs
@@ -0,0 +1,20 @@
+use crate::models::{Crate, CrateId};
+
+/// A publishing mutation executed on the graph
+pub struct PubMutation {
+ _crate: CrateId,
+ new_version: String,
+}
+
+impl PubMutation {
+ /// Createa new motation from a crate a version string
+ pub fn new(c: &Crate, new_version: String) -> Self {
+ Self {
+ _crate: c.id,
+ new_version,
+ }
+ }
+}
+
+/// A collection of mutations performed in a batch
+pub struct MutationSet {}
diff --git a/development/tools/cargo-workspace2/src/ops/error.rs b/development/tools/cargo-workspace2/src/ops/error.rs
new file mode 100644
index 000000000000..3dde73d954d8
--- /dev/null
+++ b/development/tools/cargo-workspace2/src/ops/error.rs
@@ -0,0 +1,28 @@
+use std::fmt::{self, Display, Formatter};
+
+/// Special result type that wraps an OpError
+pub type Result<T> = std::result::Result<T, OpError>;
+
+/// An error that occured while running an operation
+#[derive(Debug)]
+pub enum OpError {
+ NoSuchCrate(String),
+ CircularDependency(String, String),
+}
+
+impl Display for OpError {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ write!(
+ f,
+ "{}",
+ match self {
+ Self::NoSuchCrate(n) => format!("No crate `{}` was not found in the workspace", n),
+ Self::CircularDependency(a, b) => format!(
+ "Crates `{}` and `{}` share a hard circular dependency.\
+ Operation not possible!",
+ a, b
+ ),
+ }
+ )
+ }
+}
diff --git a/development/tools/cargo-workspace2/src/ops/executor.rs b/development/tools/cargo-workspace2/src/ops/executor.rs
new file mode 100644
index 000000000000..90a4f68303b1
--- /dev/null
+++ b/development/tools/cargo-workspace2/src/ops/executor.rs
@@ -0,0 +1,33 @@
+//! An op executor
+
+use super::{versions, Print, Publish, PublishMod as Mod, PublishType};
+use crate::models::{CrateId, DepGraph};
+use semver::Version;
+use std::path::PathBuf;
+
+#[inline]
+fn abs_path<'a>(root: &PathBuf, c_path: PathBuf, abs: bool) -> String {
+ if abs {
+ root.join(c_path).display().to_string()
+ } else {
+ c_path.display().to_string()
+ }
+}
+
+/// A simple print executor
+pub(super) fn print(p: Print, set: Vec<CrateId>, root: PathBuf, g: &mut DepGraph) {
+ set.into_iter()
+ .map(|id| g.get_crate(id))
+ .map(|_crate| {
+ let c_path = _crate.path().clone();
+
+ match p {
+ Print::Name => format!("{}", _crate.name()),
+ Print::Path { abs } => format!("{}", abs_path(&root, c_path, abs)),
+ Print::Both { abs } => {
+ format!("{}: {}", _crate.name(), abs_path(&root, c_path, abs))
+ }
+ }
+ })
+ .for_each(|s| println!("{}", s));
+}
diff --git a/development/tools/cargo-workspace2/src/ops/mod.rs b/development/tools/cargo-workspace2/src/ops/mod.rs
new file mode 100644
index 000000000000..026aaa0a140a
--- /dev/null
+++ b/development/tools/cargo-workspace2/src/ops/mod.rs
@@ -0,0 +1,106 @@
+//! Atomic operations on a cargo workspace
+//!
+//! This module contains operations that can be executed on a
+//! workspace. They take some set of inputs, modelled as fields, and
+//! produce a shared output which is represented by `Result`
+
+mod publish;
+pub(self) use publish::{versions, Publish, PublishMod, PublishType};
+
+mod error;
+mod executor;
+mod parser;
+
+use crate::models::{CrateId, DepGraph};
+pub use error::{OpError, Result};
+use std::path::PathBuf;
+
+trait RenderHelp {
+ fn render_help(c: i32) -> !;
+}
+
+/// Render the help-page for a particular command
+pub(crate) fn render_help(cmd: String) -> ! {
+ match cmd.as_str() {
+ "print" => Print::render_help(0),
+ "publish" => Publish::render_help(0),
+ c => {
+ eprintln!("Unknown command `{}`", c);
+ std::process::exit(2);
+ }
+ }
+}
+
+pub(crate) fn list_commands() {
+ eprintln!("Available commands:\n");
+ eprintln!(" - print: echo the selected crate set");
+ eprintln!(" - publish: release new crates on crates.io");
+}
+
+/// Differentiating operation enum
+pub enum Op {
+ /// Publish a new version, according to some rules
+ Publish(Publish),
+ /// Print the query selection
+ Print(Print),
+}
+
+impl Op {
+ /// Parse an arg line into an operation to execute
+ pub fn parse(line: Vec<String>) -> Self {
+ match parser::run(line) {
+ Some(op) => op,
+ None => std::process::exit(2),
+ }
+ }
+
+ pub(crate) fn execute(self, set: Vec<CrateId>, root: PathBuf, g: &mut DepGraph) {
+ match self {
+ Self::Publish(p) => publish::run(p, set, g),
+ Self::Print(p) => executor::print(p, set, root, g),
+ }
+ }
+}
+
+/// Ask the user to be sure
+pub(self) fn verify_user() {
+ eprintln!("------");
+ use std::io::{stdin, stdout, Write};
+ let mut s = String::new();
+ print!("Execute operations? [N|y]: ");
+ let _ = stdout().flush();
+ stdin().read_line(&mut s).expect("Failed to read term!");
+
+ match s.trim() {
+ "Y" | "y" => {}
+ _ => std::process::exit(0),
+ }
+}
+
+/// Selection of which type to print
+pub enum Print {
+ /// Default: just the package name
+ Name,
+ /// The path inside the repo
+ Path { abs: bool },
+ /// Both the name and path
+ Both { abs: bool },
+}
+
+impl Default for Print {
+ fn default() -> Self {
+ Self::Name
+ }
+}
+
+impl RenderHelp for Print {
+ fn render_help(code: i32) -> ! {
+ eprintln!("Print the selected set of crates");
+ eprintln!("Usage: cargo ws2 <...> print [OPTIONS]\n");
+ eprintln!("Available options:\n");
+ eprintln!(" - path: print the path of the crate, instead of the name");
+ eprintln!(" - both: print the both the path and the name");
+ eprintln!(" - abs: (If `path` or `both`) Use an absolute path, instead of relative");
+ std::process::exit(code)
+ }
+}
diff --git a/development/tools/cargo-workspace2/src/ops/parser.rs b/development/tools/cargo-workspace2/src/ops/parser.rs
new file mode 100644
index 000000000000..6b7d39b71851
--- /dev/null
+++ b/development/tools/cargo-workspace2/src/ops/parser.rs
@@ -0,0 +1,112 @@
+use super::{Op, Print, Publish, PublishMod as Mod, PublishType::*};
+use semver::Version;
+
+/// Parse a argument line into an operation
+pub(super) fn run(mut line: Vec<String>) -> Option<Op> {
+ let op = line.remove(0);
+
+ match op.as_str() {
+ "print" => parse_print(line),
+ "publish" => parse_publish(line),
+ op => {
+ eprintln!("[ERROR]: Unrecognised operation `{}`", op);
+ None
+ }
+ }
+}
+
+fn parse_print(line: Vec<String>) -> Option<Op> {
+ let abs = line
+ .get(1)
+ .map(|abs| match abs.as_str() {
+ "abs" | "absolute" => true,
+ _ => false,
+ })
+ .unwrap_or(false);
+
+ match line.get(0).map(|s| s.as_str()) {
+ Some("path") => Some(Op::Print(Print::Path { abs })),
+ Some("both") => Some(Op::Print(Print::Both { abs })),
+ Some("name") | None => Some(Op::Print(Print::Name)),
+ Some(val) => {
+ eprintln!("[ERROR]: Unrecognised param: `{}`", val);
+ None
+ }
+ }
+}
+
+fn publish_error() -> Option<Op> {
+ eprintln!(
+ "[ERROR]: Missing or invalid operands!
+Usage: cargo ws2 <QUERY LANG> publish <level> [modifier] [devel]\n
+ Valid <level>: major, minor, patch, or '=<VERSION>' to set an override version.\n
+ Valid [modifier]: alpha, beta, rc\n
+ [devel]: after publishing, set crate version to <VERSION>-devel"
+ );
+ None
+}
+
+fn parse_publish(mut line: Vec<String>) -> Option<Op> {
+ line.reverse();
+ let tt = match line.pop() {
+ Some(tt) => tt,
+ None => {
+ return publish_error();
+ }
+ };
+
+ let _mod = line.pop();
+ let tt = match (tt.as_str(), _mod.as_ref().map(|s| s.as_str())) {
+ ("major", Some("alpha")) => Major(Mod::Alpha),
+ ("major", Some("beta")) => Major(Mod::Beta),
+ ("major", Some("rc")) => Major(Mod::Rc),
+ ("minor", Some("alpha")) => Minor(Mod::Alpha),
+ ("minor", Some("beta")) => Minor(Mod::Beta),
+ ("minor", Some("rc")) => Minor(Mod::Rc),
+ ("patch", Some("alpha")) => Patch(Mod::Alpha),
+ ("patch", Some("beta")) => Patch(Mod::Beta),
+ ("patch", Some("rc")) => Patch(Mod::Rc),
+ ("major", Some(_)) | ("major", None) => Major(Mod::None),
+ ("minor", Some(_)) | ("minor", None) => Minor(Mod::None),
+ ("patch", Some(_)) | ("patch", None) => Patch(Mod::None),
+ (v, _) if v.trim().starts_with("=") => {
+ let vers = v.replace("=", "").to_string();
+ if vers.as_str() == "" {
+ return publish_error();
+ } else {
+ Fixed(vers)
+ }
+ }
+ (_, _) => {
+ return publish_error();
+ }
+ };
+
+ let devel = match tt {
+ // Means _mod was not a mod
+ Major(Mod::None) | Minor(Mod::None) | Patch(Mod::None) => {
+ if let Some(devel) = _mod {
+ if devel == "devel" {
+ true
+ } else {
+ return publish_error();
+ }
+ } else {
+ false
+ }
+ }
+ _ => {
+ if let Some(devel) = line.pop() {
+ if devel == "devel" {
+ true
+ } else {
+ false
+ }
+ } else {
+ false
+ }
+ }
+ };
+
+ Some(Op::Publish(Publish { tt, devel }))
+}
diff --git a/development/tools/cargo-workspace2/src/ops/publish/exec.rs b/development/tools/cargo-workspace2/src/ops/publish/exec.rs
new file mode 100644
index 000000000000..3f2d36863bcf
--- /dev/null
+++ b/development/tools/cargo-workspace2/src/ops/publish/exec.rs
@@ -0,0 +1,117 @@
+//! Publishing executor
+
+use super::{Publish, PublishMod as Mod, PublishType};
+use crate::{
+ models::{CrateId, DepGraph},
+ ops::verify_user,
+};
+use semver::Version;
+use textwrap;
+
+pub(in crate::ops) fn run(p: Publish, set: Vec<CrateId>, g: &mut DepGraph) {
+ let Publish { ref tt, devel } = p;
+
+ let vec = set.into_iter().fold(vec![], |mut vec, id| {
+ let c = g.mut_crate(id);
+ let c_name = c.name().clone();
+ let curr_ver = c.version();
+ let new_ver = bump(&curr_ver, tt);
+ c.publish(new_ver.clone());
+ eprintln!("Bumping `{}`: `{}` ==> `{}`", c.name(), &curr_ver, new_ver);
+ drop(c);
+
+ g.get_dependents(id).into_iter().for_each(|id| {
+ let dep = g.mut_crate(id);
+ dep.change_dependency(&c_name, &new_ver);
+ eprintln!("Changing dependency `{}`: {} = {{ version = `{}`, ... }} ==> {{ version = `{}`, ... }}",
+ dep.name(),
+ c_name,
+ &curr_ver,
+ new_ver);
+ vec.push(id);
+ });
+
+ vec.push(id);
+ vec
+ });
+
+ // If we make it past this point the user is sure
+ verify_user();
+
+ vec.into_iter().for_each(|(id)| {
+ let c = g.mut_crate(id);
+ c.sync();
+ });
+
+ eprintln!("{}", textwrap::fill("Publish complete. Check that everything is in order, \
+ then run `cargo publish` to upload to crates.io!", 80));
+}
+
+/// Bump a version number according to some rules
+fn bump(v: &String, tt: &PublishType) -> String {
+ let mut ver = Version::parse(&v).unwrap();
+ let ver = match tt {
+ PublishType::Fixed(ref ver) => Version::parse(ver).unwrap(),
+ PublishType::Major(_) => {
+ ver.increment_major();
+ ver
+ }
+ PublishType::Minor(_) => {
+ ver.increment_minor();
+ ver
+ }
+ PublishType::Patch(_) => {
+ ver.increment_patch();
+ ver
+ }
+ };
+
+ // If a mod applies, handle it...
+ if let Some(_mod) = tt._mod() {
+ if ver.is_prerelease() {
+ let v = ver.clone().to_string();
+ let num = v.split(".").last().unwrap();
+ let num: usize = num.parse().unwrap();
+ ver.clone()
+ .to_string()
+ .replace(&format!("{}", num), &format!("{}", num + 1))
+ .to_string()
+ } else {
+ format!(
+ "{}-{}",
+ ver.to_string(),
+ match _mod {
+ Mod::Alpha => "alpha.0",
+ Mod::Beta => "beta.0",
+ Mod::Rc => "rc.0",
+ _ => unreachable!(),
+ }
+ )
+ }
+ } else {
+ ver.to_string()
+ }
+}
+
+macro_rules! assert_bump {
+ ($input:expr, $level_mod:expr, $expect:expr) => {
+ assert_eq!(bump(&$input.to_owned(), $level_mod), $expect.to_string());
+ };
+}
+
+#[cfg(test)]
+use super::{PublishMod::*, PublishType::*};
+
+#[test]
+fn bump_major() {
+ assert_bump!("1.0.0", &Major(None), "2.0.0");
+ assert_bump!("1.5.4", &Major(None), "2.0.0");
+ assert_bump!("1.3.0", &Major(Alpha), "2.0.0-alpha.0");
+}
+
+#[test]
+fn bump_minor() {
+ assert_bump!("1.1.0", &Minor(None), "1.2.0");
+ assert_bump!("1.5.4", &Minor(Beta), "1.6.0-beta.0");
+ assert_bump!("1.5.4-beta.0", &Minor(Beta), "1.6.0-beta.0");
+}
diff --git a/development/tools/cargo-workspace2/src/ops/publish/mod.rs b/development/tools/cargo-workspace2/src/ops/publish/mod.rs
new file mode 100644
index 000000000000..6022496af273
--- /dev/null
+++ b/development/tools/cargo-workspace2/src/ops/publish/mod.rs
@@ -0,0 +1,132 @@
+//! Publishing operation handling
+
+mod exec;
+pub(super) use exec::run;
+
+use super::RenderHelp;
+
+/// Publish a single crate to crates.io
+///
+/// This command does the following things
+///
+/// 0. Determine if the git tree has modifications, `cargo publish`
+/// will refuse to work otherwise.
+/// 1. Find the crate in question
+/// 2. Bump the version number according to the user input
+/// 3. Find all dependent crates in the workspace with a version bound
+/// 4. Update the version bound to the new version
+pub struct Publish {
+ /// The version type to publish
+ pub tt: PublishType,
+ /// Whether to set a new devel version after publish
+ pub devel: bool,
+}
+
+impl RenderHelp for Publish {
+ fn render_help(code: i32) -> ! {
+ eprintln!("Publish the selected set of crates");
+ eprintln!("Usage: cargo ws2 <...> publish [=]<level> [OPTIONS]");
+ eprintln!("");
+ eprintln!("When prepending `=` to the level, bump crates in sync\n");
+ eprintln!("Available levels:\n");
+ eprintln!(" - major: Bump major version (1.0.0 -> 2.0.0)");
+ eprintln!(" - minor: Bump minor version (0.5.0 -> 0.6.0)");
+ eprintln!(" - patch: Bump patch version (0.5.0 -> 0.5.1)");
+ eprintln!("");
+ eprintln!("Available options:\n");
+ eprintln!(" - alpha: Create a new alpha (append `-alpha.X`)");
+ eprintln!(" - beta: Create a new beta (append `-beta.X`)");
+ eprintln!(" - rc: Create a new rc (append `-rc.X`)");
+ eprintln!(" - devel: Tag next version as -devel");
+ std::process::exit(code)
+ }
+}
+
+/// The level to which to update
+///
+/// New versions are based on the previous version, and are always
+/// computed on the fly.
+///
+/// It's recommended you use the [`versions`](./versions/index.html)
+/// builder functions.
+pub enum PublishType {
+ /// A fixed version to set the set to
+ Fixed(String),
+ /// A major bump (e.g. 1.0 -> 2.0)
+ Major(PublishMod),
+ /// A minor bump (e.g. 0.1 -> 0.2)
+ Minor(PublishMod),
+ /// A patch bump (e.g. 1.5.0 -> 1.5.1)
+ Patch(PublishMod),
+}
+
+impl PublishType {
+ pub(crate) fn _mod(&self) -> Option<&PublishMod> {
+ match self {
+ Self::Major(ref m) => Some(m),
+ Self::Minor(ref m) => Some(m),
+ Self::Patch(ref m) => Some(m),
+ Self::Fixed(_) => None,
+ }
+ .and_then(|_mod| match _mod {
+ PublishMod::None => None,
+ other => Some(other),
+ })
+ }
+}
+
+/// Version string modifier
+///
+/// It's recommended you use the [`versions`](./versions/index.html)
+/// builder functions.
+pub enum PublishMod {
+ /// No version modification
+ None,
+ /// Append `-alpha$X` where `$X` is a continuously increasing number
+ Alpha,
+ /// Append `-beta$X` where `$X` is a continuously increasing number
+ Beta,
+ /// Append `-rc$X` where `$X` is a continuously increasing number
+ Rc,
+}
+
+/// Version bump management
+pub mod versions {
+ use super::{PublishMod, PublishType};
+
+ /// Create a major publish
+ pub fn major(_mod: PublishMod) -> PublishType {
+ PublishType::Major(_mod)
+ }
+
+ /// Create a minor publish
+ pub fn minor(_mod: PublishMod) -> PublishType {
+ PublishType::Minor(_mod)
+ }
+
+ /// Create a patch publish
+ pub fn patch(_mod: PublishMod) -> PublishType {
+ PublishType::Patch(_mod)
+ }
+
+ /// Create a none modifier
+ pub fn mod_none() -> PublishMod {
+ PublishMod::None
+ }
+
+ /// Create an alpha modifier
+ pub fn mod_alpha() -> PublishMod {
+ PublishMod::Alpha
+ }
+
+ /// Create a beta modifier
+ pub fn mod_beta() -> PublishMod {
+ PublishMod::Beta
+ }
+
+ /// Create an rc modifier
+ pub fn mod_rc() -> PublishMod {
+ PublishMod::Rc
+ }
+}
+
diff --git a/development/tools/cargo-workspace2/src/query/executor.rs b/development/tools/cargo-workspace2/src/query/executor.rs
new file mode 100644
index 000000000000..da67324de4c0
--- /dev/null
+++ b/development/tools/cargo-workspace2/src/query/executor.rs
@@ -0,0 +1,83 @@
+use super::{Constraint, DepConstraint};
+use crate::models::{CrateId, DepGraph};
+use std::collections::BTreeSet;
+
+/// Execute a simple set query
+pub(crate) fn set(crates: &Vec<String>, g: &DepGraph) -> Vec<CrateId> {
+ crates
+ .iter()
+ .filter_map(|name| match g.find_crate(name) {
+ None => {
+ eprintln!("[ERROR]: Unable to find crate: `{}`", name);
+ None
+ }
+ some => some,
+ })
+ .collect()
+}
+
+/// Execute a search query on the dependency graph
+pub(crate) fn deps(mut deps: Vec<DepConstraint>, g: &DepGraph) -> Vec<CrateId> {
+ // Parse the anchor point (first crate)
+ let DepConstraint {
+ ref _crate,
+ ref constraint,
+ } = deps.remove(0);
+ let init_id = get_crate_error(_crate, g);
+
+ // Get it's dependents
+ let mut dependents: BTreeSet<_> = match constraint {
+ Constraint::Initial(true) => g.get_dependents(init_id).into_iter().collect(),
+ Constraint::Initial(false) => g
+ .get_all()
+ .iter()
+ .filter(|c| c.has_dependency(init_id))
+ .map(|c| c.id)
+ .collect(),
+ _ => {
+ eprintln!("Invalid initial constraint! Only `<` and `!<` are allowed!");
+ std::process::exit(2);
+ }
+ };
+
+ // Then loop over all other constraints and subtract crates from
+ // the dependents set until all constraints are met.
+ deps.reverse();
+ while let Some(dc) = deps.pop() {
+ let DepConstraint {
+ ref _crate,
+ ref constraint,
+ } = dc;
+
+ let id = get_crate_error(_crate, g);
+ let ldeps = g.get_dependents(id);
+ dependents = apply_constraint(dependents, ldeps, constraint);
+ }
+
+ dependents.into_iter().collect()
+}
+
+fn get_crate_error(_crate: &String, g: &DepGraph) -> CrateId {
+ match g.find_crate(&_crate.trim().to_string()) {
+ Some(id) => id,
+ None => {
+ eprintln!("[ERROR]: Crate `{}` not found in workspace!", _crate);
+ std::process::exit(2);
+ }
+ }
+}
+
+fn apply_constraint(
+ init: BTreeSet<CrateId>,
+ cmp: Vec<CrateId>,
+ cnd: &Constraint,
+) -> BTreeSet<CrateId> {
+ let cmp: BTreeSet<CrateId> = cmp.into_iter().collect();
+ let init = init.into_iter();
+ match cnd {
+ Constraint::And(true) => init.filter(|id| cmp.contains(id)).collect(),
+ Constraint::And(false) => init.filter(|id| !cmp.contains(id)).collect(),
+ Constraint::Or => init.chain(cmp.into_iter()).collect(),
+ _ => todo!(),
+ }
+}
diff --git a/development/tools/cargo-workspace2/src/query/mod.rs b/development/tools/cargo-workspace2/src/query/mod.rs
new file mode 100644
index 000000000000..a888f6571663
--- /dev/null
+++ b/development/tools/cargo-workspace2/src/query/mod.rs
@@ -0,0 +1,280 @@
+//! Parse the query language for the CLI and other tools
+//!
+//! The `cargo-ws2` query language (`ws2ql`) allows users to specify a
+//! set of inputs, and an operation to execute on it.
+//!
+//! ## Basic rules
+//!
+//! * Inside `[]` are sets (meaning items de-dup) that don't require
+//! reparations
+//! * IF `[]` contains a `/` anywhere _but_ the beginning AND end,
+//! query becomes a path glob
+//! * IF `[]` contains `/` at start and end, query becomes a regex
+//! * An operation is parsed in the order of the fields in it's struct
+//! (so publish order is `type mod devel`, etc)
+//! * Inside `{}` you can create dependency maps with arrows.
+//! * `{ foo < }` represents all crates that depend on `foo`
+//! * `{ foo < bar < }` represents all crates that depend on `foo` AND `bar`
+//! * `{ foo < bar |< }` represents all crates that depend on `foo` but NOT on `bar`
+//!
+//! ... etc.
+
+use crate::models::{CrateId, DepGraph};
+
+mod executor;
+
+/// A query on the dependency graph
+#[derive(Debug)]
+pub enum Query {
+ /// Simple set of names `[ a b c ]`
+ Set(Vec<String>),
+ /// Simple path glob query `./foo/*`
+ Path(String),
+ /// Regular expression done on paths in tree `/foo\$/`
+ Regex(String),
+ /// A dependency graph query (see documentation for rules)
+ DepGraph(Vec<DepConstraint>),
+}
+
+impl Query {
+ /// Parse an argument iterator (provided by `std::env::args`)
+ pub fn parse<'a>(line: impl Iterator<Item = String>) -> (Self, Vec<String>) {
+ let parser = QueryParser::new(line.skip(1).collect());
+ match parser.run() {
+ Some((q, rest)) => (q, rest),
+ None => {
+ eprintln!("Failed to parse query!");
+ std::process::exit(2);
+ }
+ }
+ }
+
+ /// Execute a query with a dependency graph
+ pub fn execute(self, g: &DepGraph) -> Vec<CrateId> {
+ match self {
+ Self::Set(ref crates) => executor::set(crates, g),
+ Self::DepGraph(deps) => executor::deps(deps, g),
+ _ => todo!(),
+ }
+ }
+}
+
+/// Express a dependency constraint
+#[derive(Debug)]
+pub struct DepConstraint {
+ pub _crate: String,
+ pub constraint: Constraint,
+}
+
+/// All constraints can be negated
+#[derive(Debug)]
+pub enum Constraint {
+ Initial(bool),
+ And(bool),
+ Or,
+}
+
+struct QueryParser {
+ line: Vec<String>,
+}
+
+impl QueryParser {
+ fn new(line: Vec<String>) -> Self {
+ Self { line }
+ }
+
+ /// Run the parser until it yields an error or finished query
+ fn run(mut self) -> Option<(Query, Vec<String>)> {
+ let line: Vec<String> =
+ std::mem::replace(&mut self.line, vec![])
+ .into_iter()
+ .fold(vec![], |mut vec, line| {
+ line.split(" ").for_each(|seg| {
+ vec.push(seg.into());
+ });
+ vec
+ });
+
+ // This is here to get around infinately sized enums
+ #[derive(Debug)]
+ enum Brace {
+ Block,
+ Curly,
+ }
+
+ // Track the state of the query braces
+ #[derive(Debug)]
+ enum BraceState {
+ Missing,
+ BlockOpen,
+ CurlyOpen,
+ Done(Brace),
+ }
+ use {Brace::*, BraceState::*};
+ let mut bs = Missing;
+ let mut buf = vec![];
+ let mut cbuf = String::new(); // Store a single crate name as a buffer
+ let mut skip = 1;
+
+ // Parse the crate set
+ for elem in &line {
+ match (&bs, elem.as_str()) {
+ // The segment starts with a [
+ (Missing, e) if e.starts_with("[") => {
+ bs = BlockOpen;
+ // Handle the case where we need to grab the crate from this segment
+ if let Some(_crate) = e.strip_prefix("[") {
+ if _crate != "" {
+ buf.push(_crate.to_string());
+ }
+ }
+ }
+ // The segment starts with a {
+ (Missing, e) if e.starts_with("{") => {
+ bs = CurlyOpen;
+
+ if let Some(_crate) = e.strip_prefix("{") {
+ if _crate != "" {
+ cbuf = _crate.into();
+ }
+ }
+ }
+ (BlockOpen, e) if e.ends_with("]") => {
+ if let Some(_crate) = e.strip_suffix("]") {
+ if _crate != "" {
+ buf.push(_crate.to_string());
+ }
+ }
+
+ bs = Done(Block);
+ break;
+ }
+ (BlockOpen, _crate) => buf.push(_crate.to_string()),
+ (CurlyOpen, e) if e.ends_with("}") && cbuf == "" => {
+ bs = Done(Curly);
+ break;
+ }
+ (CurlyOpen, e) if e.ends_with("}") && cbuf != "" => {
+ eprintln!("[ERROR]: Out of place `}}`, expected operand!");
+ std::process::exit(2);
+ }
+ (CurlyOpen, op) if cbuf != "" => {
+ buf.push(format!("{} $ {}", cbuf, op));
+ cbuf = "".into();
+ }
+ (CurlyOpen, _crate) => {
+ cbuf = _crate.into();
+ }
+ (_, _) => {}
+ }
+ skip += 1;
+ }
+
+ let rest = line.into_iter().skip(skip).collect();
+ match bs {
+ Done(Block) => Some((Query::Set(buf), rest)),
+ Done(Curly) => {
+ let mut init = true;
+
+ let c: Vec<_> = buf
+ .into_iter()
+ .map(|val| {
+ let mut s: Vec<_> = val.split("$").collect();
+ let _crate = s.remove(0).trim().to_string();
+ let c = s.remove(0).trim().to_string();
+
+ DepConstraint {
+ _crate,
+ constraint: match c.as_str() {
+ "<" if init => {
+ init = false;
+ Constraint::Initial(true)
+ }
+ "!<" if init => {
+ init = false;
+ Constraint::Initial(false)
+ }
+ "&<" => Constraint::And(true),
+ "!&<" => Constraint::And(false),
+ "|<" => Constraint::Or,
+ c => {
+ eprintln!("[ERROR]: Invalid constraint: `{}`", c);
+ std::process::exit(2);
+ }
+ },
+ }
+ })
+ .collect();
+
+ if c.len() < 1 {
+ eprintln!("[ERROR]: Provided an empty graph set: {{ }}. At least one dependency required!");
+ std::process::exit(2);
+ }
+
+ Some((Query::DepGraph(c), rest))
+ }
+ _ if rest.len() < 1 => crate::cli::render_help(2),
+ _line => {
+ eprintln!("[ERROR]: You reached some unimplemented code in cargo-ws2! \
+ This might be a bug, or it might be a missing feature. Contact me with your query, \
+ and we can see which one it is :)");
+ std::process::exit(2);
+ }
+ }
+ }
+}
+
+#[test]
+fn block_parser_spaced() {
+ let _ = QueryParser::new(
+ vec!["", "[", "foo", "bar", "baz", "]", "publish", "minor"]
+ .into_iter()
+ .map(Into::into)
+ .collect(),
+ )
+ .run();
+}
+
+#[test]
+fn block_parser_offset_front() {
+ let _ = QueryParser::new(
+ vec!["my-program", "[foo", "bar", "baz", "]", "publish", "minor"]
+ .into_iter()
+ .map(Into::into)
+ .collect(),
+ )
+ .run();
+}
+
+#[test]
+fn block_parser_offset_back() {
+ let _ = QueryParser::new(
+ vec!["my-program", "[", "foo", "bar", "baz]", "publish", "minor"]
+ .into_iter()
+ .map(Into::into)
+ .collect(),
+ )
+ .run();
+}
+
+#[test]
+fn block_parser_offset_both() {
+ let _ = QueryParser::new(
+ vec!["my-program", "[foo", "bar", "baz]", "publish", "minor"]
+ .into_iter()
+ .map(Into::into)
+ .collect(),
+ )
+ .run();
+}
+
+#[test]
+fn curly_parser_simple() {
+ let _ = QueryParser::new(
+ vec!["my-program", "{ foo < bar &< }", "print"]
+ .into_iter()
+ .map(Into::into)
+ .collect(),
+ )
+ .run();
+}