Skip to content

Commit

Permalink
Branching (#1217)
Browse files Browse the repository at this point in the history
* 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 d1b0b25.

* cleanup

* fix default branch

---------

Co-authored-by: Aljaž Mur Eržen <[email protected]>
  • Loading branch information
quinchs and aljazerzen authored Mar 5, 2024
1 parent da67782 commit 9dd2217
Show file tree
Hide file tree
Showing 21 changed files with 547 additions and 86 deletions.
53 changes: 53 additions & 0 deletions src/branch/context.rs
Original file line number Diff line number Diff line change
@@ -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<Context> {
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<String> {
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<String> {
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
}
}
44 changes: 44 additions & 0 deletions src/branch/create.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
45 changes: 45 additions & 0 deletions src/branch/drop.rs
Original file line number Diff line number Diff line change
@@ -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 <branch>`"
);
}

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(())
}
29 changes: 29 additions & 0 deletions src/branch/list.rs
Original file line number Diff line number Diff line change
@@ -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<String> = 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(())
}
29 changes: 29 additions & 0 deletions src/branch/main.rs
Original file line number Diff line number Diff line change
@@ -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<Context> {
let project_dir = get_project_dir(None, true).await?.expect("Missing project");
Context::new(&project_dir).await
}
11 changes: 11 additions & 0 deletions src/branch/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
64 changes: 64 additions & 0 deletions src/branch/option.rs
Original file line number Diff line number Diff line change
@@ -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<String>,

#[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 {}
26 changes: 26 additions & 0 deletions src/branch/rename.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
33 changes: 33 additions & 0 deletions src/branch/switch.rs
Original file line number Diff line number Diff line change
@@ -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<String> = 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(())
}
30 changes: 30 additions & 0 deletions src/branch/wipe.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
5 changes: 4 additions & 1 deletion src/commands/cli.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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)
}
}
}
Loading

0 comments on commit 9dd2217

Please sign in to comment.