From 78a44417c5a1008e8289d24fac684f9d8e308d8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alja=C5=BE=20Mur=20Er=C5=BEen?= Date: Fri, 31 Jan 2025 16:19:42 +0100 Subject: [PATCH] Hooks (#1449) --- .github/workflows/tests.yml | 2 + src/branch/merge.rs | 2 +- src/branch/rebase.rs | 4 +- src/branch/switch.rs | 15 +- src/branch/wipe.rs | 20 +- src/cli/install.rs | 2 +- src/commands/database.rs | 24 +-- src/hooks/mod.rs | 65 +++++++ src/main.rs | 1 + src/migrations/context.rs | 35 ++-- src/migrations/create.rs | 3 +- src/migrations/edit.rs | 4 +- src/migrations/extract.rs | 3 +- src/migrations/log.rs | 23 ++- src/migrations/merge.rs | 1 + src/migrations/migrate.rs | 59 +++--- src/migrations/rebase.rs | 1 + src/migrations/squash.rs | 2 +- src/migrations/status.rs | 2 +- src/migrations/upgrade_check.rs | 6 +- src/migrations/upgrade_format.rs | 3 +- src/portable/project/init.rs | 179 ++++++++++-------- src/portable/project/manifest.rs | 34 ++++ src/portable/project/mod.rs | 14 ++ src/portable/windows.rs | 19 +- src/process.rs | 13 +- src/watch/main.rs | 13 +- tests/portable_project.rs | 139 ++++++++++++++ tests/proj/project3/.gitignore | 1 + .../proj/project3/database_schema/default.gel | 6 + .../migrations/00001-m1mdwoy.edgeql | 7 + .../migrations/00002-m1bgeql.edgeql | 7 + tests/proj/project3/gel.toml | 15 ++ 33 files changed, 547 insertions(+), 177 deletions(-) create mode 100644 src/hooks/mod.rs create mode 100644 tests/proj/project3/.gitignore create mode 100644 tests/proj/project3/database_schema/default.gel create mode 100644 tests/proj/project3/database_schema/migrations/00001-m1mdwoy.edgeql create mode 100644 tests/proj/project3/database_schema/migrations/00002-m1bgeql.edgeql create mode 100644 tests/proj/project3/gel.toml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 98fcb9b69..ed6c84460 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -142,6 +142,8 @@ jobs: - run: | cargo test --workspace --test=${{ matrix.test }} --features portable_tests + env: + RUST_TEST_THREADS: '1' portable-tests-windows: needs: musl-test diff --git a/src/branch/merge.rs b/src/branch/merge.rs index f25a2d6be..0102d05c9 100644 --- a/src/branch/merge.rs +++ b/src/branch/merge.rs @@ -30,7 +30,7 @@ pub async fn main( None => anyhow::bail!("The branch '{}' doesn't exist", cmd.target_branch), }; - let migration_context = migrations::Context::for_project(&project)?; + let migration_context = migrations::Context::for_project(project)?; let mut merge_migrations = get_merge_migrations(source_connection, &mut target_connection).await?; diff --git a/src/branch/rebase.rs b/src/branch/rebase.rs index bd682f99b..bc1bee56a 100644 --- a/src/branch/rebase.rs +++ b/src/branch/rebase.rs @@ -36,7 +36,7 @@ pub async fn main( &temp_branch, source_connection, &mut temp_branch_connection, - &project, + project, cli_opts, !options.no_apply, ) @@ -84,7 +84,7 @@ async fn rebase( branch: &str, source_connection: &mut Connection, target_connection: &mut Connection, - project: &project::Context, + project: project::Context, cli_opts: &Options, apply_migrations: bool, ) -> anyhow::Result<()> { diff --git a/src/branch/switch.rs b/src/branch/switch.rs index 11c605280..2668f1817 100644 --- a/src/branch/switch.rs +++ b/src/branch/switch.rs @@ -1,8 +1,8 @@ -use crate::branch; use crate::branch::connections::connect_if_branch_exists; use crate::branch::context::Context; use crate::branch::create::create_branch; use crate::connect::Connector; +use crate::{branch, hooks, print}; pub async fn run( options: &Command, @@ -15,6 +15,10 @@ pub async fn run( anyhow::bail!(""); } + if let Some(project) = &context.get_project().await? { + hooks::on_action("branch.switch.before", project).await?; + } + let current_branch = if let Some(mut connection) = connect_if_branch_exists(connector).await? { let current_branch = context.get_current_branch(&mut connection).await?; if current_branch == options.target_branch { @@ -60,15 +64,20 @@ pub async fn run( } }; - eprintln!( + print::msg!( "Switching from '{}' to '{}'", - current_branch, options.target_branch + current_branch, + options.target_branch ); context .update_current_branch(&options.target_branch) .await?; + if let Some(project) = &context.get_project().await? { + hooks::on_action("branch.switch.after", project).await?; + } + Ok(branch::CommandResult { new_branch: Some(options.target_branch.clone()), }) diff --git a/src/branch/wipe.rs b/src/branch/wipe.rs index 9cf23b621..1b246eb27 100644 --- a/src/branch/wipe.rs +++ b/src/branch/wipe.rs @@ -3,11 +3,11 @@ use crate::branch::context::Context; use crate::commands::ExitCode; use crate::connect::Connector; use crate::portable::exit_codes; -use crate::{print, question}; +use crate::{hooks, print, question}; pub async fn main( cmd: &Command, - _context: &Context, + context: &Context, connector: &mut Connector, ) -> anyhow::Result<()> { let connection = connect_if_branch_exists(connector.branch(&cmd.target_branch)?).await?; @@ -30,10 +30,24 @@ pub async fn main( } } - let (status, _warnings) = connection.execute("RESET SCHEMA TO initial", &()).await?; + do_wipe(&mut connection, context).await?; + Ok(()) +} +pub async fn do_wipe( + connection: &mut crate::connect::Connection, + context: &Context, +) -> Result<(), anyhow::Error> { + if let Some(project) = context.get_project().await? { + hooks::on_action("branch.wipe.before", &project).await?; + } + + let (status, _warnings) = connection.execute("RESET SCHEMA TO initial", &()).await?; print::completion(status); + if let Some(project) = context.get_project().await? { + hooks::on_action("branch.wipe.after", &project).await?; + } Ok(()) } diff --git a/src/cli/install.rs b/src/cli/install.rs index ccc4b714c..9ed309a7a 100644 --- a/src/cli/install.rs +++ b/src/cli/install.rs @@ -472,7 +472,7 @@ fn try_project_init(new_layout: bool) -> anyhow::Result { server_start_conf: None, cloud_opts: options.clone(), }; - project::init::init_existing(&init, &project, &options)?; + project::init::init_existing(&init, project, &options)?; Ok(Initialized) } else { Ok(NotAProject) diff --git a/src/commands/database.rs b/src/commands/database.rs index 98fc27413..3d6b5a189 100644 --- a/src/commands/database.rs +++ b/src/commands/database.rs @@ -58,33 +58,35 @@ pub async fn drop( } pub async fn wipe( - cli: &mut Connection, - options: &WipeDatabase, - _: &Options, + connection: &mut Connection, + cmd: &WipeDatabase, + options: &Options, ) -> Result<(), anyhow::Error> { - if cli.get_version().await?.specific().major >= 5 { - eprintln!("'database wipe' is deprecated in {BRANDING} 5+. Please use 'branch wipe'"); + let context = crate::branch::context::Context::new(options).await?; + + if connection.get_version().await?.specific().major >= 5 { + print::warn!("'database wipe' is deprecated in {BRANDING} 5+. Please use 'branch wipe'"); } - if cli.get_version().await?.specific() < "3.0-alpha.2".parse().unwrap() { + if connection.get_version().await?.specific() < "3.0-alpha.2".parse().unwrap() { return Err(anyhow::anyhow!( "The `database wipe` command is only \ supported in {BRANDING} >= 3.0" )) .hint("Use `database drop`, `database create`")?; } - if !options.non_interactive { + if !cmd.non_interactive { let q = question::Confirm::new_dangerous(format!( "Do you really want to wipe \ the contents of the database {:?}?", - cli.database() + connection.database() )); - if !cli.ping_while(q.async_ask()).await? { + if !connection.ping_while(q.async_ask()).await? { print::error!("Canceled."); return Err(ExitCode::new(exit_codes::NOT_CONFIRMED).into()); } } - let (status, _warnings) = cli.execute("RESET SCHEMA TO initial", &()).await?; - print::completion(&status); + + crate::branch::wipe::do_wipe(connection, &context).await?; Ok(()) } diff --git a/src/hooks/mod.rs b/src/hooks/mod.rs new file mode 100644 index 000000000..436ed0cdc --- /dev/null +++ b/src/hooks/mod.rs @@ -0,0 +1,65 @@ +use crate::portable::{project, windows}; +use crate::print::{self, Highlight}; + +#[tokio::main(flavor = "current_thread")] +pub async fn on_action_sync( + action: &'static str, + project: &project::Context, +) -> anyhow::Result<()> { + on_action(action, project).await +} + +pub async fn on_action(action: &'static str, project: &project::Context) -> anyhow::Result<()> { + let Some(hook) = get_hook(action, &project.manifest) else { + return Ok(()); + }; + + print::msg!("{}", format!("hook {action}: {hook}").muted()); + + // run + let status = if !cfg!(windows) { + std::process::Command::new("/bin/sh") + .arg("-c") + .arg(hook) + .current_dir(&project.location.root) + .status()? + } else { + let wsl = windows::try_get_wsl()?; + wsl.sh(&project.location.root) + .arg("-c") + .arg(hook) + .run_for_status() + .await? + }; + + // abort on error + if !status.success() { + return Err(anyhow::anyhow!( + "Hook {action} exited with status {status}." + )); + } + Ok(()) +} + +fn get_hook<'m>( + action: &'static str, + manifest: &'m project::manifest::Manifest, +) -> Option<&'m str> { + let hooks = manifest.hooks.as_ref()?; + let hook = match action { + "project.init.before" => &hooks.project.as_ref()?.init.as_ref()?.before, + "project.init.after" => &hooks.project.as_ref()?.init.as_ref()?.after, + "branch.switch.before" => &hooks.branch.as_ref()?.switch.as_ref()?.before, + "branch.switch.after" => &hooks.branch.as_ref()?.switch.as_ref()?.after, + "branch.wipe.before" => &hooks.branch.as_ref()?.wipe.as_ref()?.before, + "branch.wipe.after" => &hooks.branch.as_ref()?.wipe.as_ref()?.after, + "migration.apply.before" => &hooks.migration.as_ref()?.apply.as_ref()?.before, + "migration.apply.after" => &hooks.migration.as_ref()?.apply.as_ref()?.after, + "migration.rebase.before" => &hooks.migration.as_ref()?.rebase.as_ref()?.before, + "migration.rebase.after" => &hooks.migration.as_ref()?.rebase.as_ref()?.after, + "migration.merge.before" => &hooks.migration.as_ref()?.merge.as_ref()?.before, + "migration.merge.after" => &hooks.migration.as_ref()?.merge.as_ref()?.after, + _ => panic!("unknown action"), + }; + hook.as_deref() +} diff --git a/src/main.rs b/src/main.rs index 88e7a1e5a..46a510299 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,6 +30,7 @@ mod error_display; mod format; mod highlight; mod hint; +mod hooks; mod interactive; mod interrupt; mod log_levels; diff --git a/src/migrations/context.rs b/src/migrations/context.rs index daea1db8d..d03e9f95a 100644 --- a/src/migrations/context.rs +++ b/src/migrations/context.rs @@ -3,43 +3,48 @@ use std::path::PathBuf; use crate::migrations::options::MigrationConfig; use crate::portable::project; -use gel_tokio::get_project_path; - pub struct Context { pub schema_dir: PathBuf, pub quiet: bool, + + pub project: Option, } impl Context { - pub async fn from_project_or_config( + pub async fn for_migration_config( cfg: &MigrationConfig, quiet: bool, ) -> anyhow::Result { + let project = project::load_ctx(None).await?; + let schema_dir = if let Some(schema_dir) = &cfg.schema_dir { schema_dir.clone() - } else if let Some(manifest_path) = get_project_path(None, true).await? { - let config = project::manifest::read(&manifest_path)?; - config - .project() - .resolve_schema_dir(manifest_path.parent().unwrap())? + } else if let Some(project) = &project { + project.resolve_schema_dir()? } else { let default_dir: PathBuf = "./dbschema".into(); if !default_dir.exists() { - anyhow::bail!("`dbschema` directory doesn't exist. Either create one or provide path via --schema-dir."); + anyhow::bail!("`dbschema` directory doesn't exist. Either create one, init a project or provide its path via --schema-dir."); } default_dir }; - Ok(Context { schema_dir, quiet }) + Ok(Context { + schema_dir, + quiet, + project, + }) } - pub fn for_project(project: &project::Context) -> anyhow::Result { + pub fn for_project(project: project::Context) -> anyhow::Result { + let schema_dir = project + .manifest + .project() + .resolve_schema_dir(&project.location.root)?; Ok(Context { - schema_dir: project - .manifest - .project() - .resolve_schema_dir(&project.location.root)?, + schema_dir, quiet: false, + project: Some(project), }) } } diff --git a/src/migrations/create.rs b/src/migrations/create.rs index 514ba767a..8337d93a3 100644 --- a/src/migrations/create.rs +++ b/src/migrations/create.rs @@ -820,7 +820,7 @@ async fn _create( options: &Options, create: &CreateMigration, ) -> anyhow::Result<()> { - let ctx = Context::from_project_or_config(&create.cfg, false).await?; + let ctx = Context::for_migration_config(&create.cfg, false).await?; if dev_mode::check_client(cli).await? { let dev_num = query_row::( @@ -1030,6 +1030,7 @@ async fn start_migration() { let ctx = Context { schema_dir, quiet: false, + project: None, }; let res = gen_start_migration(&ctx).await.unwrap(); diff --git a/src/migrations/edit.rs b/src/migrations/edit.rs index c8e0d6c47..572aeaf7b 100644 --- a/src/migrations/edit.rs +++ b/src/migrations/edit.rs @@ -73,7 +73,7 @@ pub async fn edit_no_check( _common: &Options, options: &MigrationEdit, ) -> Result<(), anyhow::Error> { - let ctx = Context::from_project_or_config(&options.cfg, false).await?; + let ctx = Context::for_migration_config(&options.cfg, false).await?; // TODO(tailhook) do we have to make the full check of whether there are no // gaps and parent revisions are okay? let (_n, path) = read_names(&ctx) @@ -139,7 +139,7 @@ async fn _edit( _common: &Options, options: &MigrationEdit, ) -> anyhow::Result<()> { - let ctx = Context::from_project_or_config(&options.cfg, false).await?; + let ctx = Context::for_migration_config(&options.cfg, false).await?; // TODO(tailhook) do we have to make the full check of whether there are no // gaps and parent revisions are okay? let (n, path) = cli diff --git a/src/migrations/extract.rs b/src/migrations/extract.rs index afca4dd3a..93a410634 100644 --- a/src/migrations/extract.rs +++ b/src/migrations/extract.rs @@ -46,7 +46,7 @@ pub async fn extract( _opts: &Options, params: &ExtractMigrations, ) -> anyhow::Result<()> { - let src_ctx = Context::from_project_or_config(¶ms.cfg, params.non_interactive).await?; + let src_ctx = Context::for_migration_config(¶ms.cfg, params.non_interactive).await?; let current = migration::read_all(&src_ctx, false).await?; let mut disk_iter = current.into_iter(); @@ -56,6 +56,7 @@ pub async fn extract( let temp_ctx = Context { schema_dir: temp_dir.path().to_path_buf(), quiet: false, + project: None, }; let mut to_delete = Vec::new(); diff --git a/src/migrations/log.rs b/src/migrations/log.rs index bceafd270..c1f8950af 100644 --- a/src/migrations/log.rs +++ b/src/migrations/log.rs @@ -3,6 +3,7 @@ use crate::connect::Connection; use crate::migrations::context::Context; use crate::migrations::options::MigrationLog; use crate::migrations::{db_migration, migration}; +use crate::print::Highlight; pub async fn log( cli: &mut Connection, @@ -35,16 +36,7 @@ async fn _log_db( options: &MigrationLog, ) -> Result<(), anyhow::Error> { let migrations = db_migration::read_all(cli, false, false).await?; - let limit = options.limit.unwrap_or(migrations.len()); - if options.newest_first { - for rev in migrations.iter().rev().take(limit) { - println!("{}", rev.0); - } - } else { - for rev in migrations.iter().take(limit) { - println!("{}", rev.0); - } - } + print(&migrations, options); Ok(()) } @@ -56,8 +48,13 @@ pub async fn log_fs(common: &Options, options: &MigrationLog) -> Result<(), anyh async fn log_fs_async(_common: &Options, options: &MigrationLog) -> Result<(), anyhow::Error> { assert!(options.from_fs); - let ctx = Context::from_project_or_config(&options.cfg, false).await?; + let ctx = Context::for_migration_config(&options.cfg, false).await?; let migrations = migration::read_all(&ctx, true).await?; + print(&migrations, options); + Ok(()) +} + +fn print(migrations: &indexmap::IndexMap, options: &MigrationLog) { let limit = options.limit.unwrap_or(migrations.len()); if options.newest_first { for rev in migrations.keys().rev().take(limit) { @@ -68,5 +65,7 @@ async fn log_fs_async(_common: &Options, options: &MigrationLog) -> Result<(), a println!("{rev}"); } } - Ok(()) + if migrations.is_empty() { + println!("{}", "".muted()); + } } diff --git a/src/migrations/merge.rs b/src/migrations/merge.rs index a8fba01ac..836d2c8dd 100644 --- a/src/migrations/merge.rs +++ b/src/migrations/merge.rs @@ -149,6 +149,7 @@ pub async fn write_merge_migrations( let temp_ctx = Context { schema_dir: temp_dir.path().to_path_buf(), quiet: false, + project: None, }; for (_, migration) in migrations.flatten() { diff --git a/src/migrations/migrate.rs b/src/migrations/migrate.rs index 600db9fbf..df177ae12 100644 --- a/src/migrations/migrate.rs +++ b/src/migrations/migrate.rs @@ -17,6 +17,7 @@ use crate::commands::Options; use crate::connect::{Connection, ResponseStream}; use crate::error_display::print_query_error; use crate::hint::HintExt; +use crate::hooks; use crate::migrations::context::Context; use crate::migrations::db_migration; use crate::migrations::db_migration::{DBMigration, MigrationGeneratedBy}; @@ -89,21 +90,17 @@ impl<'a> AsOperations for Vec> { pub async fn migrate( cli: &mut Connection, - options: &Options, + _options: &Options, migrate: &Migrate, ) -> Result<(), anyhow::Error> { let old_state = cli.set_ignore_error_state(); - let res = _migrate(cli, options, migrate).await; + let res = do_migrate(cli, migrate).await; cli.restore_state(old_state); res } -async fn _migrate( - cli: &mut Connection, - _options: &Options, - migrate: &Migrate, -) -> Result<(), anyhow::Error> { - let ctx = Context::from_project_or_config(&migrate.cfg, migrate.quiet).await?; +async fn do_migrate(cli: &mut Connection, migrate: &Migrate) -> Result<(), anyhow::Error> { + let ctx = Context::for_migration_config(&migrate.cfg, migrate.quiet).await?; if migrate.dev_mode { // TODO(tailhook) figure out progressbar in non-quiet mode return dev_mode::migrate(cli, &ctx, &ProgressBar::hidden()).await; @@ -440,30 +437,40 @@ pub async fn apply_migrations( ctx: &Context, single_transaction: bool, ) -> anyhow::Result<()> { + if let Some(project) = &ctx.project { + hooks::on_action("migration.apply.before", project).await?; + } + let old_timeout = timeout::inhibit_for_transaction(cli).await?; - async_try! { - async { - if single_transaction { - execute(cli, "START TRANSACTION", None).await?; - async_try! { - async { - apply_migrations_inner(cli, migrations, !ctx.quiet).await - }, - except async { - execute_if_connected(cli, "ROLLBACK").await - }, - else async { - execute(cli, "COMMIT", None).await + { + async_try! { + async { + if single_transaction { + execute(cli, "START TRANSACTION", None).await?; + async_try! { + async { + apply_migrations_inner(cli, migrations, !ctx.quiet).await + }, + except async { + execute_if_connected(cli, "ROLLBACK").await + }, + else async { + execute(cli, "COMMIT", None).await + } } + } else { + apply_migrations_inner(cli, migrations, !ctx.quiet).await } - } else { - apply_migrations_inner(cli, migrations, !ctx.quiet).await + }, + finally async { + timeout::restore_for_transaction(cli, old_timeout).await } - }, - finally async { - timeout::restore_for_transaction(cli, old_timeout).await } + }?; + if let Some(project) = &ctx.project { + hooks::on_action("migration.apply.after", project).await?; } + Ok(()) } pub async fn apply_migration( diff --git a/src/migrations/rebase.rs b/src/migrations/rebase.rs index e1e7d41f1..248f72ac5 100644 --- a/src/migrations/rebase.rs +++ b/src/migrations/rebase.rs @@ -262,6 +262,7 @@ pub async fn do_rebase( let temp_ctx = Context { schema_dir: temp_dir.path().to_path_buf(), quiet: false, + project: None, }; // write all the migrations to disk. diff --git a/src/migrations/squash.rs b/src/migrations/squash.rs index 7c57d5c27..dbafd89ec 100644 --- a/src/migrations/squash.rs +++ b/src/migrations/squash.rs @@ -33,7 +33,7 @@ pub async fn main( _options: &Options, create: &CreateMigration, ) -> anyhow::Result<()> { - let ctx = Context::from_project_or_config(&create.cfg, create.non_interactive).await?; + let ctx = Context::for_migration_config(&create.cfg, create.non_interactive).await?; let migrations = migration::read_all(&ctx, true).await?; let Some(db_rev) = migrations_applied(cli, &ctx, &migrations).await? else { return Err(ExitCode::new(3).into()); diff --git a/src/migrations/status.rs b/src/migrations/status.rs index 2379b459c..7d9668033 100644 --- a/src/migrations/status.rs +++ b/src/migrations/status.rs @@ -47,7 +47,7 @@ pub async fn status( _options: &Options, status: &ShowStatus, ) -> Result<(), anyhow::Error> { - let ctx = Context::from_project_or_config(&status.cfg, status.quiet).await?; + let ctx = Context::for_migration_config(&status.cfg, status.quiet).await?; let migrations = migration::read_all(&ctx, true).await?; match up_to_date_check(cli, &ctx, &migrations).await? { Some(_) if status.quiet => Ok(()), diff --git a/src/migrations/upgrade_check.rs b/src/migrations/upgrade_check.rs index 33ef4b16a..8dd24e4bb 100644 --- a/src/migrations/upgrade_check.rs +++ b/src/migrations/upgrade_check.rs @@ -60,7 +60,7 @@ pub fn upgrade_check(_options: &Options, options: &UpgradeCheck) -> anyhow::Resu break; } } - let ctx = Context::from_project_or_config(&options.cfg, false).await?; + let ctx = Context::for_migration_config(&options.cfg, false).await?; do_check(&ctx, &status_path, options.watch).await }) @@ -107,7 +107,7 @@ pub fn upgrade_check(_options: &Options, options: &UpgradeCheck) -> anyhow::Resu let runtime = tokio::runtime::Builder::new_multi_thread() .enable_all() .build()?; - let ctx = runtime.block_on(Context::from_project_or_config(&options.cfg, false))?; + let ctx = runtime.block_on(Context::for_migration_config(&options.cfg, false))?; spawn_and_check(&info, ctx, options.watch) } @@ -123,7 +123,7 @@ pub fn to_version(pkg: &PackageInfo, project: &project::Context) -> anyhow::Resu use crate::branding::BRANDING; let info = install::package(pkg).context(concatcp!("error installing ", BRANDING))?; - let ctx = Context::for_project(project)?; + let ctx = Context::for_project(project.clone())?; spawn_and_check(&info, ctx, false) } diff --git a/src/migrations/upgrade_format.rs b/src/migrations/upgrade_format.rs index 616bc1ffb..1b664a6d2 100644 --- a/src/migrations/upgrade_format.rs +++ b/src/migrations/upgrade_format.rs @@ -13,7 +13,7 @@ pub async fn upgrade_format( _opts: &Options, params: &MigrationUpgradeFormat, ) -> anyhow::Result<()> { - let ctx = Context::from_project_or_config(¶ms.cfg, false).await?; + let ctx = Context::for_migration_config(¶ms.cfg, false).await?; _upgrade_format(&ctx).await } @@ -77,6 +77,7 @@ mod test { let ctx = Context { schema_dir, quiet: false, + project: None, }; _upgrade_format(&ctx).await.unwrap(); diff --git a/src/portable/project/init.rs b/src/portable/project/init.rs index 632d1f423..ba748579a 100644 --- a/src/portable/project/init.rs +++ b/src/portable/project/init.rs @@ -15,7 +15,6 @@ use gel_errors::DuplicateDatabaseDefinitionError; use crate::branding::BRANDING_CLOUD; use crate::branding::QUERY_TAG; use crate::branding::{BRANDING, BRANDING_CLI_CMD, MANIFEST_FILE_DISPLAY_NAME}; -use crate::cloud; use crate::cloud::client::CloudClient; use crate::commands::ExitCode; use crate::connect::Connection; @@ -31,7 +30,6 @@ use crate::portable::options::InstanceName; use crate::portable::options::{CloudInstanceBillables, CloudInstanceParams}; use crate::portable::platform::optional_docker_check; use crate::portable::project; -use crate::portable::project::manifest; use crate::portable::repository::{self, Channel, PackageInfo, Query}; use crate::portable::server::install; use crate::portable::ver; @@ -40,6 +38,7 @@ use crate::portable::windows; use crate::print::{self, msg, Highlight}; use crate::question; use crate::table; +use crate::{cloud, hooks}; #[allow(clippy::collapsible_else_if)] pub fn run(options: &Command, opts: &crate::options::Options) -> anyhow::Result<()> { @@ -56,13 +55,13 @@ pub fn run(options: &Command, opts: &crate::options::Options) -> anyhow::Result< ); } - let project = project::find_project(options.project_dir.as_deref())?; + let project_loc = project::find_project(options.project_dir.as_deref())?; - if let Some(project) = project { + if let Some(project_loc) = project_loc { if options.link { - link(options, &project, &opts.cloud_options)?; + link(options, project_loc, &opts.cloud_options)?; } else { - init_existing(options, &project, &opts.cloud_options)?; + init_existing(options, project_loc, &opts.cloud_options)?; } } else { if options.link { @@ -72,16 +71,17 @@ pub fn run(options: &Command, opts: &crate::options::Options) -> anyhow::Result< a project, run `{BRANDING_CLI_CMD}` command without `--link` flag" ) } else { - let dir = options + let root = options .project_dir .clone() .unwrap_or_else(|| env::current_dir().unwrap()); - let config_path = dir.join(if cfg!(feature = "gel") { + let manifest = root.join(if cfg!(feature = "gel") { PROJECT_FILES[0] } else { PROJECT_FILES[1] }); - init_new(options, &dir, config_path, opts)?; + let location = project::Location { root, manifest }; + init_new(options, location, opts)?; } }; Ok(()) @@ -132,7 +132,7 @@ pub struct Command { pub fn init_existing( options: &Command, - project: &project::Location, + project: project::Location, cloud_options: &crate::options::CloudOptions, ) -> anyhow::Result { msg!( @@ -152,39 +152,42 @@ pub fn init_existing( anyhow::bail!("Project is already initialized."); } - let config = manifest::read(&project.manifest)?; - let schema_dir = config.project().resolve_schema_dir(&project.root)?; + let project = project::load_ctx_at(project)?; + let schema_dir = project + .manifest + .project() + .resolve_schema_dir(&project.location.root)?; let schema_files = project::find_schema_files(&schema_dir)?; let ver_query = if let Some(sver) = &options.server_version { sver.clone() } else { - config.instance.server_version + project.manifest.instance.server_version.clone() }; let mut client = CloudClient::new(cloud_options)?; - let (name, exists) = ask_name(&project.root, options, &mut client)?; + let (name, exists) = ask_name(&project.location.root, options, &mut client)?; if exists { - let mut inst = project::Handle::probe(&name, &project.root, &schema_dir, &client)?; + let mut inst = project::Handle::probe(&name, &project.location.root, &schema_dir, &client)?; let specific_version: &Specific = &inst.get_version()?.specific(); inst.check_version(&ver_query); if matches!(name, InstanceName::Cloud { .. }) { if options.non_interactive { inst.database = Some(options.database.clone().unwrap_or( - get_default_branch_or_database(specific_version, &project.root), + get_default_branch_or_database(specific_version, &project.location.root), )); } else { inst.database = Some(ask_database_or_branch( specific_version, - &project.root, + &project.location.root, options, )?); } } else { inst.database.clone_from(&options.database); } - return do_link(&inst, options, &stash_dir); + return do_link(&inst, &project, options, &stash_dir); } match &name { @@ -194,11 +197,17 @@ pub fn init_existing( let ver = cloud::versions::get_version(&ver_query, &client) .with_context(|| "could not initialize project")?; ver::print_version_hint(&ver, &ver_query); - let database = ask_database(&project.root, options)?; + let database = ask_database(&project.location.root, options)?; table::settings(&[ - ("Project directory", project.root.display().to_string()), - ("Project config", project.manifest.display().to_string()), + ( + "Project directory", + project.location.root.display().to_string(), + ), + ( + "Project config", + project.location.manifest.display().to_string(), + ), ( &format!( "Schema dir {}", @@ -229,8 +238,7 @@ pub fn init_existing( name.to_owned(), org_slug.to_owned(), &stash_dir, - &project.root, - &schema_dir, + &project, &ver, &database, options, @@ -271,8 +279,14 @@ pub fn init_existing( ); let mut rows: Vec<(&str, String)> = vec![ - ("Project directory", project.root.display().to_string()), - ("Project config", project.manifest.display().to_string()), + ( + "Project directory", + project.location.root.display().to_string(), + ), + ( + "Project config", + project.location.manifest.display().to_string(), + ), (schema_dir_key, schema_dir.display().to_string()), ("Installation method", meth), ("Version", pkg.version.to_string()), @@ -296,8 +310,7 @@ pub fn init_existing( name, &pkg, &stash_dir, - &project.root, - &schema_dir, + &project, &branch.unwrap_or(create::get_default_branch_name(specific_version)), options, ) @@ -309,8 +322,7 @@ fn do_init( name: &str, pkg: &PackageInfo, stash_dir: &Path, - project_dir: &Path, - schema_dir: &Path, + project: &project::Context, database: &str, options: &Command, ) -> anyhow::Result { @@ -391,16 +403,18 @@ fn do_init( let handle = project::Handle { name: name.into(), - project_dir: project_dir.into(), - schema_dir: schema_dir.into(), + project_dir: project.location.root.clone(), + schema_dir: project.resolve_schema_dir()?.into(), instance, database: options.database.clone(), }; - let mut stash = project::StashDir::new(project_dir, name); + let mut stash = project::StashDir::new(&project.location.root, name); stash.database = handle.database.as_deref(); stash.write(stash_dir)?; + hooks::on_action_sync("project.init.after", project)?; + if !options.no_migrations { migrate(&handle, false)?; } else { @@ -417,8 +431,7 @@ fn do_cloud_init( name: String, org: String, stash_dir: &Path, - project_dir: &Path, - schema_dir: &Path, + project: &project::Context, version: &ver::Specific, database: &str, options: &Command, @@ -439,17 +452,19 @@ fn do_cloud_init( let handle = project::Handle { name: full_name.clone(), - schema_dir: schema_dir.into(), + project_dir: project.location.root.clone(), + schema_dir: project.resolve_schema_dir()?.into(), instance: project::InstanceKind::Remote, - project_dir: project_dir.into(), database: Some(database.to_owned()), }; - let mut stash = project::StashDir::new(project_dir, &full_name); + let mut stash = project::StashDir::new(&project.location.root, &full_name); stash.cloud_profile = client.profile.as_deref().or(Some("default")); stash.database = handle.database.as_deref(); stash.write(stash_dir)?; + hooks::on_action_sync("project.init.after", project)?; + if !options.no_migrations { migrate(&handle, false)?; } else { @@ -464,7 +479,7 @@ fn do_cloud_init( fn link( options: &Command, - project: &project::Location, + project: project::Location, cloud_options: &crate::options::CloudOptions, ) -> anyhow::Result { msg!( @@ -483,8 +498,8 @@ fn link( anyhow::bail!("Project is already linked"); } - let manifest = manifest::read(&project.manifest)?; - let ver_query = &manifest.instance.server_version; + let project = project::load_ctx_at(project)?; + let ver_query = &project.manifest.instance.server_version; let mut client = CloudClient::new(cloud_options)?; let name = if let Some(name) = &options.server_instance { @@ -498,28 +513,29 @@ fn link( } else { ask_existing_instance_name(&mut client)? }; - let schema_dir = manifest.project().resolve_schema_dir(&project.root)?; - let mut inst = project::Handle::probe(&name, &project.root, &schema_dir, &client)?; + let schema_dir = project.resolve_schema_dir()?; + let mut inst = project::Handle::probe(&name, &project.location.root, &schema_dir, &client)?; if matches!(name, InstanceName::Cloud { .. }) { if options.non_interactive { inst.database = Some( options .database .clone() - .unwrap_or(directory_to_name(&project.root, "edgedb").to_owned()), + .unwrap_or(directory_to_name(&project.location.root, "edgedb").to_owned()), ) } else { - inst.database = Some(ask_database(&project.root, options)?); + inst.database = Some(ask_database(&project.location.root, options)?); } } else { inst.database.clone_from(&options.database); } inst.check_version(ver_query); - do_link(&inst, options, &stash_dir) + do_link(&inst, &project, options, &stash_dir) } fn do_link( inst: &project::Handle, + project: &project::Context, options: &Command, stash_dir: &Path, ) -> anyhow::Result { @@ -531,6 +547,8 @@ fn do_link( stash.database = inst.database.as_deref(); stash.write(stash_dir)?; + hooks::on_action_sync("project.init.after", project)?; + if !options.no_migrations { migrate(inst, !options.non_interactive)?; } else { @@ -567,16 +585,15 @@ fn directory_to_name(path: &Path, default: &str) -> String { fn init_new( options: &Command, - project_dir: &Path, - config_path: PathBuf, + location: project::Location, opts: &crate::options::Options, ) -> anyhow::Result { eprintln!( "No {MANIFEST_FILE_DISPLAY_NAME} found in `{}` or above", - project_dir.display() + location.root.display() ); - let stash_dir = get_stash_path(project_dir)?; + let stash_dir = get_stash_path(&location.root)?; if stash_dir.exists() { anyhow::bail!( "{MANIFEST_FILE_DISPLAY_NAME} deleted after \ @@ -597,15 +614,15 @@ fn init_new( } let schema_dir = Path::new("dbschema"); - let schema_dir_path = project_dir.join(schema_dir); + let schema_dir_path = location.root.join(schema_dir); let schema_files = project::find_schema_files(schema_dir)?; let mut client = CloudClient::new(&opts.cloud_options)?; - let (inst_name, exists) = ask_name(project_dir, options, &mut client)?; + let (inst_name, exists) = ask_name(&location.root, options, &mut client)?; if exists { let mut inst; - inst = project::Handle::probe(&inst_name, project_dir, schema_dir, &client)?; + inst = project::Handle::probe(&inst_name, &location.root, schema_dir, &client)?; let specific_version: &Specific = &inst.get_version()?.specific(); let version_query = Query::from_version(specific_version)?; @@ -613,28 +630,30 @@ fn init_new( instance: project::manifest::Instance { server_version: version_query, }, - project: Default::default(), + project: None, + hooks: None, }; - project::manifest::write(&config_path, &manifest)?; + project::manifest::write(&location.manifest, &manifest)?; + let ctx = project::Context { location, manifest }; if !schema_files { - project::write_schema_default(&schema_dir_path, &manifest.instance.server_version)?; + project::write_schema_default(&schema_dir_path, &ctx.manifest.instance.server_version)?; } if matches!(inst_name, InstanceName::Cloud { .. }) { if options.non_interactive { inst.database = Some(options.database.clone().unwrap_or( - get_default_branch_or_database(specific_version, project_dir), + get_default_branch_or_database(specific_version, &ctx.location.root), )); } else { inst.database = Some(ask_database_or_branch( specific_version, - project_dir, + &ctx.location.root, options, )?); } } else { inst.database.clone_from(&options.database); } - return do_link(&inst, options, &stash_dir); + return do_link(&inst, &ctx, options, &stash_dir); }; match &inst_name { @@ -644,10 +663,10 @@ fn init_new( let (ver_query, version) = ask_cloud_version(options, &client)?; ver::print_version_hint(&version, &ver_query); - let database = ask_database_or_branch(&version, project_dir, options)?; + let database = ask_database_or_branch(&version, &location.root, options)?; table::settings(&[ - ("Project directory", project_dir.display().to_string()), - ("Project config", config_path.display().to_string()), + ("Project directory", location.root.display().to_string()), + ("Project config", location.manifest.display().to_string()), ( &format!( "Schema dir {}", @@ -676,8 +695,10 @@ fn init_new( server_version: ver_query, }, project: Default::default(), + hooks: None, }; - project::manifest::write(&config_path, &manifest)?; + project::manifest::write(&location.manifest, &manifest)?; + let ctx = project::Context { location, manifest }; if !schema_files { project::write_schema_default(&schema_dir_path, &Query::from_version(&version)?)?; } @@ -686,8 +707,7 @@ fn init_new( name.to_owned(), org_slug.to_owned(), &stash_dir, - project_dir, - schema_dir, + &ctx, &version, &database, options, @@ -721,8 +741,8 @@ fn init_new( ); let mut rows: Vec<(&str, String)> = vec![ - ("Project directory", project_dir.display().to_string()), - ("Project config", config_path.display().to_string()), + ("Project directory", location.root.display().to_string()), + ("Project config", location.manifest.display().to_string()), (schema_dir_key, schema_dir_path.display().to_string()), ("Installation method", meth), ("Version", pkg.version.to_string()), @@ -740,9 +760,11 @@ fn init_new( server_version: ver_query, }, project: Default::default(), + hooks: None, }; - project::manifest::write(&config_path, &manifest)?; + project::manifest::write(&location.manifest, &manifest)?; + let project = project::Context { location, manifest }; if !schema_files { project::write_schema_default( &schema_dir_path, @@ -754,8 +776,7 @@ fn init_new( name, &pkg, &stash_dir, - project_dir, - schema_dir, + &project, &branch.unwrap_or(create::get_default_branch_name(specific_version)), options, ) @@ -1159,7 +1180,6 @@ async fn migrate(inst: &project::Handle<'_>, ask_for_running: bool) -> anyhow::R async fn migrate_async(inst: &project::Handle<'_>, ask_for_running: bool) -> anyhow::Result<()> { use crate::commands::Options; use crate::migrations::options::{Migrate, MigrationConfig}; - use Action::*; #[derive(Clone, Copy)] enum Action { @@ -1180,28 +1200,31 @@ async fn migrate_async(inst: &project::Handle<'_>, ask_for_running: bool) -> any "Cannot connect to instance {:?}. Options:", inst.name, )); - q.option("Start the service (if possible).", Service); + q.option("Start the service (if possible).", Action::Service); q.option( "Start in the foreground, \ apply migrations and shut down.", - Run, + Action::Run, + ); + q.option( + "Instance has been started manually, retry connect", + Action::Retry, ); - q.option("Instance has been started manually, retry connect", Retry); - q.option("Skip migrations.", Skip); + q.option("Skip migrations.", Action::Skip); match q.async_ask().await? { - Service => match start(inst) { + Action::Service => match start(inst) { Ok(()) => continue, Err(e) => { print::error!("{e}"); continue; } }, - Run => { + Action::Run => { run_and_migrate(inst)?; return Ok(()); } - Retry => continue, - Skip => { + Action::Retry => continue, + Action::Skip => { print::warn!("Skipping migrations."); msg!( "You can use `{BRANDING_CLI_CMD} migrate` to apply migrations \ diff --git a/src/portable/project/manifest.rs b/src/portable/project/manifest.rs index 471abd7de..cc65af41c 100644 --- a/src/portable/project/manifest.rs +++ b/src/portable/project/manifest.rs @@ -20,6 +20,7 @@ use crate::print::{self, msg, Highlight}; pub struct Manifest { pub instance: Instance, pub project: Option, + pub hooks: Option, } impl Manifest { @@ -60,6 +61,37 @@ impl Project { } } +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +pub struct Hooks { + pub project: Option, + pub branch: Option, + pub migration: Option, +} + +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +pub struct ProjectHooks { + pub init: Option, +} + +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +pub struct BranchHooks { + pub switch: Option, + pub wipe: Option, +} + +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +pub struct MigrationHooks { + pub apply: Option, + pub rebase: Option, + pub merge: Option, +} + +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +pub struct Hook { + pub before: Option, + pub after: Option, +} + #[context("error reading project config `{}`", path.display())] pub fn read(path: &Path) -> anyhow::Result { let text = fs::read_to_string(path)?; @@ -85,6 +117,7 @@ pub fn read(path: &Path) -> anyhow::Result { .and_then(|p| p.schema_dir) .map(|s| PathBuf::from(s.into_inner())), }), + hooks: val.hooks, }); } @@ -204,6 +237,7 @@ pub struct SrcManifest { #[serde(alias = "edgedb")] pub instance: SrcInstance, pub project: Option, + pub hooks: Option, #[serde(flatten)] pub extra: BTreeMap, } diff --git a/src/portable/project/mod.rs b/src/portable/project/mod.rs index 695ef10f9..d3a901439 100644 --- a/src/portable/project/mod.rs +++ b/src/portable/project/mod.rs @@ -351,6 +351,12 @@ pub async fn load_ctx(override_dir: Option<&Path>) -> anyhow::Result anyhow::Result { + let manifest = manifest::read(&location.manifest)?; + Ok(Context { location, manifest }) +} + #[tokio::main(flavor = "current_thread")] pub async fn ensure_ctx(override_dir: Option<&Path>) -> anyhow::Result { let Some(ctx) = load_ctx(override_dir).await? else { @@ -360,6 +366,14 @@ pub async fn ensure_ctx(override_dir: Option<&Path>) -> anyhow::Result Ok(ctx) } +impl Context { + pub fn resolve_schema_dir(&self) -> anyhow::Result { + self.manifest + .project() + .resolve_schema_dir(&self.location.root) + } +} + pub fn find_project_dirs_by_instance(name: &str) -> anyhow::Result> { find_project_stash_dirs("instance-name", |val| name == val, true) .map(|projects| projects.into_values().flatten().collect()) diff --git a/src/portable/windows.rs b/src/portable/windows.rs index 3847e7072..38d7463fb 100644 --- a/src/portable/windows.rs +++ b/src/portable/windows.rs @@ -97,12 +97,19 @@ impl Wsl { pro.no_proxy(); pro } - pub fn extension_loader(&self, path: &Path) -> process::Native { - let mut pro = process::Native::new("edgedb", "edgedb", "wsl"); + pub fn sh(&self, _current_dir: &Path) -> process::Native { + let mut pro = process::Native::new("sh", "sh", "wsl"); pro.arg("--user").arg("edgedb"); pro.arg("--distribution").arg(&self.distribution); - pro.arg(path); - pro.no_proxy(); + pro.arg("_EDGEDB_FROM_WINDOWS=1"); + if let Some(log_env) = env::var_os("RUST_LOG") { + let mut pair = OsString::with_capacity("RUST_LOG=".len() + log_env.len()); + pair.push("RUST_LOG="); + pair.push(log_env); + pro.arg(pair); + } + // TODO: set current dir + pro.arg("/bin/sh"); pro } #[cfg(windows)] @@ -608,7 +615,7 @@ fn get_wsl() -> anyhow::Result> { } } -fn try_get_wsl() -> anyhow::Result<&'static Wsl> { +pub fn try_get_wsl() -> anyhow::Result<&'static Wsl> { match WSL.get_or_try_init(|| get_wsl_distro(false)) { Ok(v) => Ok(v), Err(e) if e.is::() => Err(e).hint( @@ -674,7 +681,7 @@ pub fn server_cmd(instance: &str, _is_shutdown_supported: bool) -> anyhow::Resul .arg("-I") .arg(instance); let instance = String::from(instance); - pro.stop_process(move || { + pro.set_stop_process_command(move || { let mut cmd = tokio::process::Command::new("wsl"); cmd.arg("--user").arg("edgedb"); cmd.arg("--distribution").arg(&wsl.distribution); diff --git a/src/process.rs b/src/process.rs index 13f5351c6..3dee496a2 100644 --- a/src/process.rs +++ b/src/process.rs @@ -53,7 +53,7 @@ pub struct Native { program: OsString, args: Vec, envs: HashMap>, - stop_process: Option Command>>, + stop_process: Option Command + Send + Sync>>, marker: Cow<'static, str>, description: Cow<'static, str>, proxy: bool, @@ -356,6 +356,9 @@ impl Native { pub fn status(&mut self) -> anyhow::Result { block_on(self._run(false, false)).map(|out| out.status) } + pub async fn run_for_status(&mut self) -> anyhow::Result { + self._run(false, false).await.map(|x| x.status) + } async fn _run(&mut self, capture_out: bool, capture_err: bool) -> anyhow::Result { let term = interrupt::Interrupt::term(); @@ -750,10 +753,10 @@ impl Native { } } } - pub fn stop_process(&mut self, f: F) -> &mut Self - where - F: Fn() -> Command + 'static, - { + pub fn set_stop_process_command( + &mut self, + f: impl Fn() -> Command + Send + Sync + 'static, + ) -> &mut Self { self.stop_process = Some(Box::new(f)); self } diff --git a/src/watch/main.rs b/src/watch/main.rs index 48153bf38..febda8add 100644 --- a/src/watch/main.rs +++ b/src/watch/main.rs @@ -56,12 +56,12 @@ pub fn watch(options: &Options, _watch: &WatchCommand) -> anyhow::Result<()> { let project = project::ensure_ctx(None)?; let mut ctx = WatchContext { connector: options.block_on_create_connector()?, - migration: migrations::Context::for_project(&project)?, + migration: migrations::Context::for_project(project)?, last_error: false, }; log::info!( "Initialized in project dir {}", - project.location.root.as_relative().display() + ctx.project().location.root.as_relative().display() ); let (tx, rx) = watch::channel(()); let mut watch = notify::recommended_watcher(move |res: Result<_, _>| { @@ -71,7 +71,7 @@ pub fn watch(options: &Options, _watch: &WatchCommand) -> anyhow::Result<()> { .ok(); tx.send(()).unwrap(); })?; - watch.watch(&project.location.root, RecursiveMode::NonRecursive)?; + watch.watch(&ctx.project().location.root, RecursiveMode::NonRecursive)?; watch.watch(&ctx.migration.schema_dir, RecursiveMode::Recursive)?; runtime.block_on(ctx.do_update())?; @@ -80,7 +80,7 @@ pub fn watch(options: &Options, _watch: &WatchCommand) -> anyhow::Result<()> { eprintln!(" Hint: Use `{BRANDING_CLI_CMD} migration create` and `{BRANDING_CLI_CMD} migrate --dev-mode` to apply changes once done."); eprintln!( "Monitoring {}.", - project.location.root.as_relative().display() + ctx.project().location.root.as_relative().display() ); let res = runtime.block_on(watch_loop(rx, &mut ctx)); runtime @@ -187,6 +187,11 @@ impl WatchContext { } Ok(()) } + fn project(&self) -> &project::Context { + // SAFETY: watch can only be run within projects. + // We create Self::migration using migration::Context::for_project + self.migration.project.as_ref().unwrap() + } } impl From for ErrorJson { diff --git a/tests/portable_project.rs b/tests/portable_project.rs index d531b51bf..816cbdb23 100644 --- a/tests/portable_project.rs +++ b/tests/portable_project.rs @@ -237,3 +237,142 @@ fn project_link_and_init() { .context("destroy-2", "should unlink and destroy project") .success(); } + +#[test] +#[cfg(not(target_os = "windows"))] +fn hooks() { + use std::{fs, path}; + + let branch_log_file = path::Path::new("tests/proj/project3/branch.log"); + fs::remove_file(branch_log_file).ok(); + + Command::new("edgedb") + .arg("--version") + .assert() + .context("version", "command-line version option") + .success() + .stdout(predicates::str::contains(EXPECTED_VERSION)); + + Command::new("edgedb") + .arg("instance") + .arg("create") + .arg("inst2") + .arg("default-branch-name") + .arg("--non-interactive") + .assert() + .context("instance-create", "") + .success(); + + Command::new("edgedb") + .current_dir("tests/proj/project3") + .arg("project") + .arg("init") + .arg("--link") + .arg("--server-instance=inst2") + .arg("--non-interactive") + .assert() + .context("project-init", "") + .success() + .stderr(ContainsHooks { + expected: &[ + "project.init.after", + "migration.apply.before", + "migration.apply.after", + ], + }); + + Command::new("edgedb") + .current_dir("tests/proj/project3") + .arg("branch") + .arg("switch") + .arg("--create") + .arg("--empty") + .arg("another") + .assert() + .context("branch-switch", "") + .success() + .stderr(ContainsHooks { + expected: &["branch.switch.before", "branch.switch.after"], + }); + + let branch_log = fs::read_to_string(branch_log_file).unwrap(); + assert_eq!(branch_log, "another\n"); + + Command::new("edgedb") + .current_dir("tests/proj/project3") + .arg("branch") + .arg("merge") + .arg("default-branch-name") + .assert() + .context("branch-merge", "") + .success() + .stderr(ContainsHooks { + expected: &["migration.apply.before", "migration.apply.after"], + }); + + Command::new("edgedb") + .current_dir("tests/proj/project3") + .arg("branch") + .arg("wipe") + .arg("another") + .arg("--non-interactive") + .assert() + .context("branch-wipe", "") + .success() + .stderr(ContainsHooks { + expected: &["branch.wipe.before", "branch.wipe.after"], + }); + + Command::new("edgedb") + .current_dir("tests/proj/project3") + .arg("branch") + .arg("switch") + .arg("default-branch-name") + .assert() + .context("branch-switch-2", "") + .success() + .stderr(ContainsHooks { + expected: &["branch.switch.before", "branch.switch.after"], + }); + + let branch_log = fs::read_to_string(branch_log_file).unwrap(); + assert_eq!(branch_log, "another\ndefault-branch-name\n"); +} + +#[derive(Debug)] +struct ContainsHooks<'a> { + expected: &'a [&'static str], +} + +impl<'a> predicates::Predicate for ContainsHooks<'a> { + fn eval(&self, variable: &str) -> bool { + let re = regex::RegexBuilder::new(r"^hook ([a-z.]+):") + .multi_line(true) + .build() + .unwrap(); + let found_hooks: Vec<_> = re + .captures_iter(variable) + .map(|c| c.extract::<1>().1[0]) + .collect(); + + self.expected == found_hooks.as_slice() + } +} + +impl<'a> predicates::reflection::PredicateReflection for ContainsHooks<'a> { + fn parameters<'b>( + &'b self, + ) -> Box> + 'b> { + let mut params = std::vec![]; + for e in self.expected { + params.push(predicates::reflection::Parameter::new("hook", e)); + } + Box::new(params.into_iter()) + } +} + +impl<'a> std::fmt::Display for ContainsHooks<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Debug::fmt(&self, f) + } +} diff --git a/tests/proj/project3/.gitignore b/tests/proj/project3/.gitignore new file mode 100644 index 000000000..ccbbfcd5d --- /dev/null +++ b/tests/proj/project3/.gitignore @@ -0,0 +1 @@ +branch.log diff --git a/tests/proj/project3/database_schema/default.gel b/tests/proj/project3/database_schema/default.gel new file mode 100644 index 000000000..5a83be230 --- /dev/null +++ b/tests/proj/project3/database_schema/default.gel @@ -0,0 +1,6 @@ +module default { + type Hello { + property world: str; + property foo: int64; + } +} diff --git a/tests/proj/project3/database_schema/migrations/00001-m1mdwoy.edgeql b/tests/proj/project3/database_schema/migrations/00001-m1mdwoy.edgeql new file mode 100644 index 000000000..7262e09af --- /dev/null +++ b/tests/proj/project3/database_schema/migrations/00001-m1mdwoy.edgeql @@ -0,0 +1,7 @@ +CREATE MIGRATION m1mdwoyrh5c677pkvx57hvsxlsqiycrhijh6wyytflykey5essjiba + ONTO initial +{ + CREATE TYPE default::Hello { + CREATE PROPERTY world: std::str; + }; +}; diff --git a/tests/proj/project3/database_schema/migrations/00002-m1bgeql.edgeql b/tests/proj/project3/database_schema/migrations/00002-m1bgeql.edgeql new file mode 100644 index 000000000..06b75155e --- /dev/null +++ b/tests/proj/project3/database_schema/migrations/00002-m1bgeql.edgeql @@ -0,0 +1,7 @@ +CREATE MIGRATION m1bgeql5ie4pvxxcs63vu5b5h74ft6qmfte2f4febgcwqzdbo4tusa + ONTO m1mdwoyrh5c677pkvx57hvsxlsqiycrhijh6wyytflykey5essjiba +{ + ALTER TYPE default::Hello { + CREATE PROPERTY foo: std::int64; + }; +}; diff --git a/tests/proj/project3/gel.toml b/tests/proj/project3/gel.toml new file mode 100644 index 000000000..92efca778 --- /dev/null +++ b/tests/proj/project3/gel.toml @@ -0,0 +1,15 @@ +[instance] +server-version = "nightly" + +[project] +schema-dir = "./database_schema" + +[hooks] +project.init.before = "true" +project.init.after = "true" +branch.switch.before = "true" +branch.switch.after = "edgedb branch current --plain >> branch.log" +branch.wipe.before = "true" +branch.wipe.after = "true" +migration.apply.before = "true" +migration.apply.after = "true"