From 9dd22178424040f9103019cf7a647a99002c142e Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Tue, 5 Mar 2024 17:20:44 -0400 Subject: [PATCH] Branching (#1217) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * init * remove redundant check * rename and list * remove quotes from branch list * remove unused code * cleanup some code * remove database prompt * sanitize new branch name, handle drop case * remove rebase and merge for now * generic parameter names * fix create * add --force to drop & rename * missing brackets * missing space too :D * fix borrow for --force on drop * quote source_branch * quote old_name & new_name in rename * rustfmt * clippy * rustfmt * fix the config settings tests * unify ConnectionOptions database and branch reading * Add a few docs and a minor hint update * debug * remove edgedb.auto.toml * Revert "debug" This reverts commit d1b0b2547e0ad6c0659473453f921aeb8aae66e9. * cleanup * fix default branch --------- Co-authored-by: Aljaž Mur Eržen --- src/branch/context.rs | 53 ++++++++++ src/branch/create.rs | 44 ++++++++ src/branch/drop.rs | 45 ++++++++ src/branch/list.rs | 29 ++++++ src/branch/main.rs | 29 ++++++ src/branch/mod.rs | 11 ++ src/branch/option.rs | 64 ++++++++++++ src/branch/rename.rs | 26 +++++ src/branch/switch.rs | 33 ++++++ src/branch/wipe.rs | 30 ++++++ src/commands/cli.rs | 5 +- src/credentials.rs | 13 +++ src/main.rs | 2 + src/options.rs | 31 +++++- src/portable/config.rs | 169 +++++++++++++++++++++---------- src/portable/create.rs | 20 ++-- src/portable/link.rs | 7 +- src/portable/options.rs | 4 - src/portable/project.rs | 9 +- tests/docker.rs | 1 + tests/docker_portable_wrapper.rs | 8 +- 21 files changed, 547 insertions(+), 86 deletions(-) create mode 100644 src/branch/context.rs create mode 100644 src/branch/create.rs create mode 100644 src/branch/drop.rs create mode 100644 src/branch/list.rs create mode 100644 src/branch/main.rs create mode 100644 src/branch/mod.rs create mode 100644 src/branch/option.rs create mode 100644 src/branch/rename.rs create mode 100644 src/branch/switch.rs create mode 100644 src/branch/wipe.rs diff --git a/src/branch/context.rs b/src/branch/context.rs new file mode 100644 index 00000000..9f2bc159 --- /dev/null +++ b/src/branch/context.rs @@ -0,0 +1,53 @@ +use std::fs; +use crate::portable::config::Config; +use std::path::{Path, PathBuf}; +use crate::credentials; +use crate::portable::options::InstanceName; +use crate::portable::project::{instance_name, stash_path}; + +pub struct Context { + pub project_config: Config, + pub project_branch: String, + pub branch: String, + + + project_dir: PathBuf, +} + +impl Context { + pub async fn new(project_dir: &Path) -> anyhow::Result { + let project_config = crate::portable::config::read(&project_dir.join("edgedb.toml"))?; + let project_branch = project_config.edgedb.branch.clone(); + + let path = credentials::path(&get_instance_name(project_dir)?)?; + let credentials = credentials::read(&path).await?; + + Ok(Context { + project_config, + branch: credentials.database.unwrap_or(project_branch.clone()), + project_branch, + project_dir: PathBuf::from(project_dir), + }) + } + + pub fn get_instance_name(&self) -> anyhow::Result { + get_instance_name(&self.project_dir) + } + + pub async fn update_branch(&self, branch: &String) -> anyhow::Result<()> { + let path = credentials::path(&self.get_instance_name()?)?; + + let mut credentials = credentials::read(&path).await?; + + credentials.database = Some(branch.clone()); + + credentials::write_async(&path, &credentials).await + } +} + +fn get_instance_name(project_dir: &Path) -> anyhow::Result { + match instance_name(&stash_path(&fs::canonicalize(project_dir)?)?)? { + InstanceName::Local(local) => Ok(local), + InstanceName::Cloud {name: _, org_slug: _} => anyhow::bail!("Cannot use instance-name branching on cloud") // yet + } +} diff --git a/src/branch/create.rs b/src/branch/create.rs new file mode 100644 index 00000000..78d8ca15 --- /dev/null +++ b/src/branch/create.rs @@ -0,0 +1,44 @@ +use crate::branch::context::Context; +use crate::branch::option::Create; +use crate::connect::Connection; +use crate::print; + +pub async fn main( + options: &Create, + context: &Context, + connection: &mut Connection, +) -> anyhow::Result<()> { + let source_branch = options + .from + .as_ref() + .unwrap_or(&context.branch); + + let query: String; + let branch_name = edgeql_parser::helpers::quote_name(&options.branch); + + if options.empty { + query = format!("create empty branch {}", branch_name) + } else { + let branch_type = match options { + _ if options.copy_data => "data", + _ => "schema", + }; + + query = format!( + "create {} branch {} from {}", + branch_type, + branch_name, + edgeql_parser::helpers::quote_name(source_branch) + ) + } + + eprintln!("Creating branch '{}'...", options.branch); + + let status = connection.execute(&query, &()).await?; + + print::completion(&status); + + context.update_branch(&branch_name.as_ref().to_string()).await?; + + Ok(()) +} diff --git a/src/branch/drop.rs b/src/branch/drop.rs new file mode 100644 index 00000000..866598f2 --- /dev/null +++ b/src/branch/drop.rs @@ -0,0 +1,45 @@ +use crate::branch::context::Context; +use crate::branch::option::Drop; +use crate::commands::ExitCode; +use crate::connect::Connection; +use crate::portable::exit_codes; +use crate::{print, question}; + +pub async fn main( + options: &Drop, + context: &Context, + connection: &mut Connection, +) -> anyhow::Result<()> { + if context.branch == options.branch { + anyhow::bail!( + "Dropping the currently active branch is not supported, please switch to a \ + different branch to drop this one with `edgedb branch switch `" + ); + } + + if !options.non_interactive { + let q = question::Confirm::new_dangerous(format!( + "Do you really want to drop the branch {:?}?", + options.branch + )); + if !connection.ping_while(q.async_ask()).await? { + print::error("Canceled."); + return Err(ExitCode::new(exit_codes::NOT_CONFIRMED).into()); + } + } + + let mut statement = format!( + "drop branch {}", + edgeql_parser::helpers::quote_name(&options.branch) + ); + + if options.force { + statement = format!("{} force", &statement); + } + + let status = connection.execute(&statement, &()).await?; + + print::completion(status); + + Ok(()) +} diff --git a/src/branch/list.rs b/src/branch/list.rs new file mode 100644 index 00000000..b5020367 --- /dev/null +++ b/src/branch/list.rs @@ -0,0 +1,29 @@ +use crate::branch::context::Context; +use crate::branch::option::List; +use crate::connect::Connection; +use crossterm::style::Stylize; + +pub async fn main( + _options: &List, + context: &Context, + connection: &mut Connection, +) -> anyhow::Result<()> { + let branches: Vec = connection + .query( + "SELECT (SELECT sys::Database FILTER NOT .builtin).name", + &(), + ) + .await?; + + for branch in branches { + if context.branch == branch { + println!("{} - Current", branch.green()); + } else if context.project_config.edgedb.branch == branch { + println!("{} - Project default", branch.blue()); + } else { + println!("{}", branch); + } + } + + Ok(()) +} diff --git a/src/branch/main.rs b/src/branch/main.rs new file mode 100644 index 00000000..29b54faa --- /dev/null +++ b/src/branch/main.rs @@ -0,0 +1,29 @@ +use crate::branch::context::Context; +use crate::branch::option::{BranchCommand, Command}; +use crate::branch::{create, drop, list, rename, switch, wipe}; +use crate::connect::Connection; +use crate::options::Options; + +use edgedb_tokio::get_project_dir; + +#[tokio::main] +pub async fn branch_main(options: &Options, cmd: &BranchCommand) -> anyhow::Result<()> { + let context = create_context().await?; + + let mut connection: Connection = options.create_connector().await?.connect().await?; + + // match commands that don't require a connection to run, then match the ones that do with a connection. + match &cmd.subcommand { + Command::Switch(switch) => switch::main(switch, &context, &mut connection).await, + Command::Create(create) => create::main(create, &context, &mut connection).await, + Command::Drop(drop) => drop::main(drop, &context, &mut connection).await, + Command::Wipe(wipe) => wipe::main(wipe, &context, &mut connection).await, + Command::List(list) => list::main(list, &context, &mut connection).await, + Command::Rename(rename) => rename::main(rename, &context, &mut connection).await, + } +} + +async fn create_context() -> anyhow::Result { + let project_dir = get_project_dir(None, true).await?.expect("Missing project"); + Context::new(&project_dir).await +} diff --git a/src/branch/mod.rs b/src/branch/mod.rs new file mode 100644 index 00000000..4fbb3f44 --- /dev/null +++ b/src/branch/mod.rs @@ -0,0 +1,11 @@ +mod context; +mod create; +mod drop; +mod list; +pub mod main; +pub mod option; +mod rename; +mod switch; +mod wipe; + +pub use main::branch_main; diff --git a/src/branch/option.rs b/src/branch/option.rs new file mode 100644 index 00000000..51e8a666 --- /dev/null +++ b/src/branch/option.rs @@ -0,0 +1,64 @@ +#[derive(clap::Args, Debug, Clone)] +pub struct BranchCommand { + #[command(subcommand)] + pub subcommand: Command, +} + +#[derive(clap::Subcommand, Clone, Debug)] +pub enum Command { + Create(Create), + Drop(Drop), + Wipe(Wipe), + Switch(Switch), + Rename(Rename), + List(List), +} + +#[derive(clap::Args, Debug, Clone)] +pub struct Create { + pub branch: String, + + #[arg(long)] + pub from: Option, + + #[arg(long, conflicts_with = "copy_data")] + pub empty: bool, + + #[arg(long)] + pub copy_data: bool, +} + +#[derive(clap::Args, Debug, Clone)] +pub struct Drop { + pub branch: String, + + #[arg(long)] + pub non_interactive: bool, + + #[arg(long)] + pub force: bool, +} + +#[derive(clap::Args, Debug, Clone)] +pub struct Wipe { + pub branch: String, + + #[arg(long)] + pub non_interactive: bool, +} + +#[derive(clap::Args, Debug, Clone)] +pub struct Switch { + pub branch: String, +} + +#[derive(clap::Args, Debug, Clone)] +pub struct Rename { + pub old_name: String, + pub new_name: String, + + #[arg(long)] + pub force: bool, +} +#[derive(clap::Args, Debug, Clone)] +pub struct List {} diff --git a/src/branch/rename.rs b/src/branch/rename.rs new file mode 100644 index 00000000..18d4e9bf --- /dev/null +++ b/src/branch/rename.rs @@ -0,0 +1,26 @@ +use crate::branch::context::Context; +use crate::branch::option::Rename; +use crate::connect::Connection; +use crate::print; + +pub async fn main( + options: &Rename, + _context: &Context, + connection: &mut Connection, +) -> anyhow::Result<()> { + let status = connection + .execute( + &format!( + "alter branch {0}{2} rename to {1}", + edgeql_parser::helpers::quote_name(&options.old_name), + edgeql_parser::helpers::quote_name(&options.new_name), + if options.force { " force" } else { "" } + ), + &(), + ) + .await?; + + print::completion(status); + + Ok(()) +} diff --git a/src/branch/switch.rs b/src/branch/switch.rs new file mode 100644 index 00000000..59f7822c --- /dev/null +++ b/src/branch/switch.rs @@ -0,0 +1,33 @@ +use crate::branch::context::Context; +use crate::branch::option::Switch; +use crate::connect::Connection; + + +pub async fn main( + options: &Switch, + context: &Context, + connection: &mut Connection, +) -> anyhow::Result<()> { + // verify the branch exists + let branches: Vec = connection + .query( + "SELECT (SELECT sys::Database FILTER NOT .builtin).name", + &(), + ) + .await?; + + if !branches.contains(&options.branch) { + anyhow::bail!("Branch '{}' doesn't exists", options.branch) + } + + println!( + "Switching from '{}' to '{}'", + context.branch, options.branch + ); + + context.update_branch(&options.branch).await?; + + println!("Now on '{}'", options.branch); + + Ok(()) +} diff --git a/src/branch/wipe.rs b/src/branch/wipe.rs new file mode 100644 index 00000000..94dd156f --- /dev/null +++ b/src/branch/wipe.rs @@ -0,0 +1,30 @@ +use crate::branch::context::Context; +use crate::branch::option::Wipe; +use crate::commands::ExitCode; +use crate::connect::Connection; +use crate::portable::exit_codes; +use crate::{print, question}; + +pub async fn main( + options: &Wipe, + _context: &Context, + connection: &mut Connection, +) -> anyhow::Result<()> { + if !options.non_interactive { + let q = question::Confirm::new_dangerous(format!( + "Do you really want to wipe \ + the contents of the branch {:?}?", + options.branch + )); + if !connection.ping_while(q.async_ask()).await? { + print::error("Canceled."); + return Err(ExitCode::new(exit_codes::NOT_CONFIRMED).into()); + } + } + + let status = connection.execute("RESET SCHEMA TO initial", &()).await?; + + print::completion(status); + + Ok(()) +} diff --git a/src/commands/cli.rs b/src/commands/cli.rs index f0994363..772ceb69 100644 --- a/src/commands/cli.rs +++ b/src/commands/cli.rs @@ -1,7 +1,7 @@ use is_terminal::IsTerminal; use crate::cli::directory_check; -use crate::cli; +use crate::{branch, cli}; use crate::cloud::main::cloud_main; use crate::commands::parser::Common; use crate::commands; @@ -94,6 +94,9 @@ pub fn main(options: &Options) -> Result<(), anyhow::Error> { } Command::Watch(c) => { watch::watch(options, c) + }, + Command::Branch(c) => { + branch::branch_main(options, c) } } } diff --git a/src/credentials.rs b/src/credentials.rs index 9c716da5..9350faac 100644 --- a/src/credentials.rs +++ b/src/credentials.rs @@ -48,6 +48,12 @@ pub fn all_instance_names() -> anyhow::Result> { pub async fn write(path: &Path, credentials: &Credentials) -> anyhow::Result<()> { + write_async(path, credentials).await?; + Ok(()) +} + +#[context("cannot write credentials file {}", path.display())] +pub async fn write_async(path: &Path, credentials: &Credentials) -> anyhow::Result<()> { use tokio::fs; fs::create_dir_all(path.parent().unwrap()).await?; @@ -57,6 +63,13 @@ pub async fn write(path: &Path, credentials: &Credentials) Ok(()) } +pub async fn read(path: &Path) -> anyhow::Result { + use tokio::fs; + + let text = fs::read_to_string(path).await?; + Ok(serde_json::from_str(&text)?) +} + pub fn maybe_update_credentials_file( config: &Config, ask: bool ) -> anyhow::Result<()> { diff --git a/src/main.rs b/src/main.rs index 8df1ea9d..90c60e66 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,6 +48,7 @@ mod tty_password; mod variables; mod version_check; mod watch; +mod branch; fn main() { match _main() { @@ -117,6 +118,7 @@ fn _main() -> anyhow::Result<()> { } let opt = Options::from_args_and_env()?; + opt.conn_options.validate(); let cfg = config::get_config(); let mut builder = env_logger::Builder::from_env( diff --git a/src/options.rs b/src/options.rs index ef750b6a..979b8625 100644 --- a/src/options.rs +++ b/src/options.rs @@ -15,6 +15,7 @@ use edgedb_cli_derive::IntoArgs; use crate::cli::options::CliCommand; use crate::cli; + use crate::cloud::options::CloudCommand; use crate::commands::ExitCode; use crate::commands::parser::Common; @@ -29,6 +30,7 @@ use crate::print; use crate::repl::OutputFormat; use crate::tty_password; use crate::watch::options::WatchCommand; +use crate::branch::option::BranchCommand; const MAX_TERM_WIDTH: usize = 100; const MIN_TERM_WIDTH: usize = 50; @@ -105,6 +107,13 @@ pub struct ConnectionOptions { #[arg(global=true)] pub database: Option, + /// Branch to connect with + #[arg(short='b', long, help_heading=Some(CONN_OPTIONS_GROUP))] + #[arg(value_hint=clap::ValueHint::Other)] // TODO complete database + #[arg(hide=true)] + #[arg(global=true)] + pub branch: Option, + /// Ask for password on terminal (TTY) #[arg(long, help_heading=Some(CONN_OPTIONS_GROUP))] #[arg(hide=true)] @@ -217,6 +226,18 @@ pub struct ConnectionOptions { pub connect_timeout: Option, } +impl ConnectionOptions { + pub(crate) fn get_branch(&self) -> Option<&String> { + self.database.as_ref().or(self.branch.as_ref()) + } + + pub(crate) fn validate(&self) { + if self.database.is_some() { + print::warn("database connection argument is deprecated in favour of 'branch'"); + } + } +} + #[derive(clap::Parser, Debug)] #[command(disable_version_flag=true)] pub struct HelpConnect { @@ -342,6 +363,8 @@ pub enum Command { /// Start a long-running process that watches for changes in schema files in /// a project's ``dbschema`` directory, applying them in real time. Watch(WatchCommand), + /// Manage branches + Branch(BranchCommand), } #[derive(clap::Args, Clone, Debug)] @@ -874,9 +897,6 @@ pub fn prepare_conn_params(opts: &Options) -> anyhow::Result { if let Some(user) = &tmp.user { bld.user(user)?; } - if let Some(database) = &tmp.database { - bld.database(database)?; - } if let Some(val) = tmp.wait_until_available { bld.wait_until_available(val); } @@ -886,6 +906,11 @@ pub fn prepare_conn_params(opts: &Options) -> anyhow::Result { if let Some(val) = &tmp.secret_key { bld.secret_key(val); } + + if let Some(branch) = tmp.get_branch() { + bld.database(branch)?; + } + load_tls_options(tmp, &mut bld)?; Ok(bld) } diff --git a/src/portable/config.rs b/src/portable/config.rs index 8cca7325..6002ae7a 100644 --- a/src/portable/config.rs +++ b/src/portable/config.rs @@ -4,14 +4,16 @@ use std::path::{Path, PathBuf}; use fn_error_context::context; +use toml::Spanned; + use crate::commands::ExitCode; +use crate::platform::tmp_file_path; use crate::portable::exit_codes; use crate::portable::repository::{Channel, Query}; -use crate::platform::tmp_file_path; use crate::print::{self, echo, Highlight}; #[derive(serde::Deserialize)] -#[serde(rename_all="kebab-case")] +#[serde(rename_all = "kebab-case")] pub struct SrcConfig { pub edgedb: SrcEdgedb, pub project: Option, @@ -20,16 +22,18 @@ pub struct SrcConfig { } #[derive(serde::Deserialize)] -#[serde(rename_all="kebab-case")] +#[serde(rename_all = "kebab-case")] pub struct SrcEdgedb { #[serde(default)] pub server_version: Option>, + #[serde(default)] + pub branch: Option>, #[serde(flatten)] pub extra: BTreeMap, } #[derive(serde::Deserialize)] -#[serde(rename_all="kebab-case")] +#[serde(rename_all = "kebab-case")] pub struct SrcProject { #[serde(default)] pub schema_dir: Option, @@ -37,7 +41,6 @@ pub struct SrcProject { pub extra: BTreeMap, } - #[derive(Debug)] pub struct Config { pub edgedb: Edgedb, @@ -47,25 +50,28 @@ pub struct Config { #[derive(Debug)] pub struct Edgedb { pub server_version: Query, + pub branch: String, } #[derive(Debug)] pub struct Project { - pub schema_dir: PathBuf + pub schema_dir: PathBuf, } -fn warn_extra(extra: &BTreeMap, prefix: &str) { +pub fn warn_extra(extra: &BTreeMap, prefix: &str) { for key in extra.keys() { - log::warn!("Unknown config option `{}{}`", - prefix, key.escape_default()); + log::warn!("Unknown config option `{}{}`", prefix, key.escape_default()); } } pub fn format_config(version: &Query) -> String { - return format!("\ + return format!( + "\ [edgedb]\n\ server-version = {:?}\n\ - ", version.as_config_value()) + ", + version.as_config_value() + ); } #[context("error reading project config `{}`", path.display())] @@ -78,76 +84,119 @@ pub fn read(path: &Path) -> anyhow::Result { return Ok(Config { edgedb: Edgedb { - server_version: val.edgedb.server_version + server_version: val + .edgedb + .server_version .map(|x| x.into_inner()) .unwrap_or(Query { channel: Channel::Stable, version: None, }), + branch: val + .edgedb + .branch + .map(|x| x.into_inner()) + .unwrap_or("main".to_string()), }, - project: Project{ - schema_dir: val.project + project: Project { + schema_dir: val + .project .map(|p| p.schema_dir) .flatten() .map(|s| s.into()) - .unwrap_or_else(|| { - path.parent().unwrap_or(&Path::new("")) - .join("dbschema") - }) + .unwrap_or_else(|| path.parent().unwrap_or(&Path::new("")).join("dbschema")), }, - }) + }); } -fn toml_set_version(data: &str, version: &Query) - -> anyhow::Result> +/// Modify a field in a config of type `Cfg` that was deserialized from `input`. +/// The field is selected with the `selector` function. +pub fn modify_config( + parsed: &Cfg, + input: &str, + selector: Selector, + field_name: &'static str, + value: &Val, + to_str: ToStr, +) -> anyhow::Result> +where + Selector: Fn(&Cfg) -> &Option>, + Val: std::cmp::PartialEq, + ToStr: FnOnce(&Val) -> String, { use std::fmt::Write; - let mut toml = toml::de::Deserializer::new(&data); - let parsed: SrcConfig = serde_path_to_error::deserialize(&mut toml)?; - if let Some(ver_position) = &parsed.edgedb.server_version { - if ver_position.get_ref() == version { + if let Some(selected) = selector(parsed) { + if selected.get_ref() == value { return Ok(None); } - let mut out = String::with_capacity(data.len() + 5); - write!(&mut out, "{}{:?}{}", - &data[..ver_position.start()], - version.as_config_value(), - &data[ver_position.end()..], - ).unwrap(); + + let mut out = String::with_capacity(input.len() + 5); + write!( + &mut out, + "{}{:?}{}", + &input[..selected.start()], + to_str(value), + &input[selected.end()..] + ) + .unwrap(); + return Ok(Some(out)); } - print::error("No server-version found in `edgedb.toml`."); - eprintln!("Please ensure that `edgedb.toml` contains:"); - println!(" {}", - format_config(version) - .lines() - .collect::>() - .join("\n ")); - return Err(ExitCode::new(exit_codes::INVALID_CONFIG).into()); + + print::error(format!("Invalid `edgedb.toml`: missing {}", field_name)); + Err(ExitCode::new(exit_codes::INVALID_CONFIG).into()) } -#[context("cannot modify `{}`", config.display())] -pub fn modify(config: &Path, ver: &Query) -> anyhow::Result { - let input = fs::read_to_string(&config)?; - if let Some(output) = toml_set_version(&input, ver)? { - echo!("Setting `server-version = ", - format_args!("{:?}", ver.as_config_value()).emphasize(), - "` in `edgedb.toml`"); - let tmp = tmp_file_path(config); +pub fn read_modify_write( + path: &Path, + selector: Selector, + field_name: &'static str, + value: &Val, + to_str: ToStr, +) -> anyhow::Result +where + Cfg: for<'de> serde::Deserialize<'de>, + Selector: Fn(&Cfg) -> &Option>, + Val: std::cmp::PartialEq, + ToStr: FnOnce(&Val) -> String, +{ + let input = fs::read_to_string(&path)?; + let mut deserializer = toml::de::Deserializer::new(&input); + let parsed: Cfg = serde_path_to_error::deserialize(&mut deserializer)?; + + if let Some(new_contents) = modify_config(&parsed, &input, selector, field_name, value, to_str)? + { + let tmp = tmp_file_path(path); fs::remove_file(&tmp).ok(); - fs::write(&tmp, output)?; - fs::rename(&tmp, config)?; + fs::write(&tmp, new_contents)?; + fs::rename(&tmp, path)?; + Ok(true) } else { Ok(false) } } +#[context("cannot modify `{}`", config.display())] +pub fn modify_server_ver(config: &Path, ver: &Query) -> anyhow::Result { + echo!( + "Setting `server-version = ", + format_args!("{:?}", ver.as_config_value()).emphasize(), + "` in `edgedb.toml`" + ); + read_modify_write( + config, + |v: &SrcConfig| &v.edgedb.server_version, + "server-version", + ver, + Query::as_config_value, + ) +} + #[cfg(test)] mod test { use test_case::test_case; - use super::toml_set_version; const TOML_BETA1: &str = "\ [edgedb]\n\ @@ -234,6 +283,21 @@ mod test { edgedb = {server-version = \"nightly\"}\n\ project = {schema-dir = \"custom-dir\"}\n\ "; + + fn set_toml_version(data: &str, version: &super::Query) -> anyhow::Result> { + let mut toml = toml::de::Deserializer::new(&data); + let parsed: super::SrcConfig = serde_path_to_error::deserialize(&mut toml)?; + + super::modify_config( + &parsed, + data, + |v: &super::SrcConfig| &v.edgedb.server_version, + "server-version", + version, + super::Query::as_config_value, + ) + } + #[test_case(TOML_BETA1, "1.0-beta.2" => Some(TOML_BETA2.into()))] #[test_case(TOML_BETA2, "1.0-beta.2" => None)] #[test_case(TOML_NIGHTLY, "1.0-beta.2" => Some(TOML_BETA2.into()))] @@ -245,7 +309,6 @@ mod test { #[test_case(TOML_BETA2_CUSTOM_SCHEMA_DIR, "nightly" => Some(TOML_NIGHTLY_CUSTOM_SCHEMA_DIR.into()))] #[test_case(TOML_NIGHTLY, "nightly" => None)] #[test_case(TOML_2_3, "=2.3" => Some(TOML_2_3_EXACT.into()))] - #[test_case(TOML2_BETA1, "1.0-beta.2" => Some(TOML2_BETA2.into()))] #[test_case(TOML2_BETA2, "1.0-beta.2" => None)] #[test_case(TOML2_NIGHTLY, "1.0-beta.2" => Some(TOML2_BETA2.into()))] @@ -256,7 +319,6 @@ mod test { #[test_case(TOML2_BETA2, "nightly" => Some(TOML2_NIGHTLY.into()))] #[test_case(TOML2_BETA2_CUSTOM_SCHEMA_DIR, "nightly" => Some(TOML2_NIGHTLY_CUSTOM_SCHEMA_DIR.into()))] #[test_case(TOML2_NIGHTLY, "nightly" => None)] - #[test_case(TOMLI_BETA1, "1.0-beta.2" => Some(TOMLI_BETA2.into()))] #[test_case(TOMLI_BETA2, "1.0-beta.2" => None)] #[test_case(TOMLI_NIGHTLY, "1.0-beta.2" => Some(TOMLI_BETA2.into()))] @@ -268,7 +330,6 @@ mod test { #[test_case(TOMLI_BETA2_CUSTOM_SCHEMA_DIR, "nightly"=> Some(TOMLI_NIGHTLY_CUSTOM_SCHEMA_DIR.into()))] #[test_case(TOMLI_NIGHTLY, "nightly" => None)] fn modify(src: &str, ver: &str) -> Option { - toml_set_version(src, &ver.parse().unwrap()).unwrap() + set_toml_version(src, &ver.parse().unwrap()).unwrap() } - } diff --git a/src/portable/create.rs b/src/portable/create.rs index 3806a6f9..fdf52652 100644 --- a/src/portable/create.rs +++ b/src/portable/create.rs @@ -123,7 +123,7 @@ pub fn create(cmd: &Create, opts: &crate::options::Options) -> anyhow::Result<() let paths = Paths::get(&name)?; paths.check_exists() .with_context(|| format!("instance {:?} detected", name)) - .with_hint(|| format!("Use `edgedb destroy {}` \ + .with_hint(|| format!("Use `edgedb instance destroy -I {}` \ to remove rest of unused instance", name))?; @@ -154,8 +154,7 @@ pub fn create(cmd: &Create, opts: &crate::options::Options) -> anyhow::Result<() installation: Some(inst), port, }; - bootstrap(&paths, &info, - &cmd.default_database, &cmd.default_user)?; + bootstrap(&paths, &info, &cmd.default_user)?; info }; @@ -337,17 +336,11 @@ fn create_cloud( return Ok(()) } -pub fn bootstrap_script(database: &str, user: &str, password: &str) -> String { +fn bootstrap_script(user: &str, password: &str) -> String { use std::fmt::Write; use edgeql_parser::helpers::{quote_string, quote_name}; let mut output = String::with_capacity(1024); - if database != "edgedb" { - write!(&mut output, - "CREATE DATABASE {};", - quote_name(&database), - ).unwrap(); - } if user == "edgedb" { write!(&mut output, r###" ALTER ROLE {name} {{ @@ -370,8 +363,7 @@ pub fn bootstrap_script(database: &str, user: &str, password: &str) -> String { } #[context("cannot bootstrap EdgeDB instance")] -pub fn bootstrap(paths: &Paths, info: &InstanceInfo, - database: &str, user: &str) +pub fn bootstrap(paths: &Paths, info: &InstanceInfo, user: &str) -> anyhow::Result<()> { let server_path = info.server_path()?; @@ -385,7 +377,7 @@ pub fn bootstrap(paths: &Paths, info: &InstanceInfo, .with_context(|| format!("creating {:?}", &tmp_data))?; let password = generate_password(); - let script = bootstrap_script(database, user, &password); + let script = bootstrap_script(user, &password); echo!("Initializing EdgeDB instance..."); let mut cmd = process::Native::new("bootstrap", "edgedb", server_path); @@ -409,7 +401,7 @@ pub fn bootstrap(paths: &Paths, info: &InstanceInfo, let mut creds = Credentials::default(); creds.port = info.port; creds.user = user.into(); - creds.database = Some(database.into()); + creds.database = Some("edgedb".into()); creds.password = Some(password.into()); creds.tls_ca = Some(cert); credentials::write(&paths.credentials, &creds)?; diff --git a/src/portable/link.rs b/src/portable/link.rs index 89dbde44..c54baf70 100644 --- a/src/portable/link.rs +++ b/src/portable/link.rs @@ -344,10 +344,11 @@ async fn prompt_conn_params( .ask()? )?; } - if options.database.is_none() { + + if options.branch.is_none() { builder.database( - &question::String::new("Specify database name") - .default(config.database()) + &question::String::new("Specify branch") + .default("main") .ask()? )?; } diff --git a/src/portable/options.rs b/src/portable/options.rs index aea9422e..e38add2e 100644 --- a/src/portable/options.rs +++ b/src/portable/options.rs @@ -226,10 +226,6 @@ pub struct Create { #[arg(long, hide=true)] pub start_conf: Option, - /// Default database name (created during initialization and saved in - /// credentials file) - #[arg(long, default_value="edgedb")] - pub default_database: String, /// Default user name (created during initialization and saved in /// credentials file) #[arg(long, default_value="edgedb")] diff --git a/src/portable/project.rs b/src/portable/project.rs index fb8fd050..2ad8467e 100644 --- a/src/portable/project.rs +++ b/src/portable/project.rs @@ -706,7 +706,6 @@ fn do_init(name: &str, pkg: &PackageInfo, }, port: Some(port), start_conf: None, - default_database: "edgedb".into(), default_user: "edgedb".into(), non_interactive: true, cloud_opts: options.cloud_opts.clone(), @@ -724,7 +723,7 @@ fn do_init(name: &str, pkg: &PackageInfo, installation: Some(inst), port, }; - create::bootstrap(&paths, &info, "edgedb", "edgedb")?; + create::bootstrap(&paths, &info, "edgedb")?; match create::create_service(&info) { Ok(()) => {}, Err(e) => { @@ -1503,7 +1502,7 @@ fn search_for_unlink(base: &Path) -> anyhow::Result { } #[context("cannot read instance name of {:?}", stash_dir)] -fn instance_name(stash_dir: &Path) -> anyhow::Result { +pub fn instance_name(stash_dir: &Path) -> anyhow::Result { let inst = fs::read_to_string(&stash_dir.join("instance-name"))?; Ok(InstanceName::from_str(inst.trim())?) } @@ -1775,7 +1774,7 @@ pub fn update_toml( if !stash_dir.exists() { log::warn!("No associated instance found."); - if config::modify(&config_path, &query)? { + if config::modify_server_ver(&config_path, &query)? { print::success("Config updated successfully."); } else { print::success("Config is up to date."); @@ -1809,7 +1808,7 @@ pub fn update_toml( Query::from_version(&pkg_ver)? }; - if config::modify(&config_path, &config_version)? { + if config::modify_server_ver(&config_path, &config_version)? { echo!("Remember to commit it to version control."); } let name_str = name.to_string(); diff --git a/tests/docker.rs b/tests/docker.rs index 487b187b..5819ad81 100644 --- a/tests/docker.rs +++ b/tests/docker.rs @@ -1,3 +1,4 @@ +//! Utils for invoking docker via spawning new processes. #![allow(dead_code)] use std::env; diff --git a/tests/docker_portable_wrapper.rs b/tests/docker_portable_wrapper.rs index 3cafe6bd..3ca3e762 100644 --- a/tests/docker_portable_wrapper.rs +++ b/tests/docker_portable_wrapper.rs @@ -1,5 +1,9 @@ -#![cfg_attr(not(feature="test_docker_wrapper"), - allow(dead_code, unused_imports))] +//! Tests that compile the tests in the current environment +//! and then copy the test binaries to a docker container where they are executed. +//! +//! Note: these tests likely won't run on non-Ubuntu OSs, since the test binaries +//! might have dynamic library dependencies into the host system. +#![cfg_attr(not(feature="test_docker_wrapper"), allow(dead_code, unused_imports))] use std::path::{Path, PathBuf}; use std::collections::HashMap;