mirror of
https://codeberg.org/icewind/haze.git
synced 2026-06-03 09:04:12 +02:00
694 lines
21 KiB
Rust
694 lines
21 KiB
Rust
use crate::cloud::CloudOptions;
|
|
use crate::config::{HazeConfig, Preset};
|
|
use crate::service::{Service, ServiceTrait};
|
|
use miette::{IntoDiagnostic, Report, Result};
|
|
use std::fmt::{Debug, Display};
|
|
use std::rc::Rc;
|
|
use std::str::FromStr;
|
|
use strum::{
|
|
Display, EnumIter, EnumMessage, EnumProperty, EnumString, IntoEnumIterator, IntoStaticStr,
|
|
};
|
|
|
|
pub trait SubCommand: Display + EnumMessage + EnumProperty + Debug {
|
|
fn parent(&self) -> &'static str {
|
|
""
|
|
}
|
|
|
|
fn allows_filter(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn sub_commands(&self) -> Option<Box<dyn Iterator<Item = Box<dyn SubCommand>>>> {
|
|
None
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum HazeArgs {
|
|
List {
|
|
filter: Option<String>,
|
|
},
|
|
Start {
|
|
options: CloudOptions,
|
|
},
|
|
Stop {
|
|
filter: Option<String>,
|
|
},
|
|
Test {
|
|
options: CloudOptions,
|
|
args: Vec<String>,
|
|
},
|
|
Exec {
|
|
filter: Option<String>,
|
|
service: Option<ExecService>,
|
|
command: Vec<String>,
|
|
root: bool,
|
|
},
|
|
Occ {
|
|
filter: Option<String>,
|
|
command: Vec<String>,
|
|
},
|
|
Db {
|
|
filter: Option<String>,
|
|
root: bool,
|
|
command: Vec<String>,
|
|
index: Option<String>,
|
|
},
|
|
Clean,
|
|
Logs {
|
|
filter: Option<String>,
|
|
follow: bool,
|
|
service: Option<LogService>,
|
|
count: Option<usize>,
|
|
},
|
|
Open {
|
|
filter: Option<String>,
|
|
},
|
|
Fmt {
|
|
path: String,
|
|
},
|
|
Integration {
|
|
options: CloudOptions,
|
|
args: Vec<String>,
|
|
},
|
|
Shell {
|
|
options: CloudOptions,
|
|
command: Vec<String>,
|
|
},
|
|
Pin {
|
|
filter: Option<String>,
|
|
},
|
|
Unpin {
|
|
filter: Option<String>,
|
|
},
|
|
Proxy,
|
|
Git {
|
|
operation: GitOperation,
|
|
},
|
|
Env {
|
|
filter: Option<String>,
|
|
command: String,
|
|
args: Vec<String>,
|
|
},
|
|
Update,
|
|
Help {
|
|
command: Option<Rc<dyn SubCommand>>,
|
|
},
|
|
Version,
|
|
Edit {
|
|
filter: Option<String>,
|
|
path: String,
|
|
},
|
|
Reload {
|
|
filter: Option<String>,
|
|
},
|
|
}
|
|
|
|
#[derive(
|
|
Debug,
|
|
Clone,
|
|
Eq,
|
|
PartialEq,
|
|
Display,
|
|
EnumProperty,
|
|
EnumString,
|
|
IntoStaticStr,
|
|
EnumMessage,
|
|
EnumIter,
|
|
)]
|
|
#[strum(serialize_all = "lowercase")]
|
|
pub enum GitOperation {
|
|
/// Pull the active branch in all apps
|
|
Pull,
|
|
/// Checkout a branch in all apps
|
|
///
|
|
/// "main" and "master" can be used interchangeably.
|
|
#[strum(props(
|
|
Args = "[branch] branch to checkout, defaults to the branch matching the current server version [-v] verbose"
|
|
))]
|
|
Checkout {
|
|
branch: Option<String>,
|
|
verbose: bool,
|
|
},
|
|
}
|
|
|
|
impl SubCommand for GitOperation {
|
|
fn parent(&self) -> &'static str {
|
|
" git"
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
|
pub enum LogService {
|
|
Service(Service),
|
|
Database,
|
|
}
|
|
|
|
impl LogService {
|
|
pub fn from_type(presets: &[Preset], ty: &str) -> Option<Self> {
|
|
if ty == "db" {
|
|
return Some(LogService::Database);
|
|
}
|
|
Some(LogService::Service(
|
|
Service::from_type(presets, ty)?.into_iter().next()?,
|
|
))
|
|
}
|
|
|
|
pub fn container_name(&self, cloud_id: &str) -> Option<String> {
|
|
match self {
|
|
LogService::Database => Some(format!("{}-db", cloud_id)),
|
|
LogService::Service(service) => service.container_name(cloud_id),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
|
pub enum ExecService {
|
|
Db,
|
|
}
|
|
|
|
impl HazeArgs {
|
|
pub fn parse<I, S>(config: &HazeConfig, mut args: I) -> Result<HazeArgs>
|
|
where
|
|
S: AsRef<str> + Into<String> + Display,
|
|
I: Iterator<Item = S>,
|
|
{
|
|
let _bin = args.next();
|
|
let command_or_filter = match args.next() {
|
|
Some(s) => s,
|
|
None => return Ok(HazeArgs::List { filter: None }),
|
|
};
|
|
let (cmd, filter) = match HazeCommand::from_str(command_or_filter.as_ref()) {
|
|
Ok(cmd) => (cmd, None),
|
|
Err(_) => {
|
|
let cmd = match args.next() {
|
|
Some(cmd) => HazeCommand::from_str(cmd.as_ref()).into_diagnostic()?,
|
|
None => {
|
|
return Ok(HazeArgs::List {
|
|
filter: Some(command_or_filter.into()),
|
|
});
|
|
}
|
|
};
|
|
if !cmd.allows_filter() {
|
|
return Err(Report::msg(format!(
|
|
"{} doesn't allow specifying a filter",
|
|
cmd
|
|
)));
|
|
}
|
|
(cmd, Some(command_or_filter.into()))
|
|
}
|
|
};
|
|
|
|
match cmd {
|
|
HazeCommand::List => Ok(HazeArgs::List {
|
|
filter: filter.or_else(|| args.next().map(S::into)),
|
|
}),
|
|
HazeCommand::Start => {
|
|
let mut args = args.peekable();
|
|
let options = CloudOptions::parse(config, &mut args)?;
|
|
if let Some(leftover) = args.next() {
|
|
return Err(Report::msg(format!("unrecognized option {}", leftover)));
|
|
}
|
|
Ok(HazeArgs::Start { options })
|
|
}
|
|
HazeCommand::Stop => Ok(HazeArgs::Stop { filter }),
|
|
HazeCommand::Test => {
|
|
let mut args = args.peekable();
|
|
let options = CloudOptions::parse(config, &mut args)?;
|
|
let args = args.map(S::into).collect();
|
|
Ok(HazeArgs::Test { options, args })
|
|
}
|
|
HazeCommand::Integration => {
|
|
let mut args = args.peekable();
|
|
let options = CloudOptions::parse(config, &mut args)?;
|
|
let args = args.map(S::into).collect();
|
|
Ok(HazeArgs::Integration { options, args })
|
|
}
|
|
HazeCommand::Exec => {
|
|
let mut args = args.peekable();
|
|
|
|
let service = match args.peek() {
|
|
Some(arg) if arg.as_ref() == "db" => {
|
|
args.next();
|
|
Some(ExecService::Db)
|
|
}
|
|
_ => None,
|
|
};
|
|
|
|
let root = match args.peek() {
|
|
Some(arg) if arg.as_ref() == "root" => {
|
|
args.next();
|
|
true
|
|
}
|
|
_ => false,
|
|
};
|
|
|
|
let command = args.map(S::into).collect();
|
|
Ok(HazeArgs::Exec {
|
|
filter,
|
|
service,
|
|
command,
|
|
root,
|
|
})
|
|
}
|
|
HazeCommand::Occ => Ok(HazeArgs::Occ {
|
|
filter,
|
|
command: args.map(S::into).collect(),
|
|
}),
|
|
HazeCommand::Db => {
|
|
let mut args = args.peekable();
|
|
let root = if let Some(first) = args.peek() {
|
|
let root = first.as_ref() == "root";
|
|
if root {
|
|
let _ = args.next();
|
|
}
|
|
root
|
|
} else {
|
|
false
|
|
};
|
|
let index = if let Some(next) = args.peek() {
|
|
let all = next.as_ref() == "all";
|
|
let is_number = next.as_ref().chars().all(char::is_numeric);
|
|
if all || is_number {
|
|
args.next().map(Into::into)
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
let command = args.map(S::into).collect();
|
|
Ok(HazeArgs::Db {
|
|
filter,
|
|
root,
|
|
command,
|
|
index,
|
|
})
|
|
}
|
|
HazeCommand::Clean => Ok(HazeArgs::Clean),
|
|
HazeCommand::Logs => {
|
|
let mut args = args.peekable();
|
|
let follow = args.next_if(|arg| arg.as_ref() == "-f").is_some();
|
|
let service = args
|
|
.next_if(|arg| LogService::from_type(&config.preset, arg.as_ref()).is_some())
|
|
.and_then(|arg| LogService::from_type(&config.preset, arg.as_ref()));
|
|
Ok(HazeArgs::Logs {
|
|
filter,
|
|
follow,
|
|
service,
|
|
count: args
|
|
.next()
|
|
.map(|arg| arg.as_ref().parse())
|
|
.transpose()
|
|
.into_diagnostic()?,
|
|
})
|
|
}
|
|
HazeCommand::Open => Ok(HazeArgs::Open { filter }),
|
|
HazeCommand::Fmt => {
|
|
let path = args
|
|
.next()
|
|
.map(S::into)
|
|
.ok_or_else(|| Report::msg("No path provided"))?;
|
|
Ok(HazeArgs::Fmt { path })
|
|
}
|
|
HazeCommand::Shell => {
|
|
let mut args = args.peekable();
|
|
let options = CloudOptions::parse(config, &mut args)?;
|
|
let command = args.map(S::into).collect();
|
|
Ok(HazeArgs::Shell { options, command })
|
|
}
|
|
HazeCommand::Pin => Ok(HazeArgs::Pin { filter }),
|
|
HazeCommand::Unpin => Ok(HazeArgs::Unpin { filter }),
|
|
HazeCommand::Proxy => Ok(HazeArgs::Proxy),
|
|
HazeCommand::Checkout => {
|
|
let branch = args.next().map(S::into);
|
|
Ok(HazeArgs::Git {
|
|
operation: GitOperation::Checkout {
|
|
branch,
|
|
verbose: false,
|
|
},
|
|
})
|
|
}
|
|
HazeCommand::Git => {
|
|
let mut args = args.peekable();
|
|
let operation = args
|
|
.next()
|
|
.ok_or_else(|| Report::msg("No git operation provided"))?;
|
|
match operation.as_ref() {
|
|
"checkout" => {
|
|
let verbose = args.next_if(|arg| arg.as_ref() == "-v").is_some();
|
|
let branch = args.next().map(S::into);
|
|
let verbose = verbose | args.next_if(|arg| arg.as_ref() == "-v").is_some();
|
|
Ok(HazeArgs::Git {
|
|
operation: GitOperation::Checkout { branch, verbose },
|
|
})
|
|
}
|
|
"pull" => Ok(HazeArgs::Git {
|
|
operation: GitOperation::Pull,
|
|
}),
|
|
operation => Err(Report::msg(format!("Unknown git operation {operation}"))),
|
|
}
|
|
}
|
|
HazeCommand::Env => {
|
|
let mut args = args.map(S::into);
|
|
let command = args
|
|
.next()
|
|
.ok_or_else(|| Report::msg("No command provided"))?;
|
|
Ok(HazeArgs::Env {
|
|
filter,
|
|
command,
|
|
args: args.collect(),
|
|
})
|
|
}
|
|
HazeCommand::Update => Ok(HazeArgs::Update),
|
|
HazeCommand::Help => {
|
|
let command = args
|
|
.next()
|
|
.map(|command| match command.as_ref() {
|
|
"git" => match args.next() {
|
|
Some(op) => GitOperation::from_str(op.as_ref())
|
|
.map(|op| Rc::new(op) as Rc<dyn SubCommand>)
|
|
.map_err(|_| Report::msg("Unknown command")),
|
|
None => Ok(Rc::new(HazeCommand::Git) as Rc<dyn SubCommand>),
|
|
},
|
|
command => HazeCommand::from_str(command)
|
|
.map(|command| Rc::new(command) as Rc<dyn SubCommand>)
|
|
.map_err(|_| Report::msg("Unknown command")),
|
|
})
|
|
.transpose()?;
|
|
|
|
Ok(HazeArgs::Help { command })
|
|
}
|
|
HazeCommand::Version => Ok(HazeArgs::Version),
|
|
HazeCommand::Edit => Ok(HazeArgs::Edit {
|
|
filter,
|
|
path: args
|
|
.next()
|
|
.ok_or_else(|| Report::msg("No path provided"))?
|
|
.into(),
|
|
}),
|
|
HazeCommand::Reload => Ok(HazeArgs::Reload { filter }),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(
|
|
Debug,
|
|
Clone,
|
|
Copy,
|
|
Eq,
|
|
PartialEq,
|
|
Display,
|
|
IntoStaticStr,
|
|
EnumIter,
|
|
EnumString,
|
|
EnumMessage,
|
|
EnumProperty,
|
|
)]
|
|
#[strum(serialize_all = "lowercase")]
|
|
pub enum HazeCommand {
|
|
/// List all instances
|
|
List,
|
|
/// Start a new instance
|
|
#[strum(props(InstanceArgs = true))]
|
|
Start,
|
|
/// Stop an instance
|
|
Stop,
|
|
/// Run tests in a new instance
|
|
#[strum(props(
|
|
InstanceArgs = true,
|
|
Args = "[phpunit arguments] arguments to pass to phpunit"
|
|
))]
|
|
Test,
|
|
/// Run a command in an instance
|
|
#[strum(props(
|
|
Args = "[service] run command on a service container instead [command] command to run"
|
|
))]
|
|
Exec,
|
|
/// Run an occ command in an instance
|
|
#[strum(props(Args = "[occ arguments] arguments to pass to occ"))]
|
|
Occ,
|
|
/// Connect to the database of an instance
|
|
#[strum(props(
|
|
Args = "[root] connect to the database as root [db index] database instance to use for sharded setup [sql] sql command to run"
|
|
))]
|
|
Db,
|
|
/// Remove all non-pinned instances
|
|
Clean,
|
|
/// View the logs from an instance or service
|
|
#[strum(props(
|
|
Args = "[service] service to show logs from [follow] show logs lines as they appear [count] number of lines to show"
|
|
))]
|
|
Logs,
|
|
/// Open an instance in the browser
|
|
Open,
|
|
/// Run code formatting from a new instance
|
|
#[strum(props(Args = "[path] path to format"))]
|
|
Fmt,
|
|
/// Run integration tests in a new instance
|
|
#[strum(props(InstanceArgs = true, Args = "[args] arguments to pass to behat"))]
|
|
Integration,
|
|
/// Start a shell in an empirical instance
|
|
#[strum(props(InstanceArgs = true, Args = "[command] command to run"))]
|
|
Shell,
|
|
/// Pin an instance
|
|
Pin,
|
|
/// Unpin an instance
|
|
Unpin,
|
|
/// Start the proxy
|
|
Proxy,
|
|
/// Perform git operations on all apps
|
|
Git,
|
|
/// Deprecated, use `haze git checkout` instead
|
|
#[strum(props(Args = "[branch] branch to checkout"))]
|
|
Checkout,
|
|
/// Run command with notify_push environment variables
|
|
Env,
|
|
#[strum(props(Args = "[command] command to run with environment variables"))]
|
|
/// Update docker images
|
|
Update,
|
|
/// Show help text
|
|
#[strum(serialize = "--help", to_string = "help")]
|
|
Help,
|
|
/// Show version number
|
|
#[strum(serialize = "--version", to_string = "version")]
|
|
Version,
|
|
/// Edit a file in the instance with $EDITOR on the host
|
|
#[strum(props(Args = "[path] file to edit"))]
|
|
Edit,
|
|
/// Reload the php configuration in the instance
|
|
#[strum(props(
|
|
Details = "note: you can overwrite <yellow>php.ini</yellow> settings with <literal>haze</literal> <arg>[filter]</arg> <literal>edit /config/php.ini</literal>"
|
|
))]
|
|
Reload,
|
|
}
|
|
|
|
impl SubCommand for HazeCommand {
|
|
fn allows_filter(&self) -> bool {
|
|
matches!(
|
|
self,
|
|
HazeCommand::List
|
|
| HazeCommand::Stop
|
|
| HazeCommand::Exec
|
|
| HazeCommand::Occ
|
|
| HazeCommand::Db
|
|
| HazeCommand::Logs
|
|
| HazeCommand::Open
|
|
| HazeCommand::Pin
|
|
| HazeCommand::Unpin
|
|
| HazeCommand::Env
|
|
| HazeCommand::Edit
|
|
| HazeCommand::Reload
|
|
)
|
|
}
|
|
|
|
fn sub_commands(&self) -> Option<Box<dyn Iterator<Item = Box<dyn SubCommand>>>> {
|
|
match self {
|
|
HazeCommand::Git => Some(Box::new(GitOperation::iter().map(|op| Box::new(op) as _))),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_arg_parse() {
|
|
let config = HazeConfig {
|
|
sources_root: Default::default(),
|
|
app_directories: Default::default(),
|
|
work_dir: Default::default(),
|
|
auto_setup: Default::default(),
|
|
volume: vec![],
|
|
blackfire: None,
|
|
proxy: Default::default(),
|
|
preset: vec![],
|
|
};
|
|
|
|
fn assert_eq<A: Debug, B: Debug>(a: A, b: B) {
|
|
assert_eq!(format!("{a:?}"), format!("{b:?}"));
|
|
}
|
|
|
|
assert_eq(
|
|
HazeArgs::parse(&config, vec!["haze"].into_iter()).unwrap(),
|
|
HazeArgs::List { filter: None },
|
|
);
|
|
assert_eq(
|
|
HazeArgs::parse(&config, vec!["haze", "test"].into_iter()).unwrap(),
|
|
HazeArgs::Test {
|
|
options: Default::default(),
|
|
args: vec![],
|
|
},
|
|
);
|
|
assert_eq(
|
|
HazeArgs::parse(&config, vec!["haze", "asdasd"].into_iter()).unwrap(),
|
|
HazeArgs::List {
|
|
filter: Some("asdasd".to_string()),
|
|
},
|
|
);
|
|
assert_eq(
|
|
HazeArgs::parse(&config, vec!["haze", "asdasd", "db"].into_iter()).unwrap(),
|
|
HazeArgs::Db {
|
|
filter: Some("asdasd".to_string()),
|
|
root: false,
|
|
command: Vec::new(),
|
|
index: None,
|
|
},
|
|
);
|
|
assert_eq(
|
|
HazeArgs::parse(&config, vec!["haze", "asdasd", "db", "root"].into_iter()).unwrap(),
|
|
HazeArgs::Db {
|
|
filter: Some("asdasd".to_string()),
|
|
root: true,
|
|
command: Vec::new(),
|
|
index: None,
|
|
},
|
|
);
|
|
assert_eq(
|
|
HazeArgs::parse(
|
|
&config,
|
|
vec!["haze", "asdasd", "db", "select", "1"].into_iter(),
|
|
)
|
|
.unwrap(),
|
|
HazeArgs::Db {
|
|
filter: Some("asdasd".to_string()),
|
|
root: false,
|
|
command: vec!["select".to_string(), "1".to_string()],
|
|
index: None,
|
|
},
|
|
);
|
|
assert_eq(
|
|
HazeArgs::parse(
|
|
&config,
|
|
vec!["haze", "asdasd", "db", "root", "select 1"].into_iter(),
|
|
)
|
|
.unwrap(),
|
|
HazeArgs::Db {
|
|
filter: Some("asdasd".to_string()),
|
|
root: true,
|
|
command: vec!["select 1".to_string()],
|
|
index: None,
|
|
},
|
|
);
|
|
assert_eq(
|
|
HazeArgs::parse(
|
|
&config,
|
|
vec!["haze", "asdasd", "db", "root", "1", "select 1"].into_iter(),
|
|
)
|
|
.unwrap(),
|
|
HazeArgs::Db {
|
|
filter: Some("asdasd".to_string()),
|
|
root: true,
|
|
command: vec!["select 1".to_string()],
|
|
index: Some("1".into()),
|
|
},
|
|
);
|
|
assert_eq(
|
|
HazeArgs::parse(
|
|
&config,
|
|
vec!["haze", "asdasd", "db", "all", "select 1"].into_iter(),
|
|
)
|
|
.unwrap(),
|
|
HazeArgs::Db {
|
|
filter: Some("asdasd".to_string()),
|
|
root: false,
|
|
command: vec!["select 1".to_string()],
|
|
index: Some("all".into()),
|
|
},
|
|
);
|
|
assert_eq(
|
|
HazeArgs::parse(&config, vec!["haze", "exec", "foo", "bar"].into_iter()).unwrap(),
|
|
HazeArgs::Exec {
|
|
filter: None,
|
|
service: None,
|
|
command: vec!["foo".to_string(), "bar".to_string()],
|
|
root: false,
|
|
},
|
|
);
|
|
assert_eq(
|
|
HazeArgs::parse(
|
|
&config,
|
|
vec!["haze", "exec", "root", "foo", "bar"].into_iter(),
|
|
)
|
|
.unwrap(),
|
|
HazeArgs::Exec {
|
|
filter: None,
|
|
service: None,
|
|
command: vec!["foo".to_string(), "bar".to_string()],
|
|
root: true,
|
|
},
|
|
);
|
|
assert_eq(
|
|
HazeArgs::parse(
|
|
&config,
|
|
vec!["haze", "asdasd", "exec", "foo", "bar"].into_iter(),
|
|
)
|
|
.unwrap(),
|
|
HazeArgs::Exec {
|
|
filter: Some("asdasd".to_string()),
|
|
service: None,
|
|
command: vec!["foo".to_string(), "bar".to_string()],
|
|
root: false,
|
|
},
|
|
);
|
|
assert_eq(
|
|
HazeArgs::parse(
|
|
&config,
|
|
vec!["haze", "asdasd", "exec", "db", "foo", "bar"].into_iter(),
|
|
)
|
|
.unwrap(),
|
|
HazeArgs::Exec {
|
|
filter: Some("asdasd".to_string()),
|
|
service: Some(ExecService::Db),
|
|
command: vec!["foo".to_string(), "bar".to_string()],
|
|
root: false,
|
|
},
|
|
);
|
|
assert_eq(
|
|
HazeArgs::parse(&config, vec!["haze", "test", "foo", "bar"].into_iter()).unwrap(),
|
|
HazeArgs::Test {
|
|
options: Default::default(),
|
|
args: vec!["foo".into(), "bar".into()],
|
|
},
|
|
);
|
|
assert_eq(
|
|
HazeArgs::parse(&config, vec!["haze", "logs", "-f", "smb"].into_iter()).unwrap(),
|
|
HazeArgs::Logs {
|
|
filter: None,
|
|
follow: true,
|
|
service: Some(LogService::from_type(&[], "smb").unwrap()),
|
|
count: None,
|
|
},
|
|
);
|
|
assert_eq(
|
|
HazeArgs::parse(
|
|
&config,
|
|
vec!["haze", "asdasd", "logs", "smb", "123"].into_iter(),
|
|
)
|
|
.unwrap(),
|
|
HazeArgs::Logs {
|
|
filter: Some("asdasd".to_string()),
|
|
follow: false,
|
|
service: Some(LogService::from_type(&[], "smb").unwrap()),
|
|
count: Some(123),
|
|
},
|
|
);
|
|
}
|