use crate::{
fs::{config_dir, data_dir, home_dir, load, save, Existence},
glob_match,
model::{Cluster, DeploymentKind, DirAlias, NodeEntry},
store::{DeployAction, MultiNodeClone, Node, Root, TablizeCluster},
Error, Result,
};
use clap::{Parser, Subcommand};
use inquire::prompt_confirmation;
use std::{ffi::OsString, fs::remove_dir_all, path::PathBuf};
use tracing::{instrument, warn};
#[derive(Debug, Clone, Parser)]
#[command(
about,
override_usage = "\n ocd [options] <ocd-command>\n ocd [options] [pattern]... <git-command>",
subcommand_help_heading = "Commands",
version
)]
pub struct Ocd {
#[command(subcommand)]
pub command: Command,
}
impl Ocd {
pub async fn run(self) -> Result<()> {
match self.command {
Command::Clone(opts) => run_clone(opts).await,
Command::Init(opts) => run_init(opts),
Command::Deploy(opts) => run_deploy(opts),
Command::Undeploy(opts) => run_undeploy(opts),
Command::Remove(opts) => run_remove(opts),
Command::List(opts) => run_list(opts),
Command::Git(opts) => run_git(opts),
}
}
}
#[derive(Debug, Clone, Subcommand)]
pub enum Command {
#[command(override_usage = "ocd clone [options] <url>")]
Clone(CloneOptions),
#[command(override_usage = "ocd init [options] <node_name>")]
Init(InitOptions),
#[command(override_usage = "ocd deploy [options] [pattern]...")]
Deploy(DeployOptions),
#[command(override_usage = "ocd undeploy [options] [pattern]...")]
Undeploy(UndeployOptions),
#[command(name = "rm", override_usage = "ocd rm [options] [pattern]...")]
Remove(RemoveOptions),
#[command(name = "ls", override_usage = "ocd list [options]")]
List(ListOptions),
#[command(external_subcommand)]
Git(Vec<OsString>),
}
#[derive(Parser, Clone, Debug)]
#[command(author, about, long_about)]
pub struct CloneOptions {
#[arg(value_name = "url")]
pub url: String,
#[arg(short, long, value_name = "limit")]
pub jobs: Option<usize>,
}
#[derive(Parser, Clone, Debug)]
#[command(author, about, long_about)]
pub struct InitOptions {
#[arg(group = "entry", value_name = "node_name")]
pub node_name: Option<String>,
#[arg(groups = ["entry", "kind"], short, long)]
pub root: bool,
#[arg(group = "node", short, long, value_name = "dir_path")]
pub dir_alias: Option<PathBuf>,
#[arg(groups = ["node", "kind"], short = 'H', long)]
pub home_alias: bool,
}
#[derive(Parser, Clone, Debug)]
#[command(author, about, long_about)]
pub struct DeployOptions {
#[arg(value_parser, num_args = 1.., value_delimiter = ',', value_name = "pattern")]
pub patterns: Vec<String>,
#[arg(short, long)]
pub only: bool,
#[arg(short, long)]
pub with_excluded: bool,
}
#[derive(Parser, Clone, Debug)]
#[command(author, about, long_about)]
pub struct UndeployOptions {
#[arg(value_parser, num_args = 1.., value_delimiter = ',', value_name = "pattern")]
pub patterns: Vec<String>,
#[arg(short, long)]
pub only: bool,
#[arg(short, long)]
pub excluded_only: bool,
}
#[derive(Parser, Clone, Debug)]
#[command(author, about, long_about)]
pub struct RemoveOptions {
#[arg(value_parser, num_args = 1.., value_delimiter = ',', value_name = "pattern")]
pub patterns: Vec<String>,
}
#[derive(Parser, Clone, Debug)]
#[command(author, about, long_about)]
pub struct ListOptions {
#[arg(short, long)]
pub names_only: bool,
}
async fn run_clone(opts: CloneOptions) -> Result<()> {
let _ = match Root::new_clone(&opts.url) {
Ok(root) => root,
Err(error) => {
remove_dir_all(data_dir()?)?;
remove_dir_all(config_dir()?)?;
return Err(error);
}
};
let cluster: Cluster = load("cluster.toml", Existence::Required)?;
let multi_clone = MultiNodeClone::new(&cluster, opts.jobs)?;
multi_clone.clone_all().await?;
Ok(())
}
pub fn run_init(opts: InitOptions) -> Result<()> {
let mut cluster: Cluster = load("cluster.toml", Existence::Required)?;
if opts.root {
let _ = Root::new_init()?;
} else {
if !data_dir()?.join("root").exists() {
let _ = Root::new_init()?;
}
let deployment = if opts.home_alias {
DeploymentKind::BareAlias(DirAlias::new(home_dir()?))
} else if opts.dir_alias.is_some() {
DeploymentKind::BareAlias(DirAlias::new(opts.dir_alias.unwrap()))
} else {
DeploymentKind::Normal
};
let node = NodeEntry { deployment, ..Default::default() };
let name = opts.node_name.as_ref().ok_or(Error::NoNodeName)?;
let _ = Node::new_init(name, &node)?;
cluster.add_node(name, node)?;
}
save("cluster.toml", cluster.to_string())?;
Ok(())
}
#[instrument(skip(opts), level = "debug")]
pub fn run_deploy(mut opts: DeployOptions) -> Result<()> {
let root = Root::new_open()?;
let cluster: Cluster = load("cluster.toml", Existence::Required)?;
let action = if opts.with_excluded { DeployAction::DeployAll } else { DeployAction::Deploy };
opts.patterns.dedup();
for pattern in &mut opts.patterns {
pattern.retain(|c| !c.is_whitespace());
}
if let Some(index) = opts.patterns.iter().position(|x| *x == "root") {
opts.patterns.swap_remove(index);
root.deploy(action)?;
}
let targets = glob_match(&opts.patterns, cluster.nodes.keys());
for target in &targets {
if opts.only {
let entry = cluster.get_node(target)?;
let node = if data_dir()?.join(target).exists() {
Node::new_open(target, entry)?
} else {
warn!("Node {target:?} does not exist in repository store, cloning it");
Node::new_clone(target, entry)?
};
node.deploy(action)?;
} else {
for (name, entry) in cluster.dependency_iter(target) {
let node = if data_dir()?.join(name).exists() {
Node::new_open(name, entry)?
} else {
warn!("Node {name:?} does not exist in repository store, cloning it");
Node::new_clone(name, entry)?
};
node.deploy(action)?;
}
}
}
Ok(())
}
fn run_undeploy(mut opts: UndeployOptions) -> Result<()> {
let root = Root::new_open()?;
let cluster: Cluster = load("cluster.toml", Existence::Required)?;
let action =
if opts.excluded_only { DeployAction::UndeployExcludes } else { DeployAction::Undeploy };
opts.patterns.dedup();
for pattern in &mut opts.patterns {
pattern.retain(|c| !c.is_whitespace());
}
if let Some(index) = opts.patterns.iter().position(|x| *x == "root") {
opts.patterns.swap_remove(index);
root.deploy(action)?;
}
let targets = glob_match(&opts.patterns, cluster.nodes.keys());
for target in &targets {
if opts.only {
let entry = cluster.get_node(target)?;
let node = if data_dir()?.join(target).exists() {
Node::new_open(target, entry)?
} else {
warn!("Node {target:?} does not exist in repository store, cloning it");
Node::new_clone(target, entry)?
};
node.deploy(action)?;
} else {
for (name, entry) in cluster.dependency_iter(target) {
let node = if data_dir()?.join(name).exists() {
Node::new_open(name, entry)?
} else {
warn!("Node {name:?} does not exist in repository store, cloning it");
Node::new_clone(name, entry)?
};
node.deploy(action)?;
}
}
}
Ok(())
}
#[instrument(skip(opts), level = "debug")]
fn run_remove(mut opts: RemoveOptions) -> Result<()> {
let mut cluster: Cluster = load("cluster.toml", Existence::Required)?;
opts.patterns.dedup();
for pattern in &mut opts.patterns {
pattern.retain(|c| !c.is_whitespace());
}
if let Some(index) = opts.patterns.iter().position(|x| *x == "root") {
warn!("Removing root will nuke your entire cluster");
if prompt_confirmation("Do you want to send your cluster to the gallows? [y/n]")? {
let root = Root::new_open()?;
root.nuke()?;
return Ok(());
} else {
opts.patterns.swap_remove(index);
}
}
let targets = glob_match(&opts.patterns, cluster.nodes.keys());
for target in &targets {
let node = cluster.remove_node(target)?;
let repo = Node::new_open(target, &node)?;
repo.deploy(DeployAction::Undeploy)?;
remove_dir_all(repo.path())?;
}
save("cluster.toml", cluster.to_string())?;
Ok(())
}
fn run_list(opts: ListOptions) -> Result<()> {
let root = Root::new_open()?;
let cluster: Cluster = load("cluster.toml", Existence::Required)?;
let tablize = TablizeCluster::new(&root, &cluster);
if opts.names_only {
tablize.names_only()?;
} else {
tablize.fancy()?;
}
Ok(())
}
fn run_git(opts: Vec<OsString>) -> Result<()> {
let cluster: Cluster = load("cluster.toml", Existence::Required)?;
let mut patterns = opts[0].to_string_lossy().into_owned();
patterns.retain(|c| !c.is_whitespace());
let mut patterns: Vec<&str> = patterns.split(',').collect();
patterns.dedup();
if let Some(index) = patterns.iter().position(|x| *x == "root") {
patterns.swap_remove(index);
let root = Root::new_open()?;
root.gitcall(opts[1..].to_vec())?;
}
let targets = glob_match(patterns, cluster.nodes.keys());
for target in &targets {
let node = cluster.get_node(target)?;
let node = Node::new_open(target, node)?;
node.gitcall(opts[1..].to_vec())?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn cli_verify_structure() {
Ocd::command().debug_assert();
}
}