1
0
Fork 0
mirror of https://codeberg.org/icewind/haze.git synced 2026-10-01 08:44:09 +02:00

Merge pull request 'feat(start): Add --detach flag' (#34) from artonge/haze:artonge/feat/detach_arg into main

Reviewed-on: https://codeberg.org/icewind/haze/pulls/34
This commit is contained in:
Robin Appelman 2026-08-17 17:04:58 +02:00
commit ed78642194
9 changed files with 339 additions and 27 deletions

View file

@ -56,7 +56,7 @@ See the [configuration section](#configuration) for more options.
#### Start an instance #### Start an instance
```bash ```bash
haze start [--name <name>] [database] [php-version] [services] [vX.Y.Z] haze start [--name <name>] [--detach] [database] [php-version] [services] [vX.Y.Z]
``` ```
Where `database` is one of `sqlite`, `mysql`, `mariadb`, `pgsql` or `oracle` Where `database` is one of `sqlite`, `mysql`, `mariadb`, `pgsql` or `oracle`
@ -70,6 +70,10 @@ release instead of using the local sources.
Use `--name <name>` to give the instance a specific name instead of a randomly Use `--name <name>` to give the instance a specific name instead of a randomly
generated one. For example: generated one. For example:
Use `--detach` to give the instance
[its own sources](#instances-with-their-own-sources) instead of sharing
`sources_root` with all other instances.
Additionally, you can use the following options when starting an instance: Additionally, you can use the following options when starting an instance:
- `s3`: set up an S3 server and configure to Nextcloud to use it as primary - `s3`: set up an S3 server and configure to Nextcloud to use it as primary
@ -134,6 +138,24 @@ haze list
haze clean haze clean
``` ```
### Instances with their own sources
By default every instance shares the sources configured as `sources_root`, so
all instances always run the same code. An instance started with `--detach` gets
its own `git worktree` of `sources_root` instead, created in `worktree_dir`,
which lets you run several instances on different branches at the same time.
```bash
haze start --name my-fix --detach
```
The worktree is checked out with a detached head. Every app in one of the
`app_directories` that gets enabled during setup receives its own worktree as
well, other apps keep running from the shared app directory.
The worktrees are removed together with the instance, by `haze stop` or
`haze clean`.
## Controlling running instances ## Controlling running instances
The following commands run against the most recently started instance and allow The following commands run against the most recently started instance and allow
@ -358,6 +380,7 @@ options
sources_root = "/path/to/sources" # path of the nextcloud sources. required sources_root = "/path/to/sources" # path of the nextcloud sources. required
app_directories = ["/path/to/sources/more_app"] # paths to additional app directories. app_directories = ["/path/to/sources/more_app"] # paths to additional app directories.
work_dir = "/path/to/temp/dir" # path to temporary directory. optional, defaults to "/tmp/haze" work_dir = "/path/to/temp/dir" # path to temporary directory. optional, defaults to "/tmp/haze"
worktree_dir = "/path/to/worktrees" # where to create the worktrees of detached instances. optional, defaults to "<work_dir>/worktrees"
[auto_setup] # optional [auto_setup] # optional
enabled = false # whether or not to automatically install nextcloud on `haze start`. enabled by default enabled = false # whether or not to automatically install nextcloud on `haze start`. enabled by default

View file

@ -20,9 +20,26 @@ shadow-setup
echo $"Running as ($HAZE_UID):($HAZE_GID)" echo $"Running as ($HAZE_UID):($HAZE_GID)"
mkdir /var/www/html/core/skeleton /var/www/html/build/integration/vendor /var/www/html/build/integration/output /var/www/html/build/integration/work /var/www/html/core/skeleton /var/www/.composer/cache /var/www/html/apps/spreed/tests/integration/vendor/composer mkdir /var/www/html/core/skeleton \
/var/www/html/build/integration/vendor \
/var/www/html/build/integration/output \
/var/www/html/build/integration/work \
/var/www/html/core/skeleton \
/var/www/.composer/cache \
/var/www/html/apps/spreed/tests/integration/vendor/composer
chown -R $"($HAZE_UID):($HAZE_GID)" /var/www/html/data /var/www/html/config chown -R $"($HAZE_UID):($HAZE_GID)" /var/www/html/data /var/www/html/config
chown $"($HAZE_UID):($HAZE_GID)" /var/www/html/core/skeleton /var/www/html/build/integration/vendor /var/www/html/build/integration/composer.lock /var/www/html/build/integration/output /var/www/html/build/integration/work /var/www/html/core/skeleton /var/www/.composer/cache /var/www/html/apps/spreed/tests/integration/vendor/composer chown $"($HAZE_UID):($HAZE_GID)" /var/www/html/core/skeleton \
/var/www/html/build/integration/vendor \
/var/www/html/build/integration/composer.lock \
/var/www/html/build/integration/output \
/var/www/html/build/integration/work \
/var/www/html/core/skeleton \
/var/www/.composer/cache \
/var/www/html/apps/spreed \
/var/www/html/apps/spreed/tests \
/var/www/html/apps/spreed/tests/integration \
/var/www/html/apps/spreed/tests/integration/vendor \
/var/www/html/apps/spreed/tests/integration/vendor/composer
echo "{}\n" | save -f /var/www/html/build/integration/composer.lock echo "{}\n" | save -f /var/www/html/build/integration/composer.lock

View file

@ -525,6 +525,7 @@ fn test_arg_parse() {
sources_root: Default::default(), sources_root: Default::default(),
app_directories: Default::default(), app_directories: Default::default(),
work_dir: Default::default(), work_dir: Default::default(),
worktree_dir: Default::default(),
auto_setup: Default::default(), auto_setup: Default::default(),
volume: vec![], volume: vec![],
blackfire: None, blackfire: None,

View file

@ -1,6 +1,7 @@
use crate::config::{HazeConfig, HazeVolumeConfig}; use crate::config::{HazeConfig, HazeVolumeConfig};
use crate::database::Database; use crate::database::Database;
use crate::exec::{exec, exec_io, exec_tty, ExitCode}; use crate::exec::{exec, exec_io, exec_tty, ExitCode};
use crate::git::{create_worktree, find_app_repo, init_submodules, remove_worktree};
use crate::mapping::{for_config, Mapping}; use crate::mapping::{for_config, Mapping};
use crate::php::PhpVersion; use crate::php::PhpVersion;
use crate::service::Service; use crate::service::Service;
@ -53,6 +54,116 @@ fn get_max_php_version(base_path: &Utf8Path) -> Option<PhpVersion> {
const PINNED_FILE: &str = "/var/www/html/data/.haze-pinned"; const PINNED_FILE: &str = "/var/www/html/data/.haze-pinned";
fn enabled_apps(config: &HazeConfig, options: &CloudOptions) -> Vec<String> {
let mut apps: Vec<String> = config.auto_setup.enable_apps.clone();
for service in &options.services {
apps.extend(service.apps().iter().map(|app| app.to_string()));
// Presets do not list apps in apps(), so we need to fetch them from the config.
for preset in config.preset.iter().filter(|p| p.name == service.name()) {
apps.extend(preset.apps.iter().cloned());
}
}
apps.dedup();
apps
}
async fn creates_worktrees_for_instance(
config: &HazeConfig,
id: &str,
options: &CloudOptions,
git_ref: &str,
) -> Result<(Utf8PathBuf, Vec<HazeVolumeConfig>)> {
let worktree_dir = config.worktree_dir.join(id);
create_dir_all(&worktree_dir)
.await
.into_diagnostic()
.wrap_err("Failed to create directory for worktrees")?;
println!("Creating worktree of {} at {}", git_ref, worktree_dir);
create_worktree(&config.sources_root, &worktree_dir, git_ref)?;
init_submodules(&worktree_dir).wrap_err("Failed to setup submodules")?;
for app in enabled_apps(config, options) {
let Some(app_repo) = find_app_repo(config, &app) else {
continue;
};
let Some(dir_name) = app_repo.parent().and_then(|dir| dir.file_name()) else {
continue;
};
let app_worktree = worktree_dir.join(dir_name).join(&app);
println!("Creating worktree of {} at {}", app, app_worktree);
create_worktree(&app_repo, &app_worktree, "HEAD")
.wrap_err_with(|| format!("Failed to create worktree for {app}"))?;
}
Ok((worktree_dir, worktree_volumes(config, id)))
}
async fn remove_worktrees_for_instance(config: &HazeConfig, id: &str) -> Result<()> {
let worktree_dir = config.worktree_dir.join(id);
remove_worktree(&worktree_dir)?;
for app_dir in &config.app_directories {
let Some(dir_name) = app_dir.file_name() else {
continue;
};
let Ok(app_worktrees) = worktree_dir.join(dir_name).read_dir_utf8() else {
continue;
};
for app_worktree in app_worktrees.flatten() {
remove_worktree(app_worktree.path())?;
}
}
Ok(())
}
fn worktree_volumes(config: &HazeConfig, id: &str) -> Vec<HazeVolumeConfig> {
// In order to be able to run git operations on the worktree within the container,
// we need to mount the original repository too.
fn repo_volume(repo: &Utf8Path) -> HazeVolumeConfig {
HazeVolumeConfig {
create: false,
source: repo.join(".git"),
read_only: false,
target: repo.join(".git"),
}
}
let worktree_dir = config.worktree_dir.join(id);
let mut volumes = vec![repo_volume(&config.sources_root)];
for app_dir in &config.app_directories {
let Some(dir_name) = app_dir.file_name() else {
continue;
};
let Ok(app_worktrees) = worktree_dir.join(dir_name).read_dir_utf8() else {
continue;
};
for app_worktree in app_worktrees.flatten() {
let app = app_worktree.file_name().to_string();
let Some(app_repo) = find_app_repo(config, &app) else {
continue;
};
volumes.push(HazeVolumeConfig {
create: false,
source: app_worktree.into_path(),
read_only: false,
target: format!("/var/www/html/{dir_name}/{app}").into(),
});
volumes.push(repo_volume(&app_repo));
}
}
volumes
}
#[derive(Clone, Debug, Eq, PartialEq, Default)] #[derive(Clone, Debug, Eq, PartialEq, Default)]
pub struct CloudOptions { pub struct CloudOptions {
pub name: Option<String>, pub name: Option<String>,
@ -61,6 +172,7 @@ pub struct CloudOptions {
pub services: Vec<Service>, pub services: Vec<Service>,
pub app_packages: Vec<Utf8PathBuf>, pub app_packages: Vec<Utf8PathBuf>,
pub version: Option<String>, pub version: Option<String>,
pub detach: bool,
} }
impl CloudOptions { impl CloudOptions {
@ -74,6 +186,7 @@ impl CloudOptions {
services: vec![], services: vec![],
app_packages: vec![], app_packages: vec![],
version: None, version: None,
detach: false,
} }
} }
@ -88,6 +201,7 @@ impl CloudOptions {
let mut services = Vec::new(); let mut services = Vec::new();
let mut app_package = Vec::new(); let mut app_package = Vec::new();
let mut version = None; let mut version = None;
let mut detach = false;
while let Some(option) = args.peek() { while let Some(option) = args.peek() {
if let Ok(db_option) = Database::from_str(option.as_ref()) { if let Ok(db_option) = Database::from_str(option.as_ref()) {
@ -108,6 +222,9 @@ impl CloudOptions {
} else if option.as_ref() == "--name" { } else if option.as_ref() == "--name" {
let _ = args.next(); let _ = args.next();
name = args.next().map(|s| s.into()); name = args.next().map(|s| s.into());
} else if option.as_ref() == "--detach" {
let _ = args.next();
detach = true;
} else { } else {
break; break;
} }
@ -122,6 +239,7 @@ impl CloudOptions {
services, services,
app_packages: app_package, app_packages: app_package,
version, version,
detach,
}) })
} }
} }
@ -254,10 +372,10 @@ impl Cloud {
.wrap_err("Failed to create directory for app packages")?; .wrap_err("Failed to create directory for app packages")?;
} }
let source_root = if let Some(version) = options.version.as_deref() { let (source_root, source_volumes) = match (options.version.as_deref(), options.detach) {
download_nc(config, version).await? (Some(version), _) => (download_nc(config, version).await?, Vec::new()),
} else { (None, true) => creates_worktrees_for_instance(config, &id, &options, "HEAD").await?,
config.sources_root.clone() (None, false) => (config.sources_root.clone(), Vec::new()),
}; };
let app_volumes = options let app_volumes = options
@ -289,11 +407,12 @@ impl Cloud {
.collect::<Result<Vec<_>>>()?; .collect::<Result<Vec<_>>>()?;
let mappings = for_config(config) let mappings = for_config(config)
.chain(source_volumes.iter().map(Mapping::from))
.chain(app_volumes.iter().map(Mapping::from)) .chain(app_volumes.iter().map(Mapping::from))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
for mapping in &mappings { for mapping in &mappings {
mapping mapping
.create(&id, config) .create(&id, config, &source_root)
.await .await
.wrap_err_with(|| format!("Failed to setup work directory {}", mapping.source))?; .wrap_err_with(|| format!("Failed to setup work directory {}", mapping.source))?;
} }
@ -561,7 +680,7 @@ impl Cloud {
}) })
} }
pub async fn destroy(self, docker: &Docker) -> Result<()> { pub async fn destroy(self, docker: &Docker, config: &HazeConfig) -> Result<()> {
for container in self.containers { for container in self.containers {
let _ = docker let _ = docker
.kill_container(container.trim_start_matches('/'), None) .kill_container(container.trim_start_matches('/'), None)
@ -593,6 +712,8 @@ impl Cloud {
} }
} }
remove_worktrees_for_instance(config, &self.id).await?;
Ok(()) Ok(())
} }
@ -701,6 +822,15 @@ impl Cloud {
let db = labels.get("haze-db")?.parse().ok()?; let db = labels.get("haze-db")?.parse().ok()?;
let php = labels.get("haze-php")?.parse().ok()?; let php = labels.get("haze-php")?.parse().ok()?;
let version = labels.get("haze-version").cloned(); let version = labels.get("haze-version").cloned();
let sources_root = cloud
.mounts
.as_ref()?
.iter()
.find(|mount| mount.destination.as_deref() == Some("/var/www/html"))?
.source
.as_deref();
let detach = sources_root.map(Utf8Path::new)? != config.sources_root;
let found_services = labels let found_services = labels
.get("haze-services")? .get("haze-services")?
@ -740,6 +870,7 @@ impl Cloud {
services: found_services, services: found_services,
app_packages: vec![], app_packages: vec![],
version, version,
detach,
}, },
address, address,
preset_config: HashMap::default(), preset_config: HashMap::default(),
@ -847,15 +978,24 @@ impl Cloud {
format!("/var/www/html/{path}").into() format!("/var/www/html/{path}").into()
}; };
let mut mappings = for_config(config).collect::<Vec<_>>(); let worktree_volumes = worktree_volumes(config, &self.id);
let mut mappings = for_config(config)
.chain(worktree_volumes.iter().map(Mapping::from))
.collect::<Vec<_>>();
mappings.sort_by_key(|mapping| usize::MAX - mapping.target.as_str().len()); mappings.sort_by_key(|mapping| usize::MAX - mapping.target.as_str().len());
let source_root = if self.options.detach {
config.worktree_dir.join(&self.id)
} else {
config.sources_root.clone()
};
for mapping in mappings { for mapping in mappings {
if let Some(rel_path) = path.strip_prefix(mapping.target.as_str()) { if let Some(rel_path) = path.strip_prefix(mapping.target.as_str()) {
let rel_path = rel_path.trim_matches('/'); let rel_path = rel_path.trim_matches('/');
return Some( return Some(
mapping mapping
.source(&self.id, config, &config.sources_root) .source(&self.id, config, &source_root)
.join(rel_path), .join(rel_path),
); );
} }

View file

@ -16,6 +16,7 @@ pub struct HazeConfig {
pub sources_root: Utf8PathBuf, pub sources_root: Utf8PathBuf,
pub app_directories: Vec<Utf8PathBuf>, pub app_directories: Vec<Utf8PathBuf>,
pub work_dir: Utf8PathBuf, pub work_dir: Utf8PathBuf,
pub worktree_dir: Utf8PathBuf,
pub auto_setup: HazeAutoSetupConfig, pub auto_setup: HazeAutoSetupConfig,
pub volume: Vec<HazeVolumeConfig>, pub volume: Vec<HazeVolumeConfig>,
pub blackfire: Option<HazeBlackfireConfig>, pub blackfire: Option<HazeBlackfireConfig>,
@ -29,6 +30,8 @@ pub struct RawHazeConfig {
#[serde(default = "default_work_dir")] #[serde(default = "default_work_dir")]
pub work_dir: Utf8PathBuf, pub work_dir: Utf8PathBuf,
#[serde(default)] #[serde(default)]
pub worktree_dir: Option<Utf8PathBuf>,
#[serde(default)]
pub app_directories: Vec<Utf8PathBuf>, pub app_directories: Vec<Utf8PathBuf>,
#[serde(default)] #[serde(default)]
pub auto_setup: HazeAutoSetupConfig, pub auto_setup: HazeAutoSetupConfig,
@ -57,6 +60,8 @@ impl From<RawHazeConfig> for HazeConfig {
} }
} }
let work_dir = normalize_path(raw.work_dir);
HazeConfig { HazeConfig {
sources_root: normalize_path(raw.sources_root), sources_root: normalize_path(raw.sources_root),
app_directories: raw app_directories: raw
@ -64,7 +69,11 @@ impl From<RawHazeConfig> for HazeConfig {
.into_iter() .into_iter()
.map(normalize_path) .map(normalize_path)
.collect(), .collect(),
work_dir: normalize_path(raw.work_dir), worktree_dir: raw
.worktree_dir
.map(normalize_path)
.unwrap_or_else(|| work_dir.join("worktrees")),
work_dir,
auto_setup: raw.auto_setup, auto_setup: raw.auto_setup,
volume: raw.volume, volume: raw.volume,
blackfire: raw.blackfire, blackfire: raw.blackfire,

View file

@ -1,9 +1,10 @@
use crate::config::HazeConfig; use crate::config::HazeConfig;
use crate::Result; use crate::Result;
use camino::{Utf8Path, Utf8PathBuf};
use git2::build::CheckoutBuilder; use git2::build::CheckoutBuilder;
use git2::{Branch, BranchType, Repository, RepositoryState}; use git2::{Branch, BranchType, Repository, RepositoryState};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use miette::{Context, IntoDiagnostic}; use miette::{Context, IntoDiagnostic, Report};
use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use rayon::ThreadPoolBuilder; use rayon::ThreadPoolBuilder;
use std::fs::read_dir; use std::fs::read_dir;
@ -69,6 +70,83 @@ pub fn checkout_all(config: &HazeConfig, mut name: &str, verbose: bool) -> Resul
Ok(()) Ok(())
} }
pub fn find_app_repo(config: &HazeConfig, app: &str) -> Option<Utf8PathBuf> {
find_app_repos(config)
.ok()?
.filter_map(|dir| Utf8PathBuf::try_from(dir).ok())
.find(|dir| dir.file_name().is_some_and(|name| name == app))
}
fn git(repo: &Utf8Path, args: &[&str]) -> Result<String> {
let output = Command::new(GIT_BINARY)
.args(args)
.current_dir(repo)
.output()
.into_diagnostic()
.wrap_err_with(|| format!("Failed to run git {} for {}", args.join(" "), repo))?;
if !output.status.success() {
let err = String::from_utf8_lossy(&output.stderr);
return Err(Report::msg(format!(
"git {} failed in {}: {}",
args.join(" "),
repo,
err.trim()
)));
}
Ok(String::from_utf8_lossy(&output.stdout).trim().into())
}
fn has_ref(repo: &Utf8Path, git_ref: &str) -> bool {
git(
repo,
&[
"rev-parse",
"--verify",
"--quiet",
&format!("{git_ref}^{{commit}}"),
],
)
.is_ok()
}
pub fn create_worktree(repo: &Utf8Path, dest: &Utf8Path, git_ref: &str) -> Result<()> {
if !has_ref(repo, git_ref) {
// The reference might be a tag that hasn't been fetched yet.
let _ = git(repo, &["fetch", "origin", "tag", git_ref]);
if !has_ref(repo, git_ref) {
return Err(Report::msg(format!("{git_ref} not found in {repo}")));
}
}
git(
repo,
&["worktree", "add", "--detach", dest.as_str(), git_ref], // Use a detached head so the worktree doesn't conflict with another one.
)?;
Ok(())
}
pub fn remove_worktree(repo: &Utf8Path) -> Result<()> {
git(repo, &["worktree", "remove", "--force", repo.as_str()])?;
Ok(())
}
pub fn init_submodules(worktree: &Utf8Path) -> Result<()> {
git(worktree, &["submodule", "update", "--init", "--recursive"])?;
Ok(())
}
pub fn prune_worktrees(config: &HazeConfig) -> Result<()> {
let app_repos = find_app_repos(config)?
.filter_map(|repo| Utf8PathBuf::try_from(repo).ok())
.collect::<Vec<_>>();
for repo in once(config.sources_root.clone()).chain(app_repos) {
if let Err(e) = git(&repo, &["worktree", "prune"]) {
eprintln!("Failed to prune worktrees of {repo}: {e:#}");
}
}
Ok(())
}
fn current_branch_name(repo: &Repository) -> Option<String> { fn current_branch_name(repo: &Repository) -> Option<String> {
let (branch, _) = repo let (branch, _) = repo
.branches(None) .branches(None)

View file

@ -89,6 +89,8 @@ fn subcommand_help(command: &dyn SubCommand) {
print!(" {}", "[database type]".green()); print!(" {}", "[database type]".green());
print!(" {}", "[services]".green()); print!(" {}", "[services]".green());
print!(" {}", "[vX.Y.Z]".green()); print!(" {}", "[vX.Y.Z]".green());
print!(" {}", "[--name <name>]".green());
print!(" {}", "[--detach]".green());
} }
let args = if let Some(args) = command.get_str("Args") { let args = if let Some(args) = command.get_str("Args") {
@ -156,6 +158,17 @@ fn subcommand_help(command: &dyn SubCommand) {
service.get_documentation().unwrap_or_default(), service.get_documentation().unwrap_or_default(),
); );
} }
println!();
println!("{}", "Options:".yellow().bold());
println!(
" {} name the instance instead of generating a random name",
"--name <name>".blue()
);
println!(
" {} give the instance its own worktree of the sources",
"--detach".blue()
);
} }
if let Some(sub_commands) = command.sub_commands() { if let Some(sub_commands) = command.sub_commands() {

View file

@ -5,7 +5,7 @@ use crate::cloud::{Cloud, CloudOptions};
use crate::config::HazeConfig; use crate::config::HazeConfig;
use crate::database::DatabaseFamily; use crate::database::DatabaseFamily;
use crate::exec::{container_logs, exec, exec_tty}; use crate::exec::{container_logs, exec, exec_tty};
use crate::git::{checkout_all, pull_all}; use crate::git::{checkout_all, prune_worktrees, pull_all};
use crate::help::help; use crate::help::help;
use crate::image::update_image; use crate::image::update_image;
use crate::network::clear_networks; use crate::network::clear_networks;
@ -79,7 +79,7 @@ async fn main() -> Result<ExitCode> {
} }
for cloud in remove { for cloud in remove {
if let Err(e) = cloud.destroy(&docker).await { if let Err(e) = cloud.destroy(&docker, &config).await {
eprintln!("Error while removing cloud: {:#}", e); eprintln!("Error while removing cloud: {:#}", e);
} }
} }
@ -91,6 +91,11 @@ async fn main() -> Result<ExitCode> {
if let Some(id) = cache_dir.file_name().to_str() { if let Some(id) = cache_dir.file_name().to_str() {
if !retain.iter().any(|cloud| cloud.id == id) { if !retain.iter().any(|cloud| cloud.id == id) {
let path = cache_dir.path(); let path = cache_dir.path();
// By default, the worktrees live within the work dir, we do not want to delete it here.
if path == config.worktree_dir {
continue;
}
if path.is_dir() { if path.is_dir() {
remove_dir_all(&path).into_diagnostic().wrap_err_with(|| { remove_dir_all(&path).into_diagnostic().wrap_err_with(|| {
format!("Failed to cleanup stray cache dir: {}", path.display()) format!("Failed to cleanup stray cache dir: {}", path.display())
@ -103,6 +108,8 @@ async fn main() -> Result<ExitCode> {
} }
} }
} }
prune_worktrees(&config)?;
} }
HazeArgs::List { filter } => { HazeArgs::List { filter } => {
let list = Cloud::list(&docker, filter, &config).await?; let list = Cloud::list(&docker, filter, &config).await?;
@ -119,13 +126,19 @@ async fn main() -> Result<ExitCode> {
Some(version) => format!(", v{version}"), Some(version) => format!(", v{version}"),
None => String::new(), None => String::new(),
}; };
let detached = if cloud.options.detach {
", detached"
} else {
""
};
println!( println!(
"Cloud {}{}, {}, {}{}, running on {}", "Cloud {}{}, {}, {}{}{}, running on {}",
cloud.id, cloud.id,
pin, pin,
cloud.php().name(), cloud.php().name(),
services, services,
version, version,
detached,
cloud.address, cloud.address,
); );
} }
@ -135,7 +148,7 @@ async fn main() -> Result<ExitCode> {
} }
HazeArgs::Stop { filter } => { HazeArgs::Stop { filter } => {
let cloud = Cloud::get_by_filter(&docker, filter, &config).await?; let cloud = Cloud::get_by_filter(&docker, filter, &config).await?;
cloud.destroy(&docker).await?; cloud.destroy(&docker, &config).await?;
} }
HazeArgs::Logs { HazeArgs::Logs {
filter, filter,
@ -290,7 +303,7 @@ async fn main() -> Result<ExitCode> {
) )
.await .await
{ {
cloud.destroy(&docker).await?; cloud.destroy(&docker, &config).await?;
return Err(e); return Err(e);
} }
if let Some(app) = args if let Some(app) = args
@ -307,7 +320,7 @@ async fn main() -> Result<ExitCode> {
} }
args.insert(0, "tests".to_string()); args.insert(0, "tests".to_string());
let result = cloud.exec(&docker, args, false, get_forward_env()).await?; let result = cloud.exec(&docker, args, false, get_forward_env()).await?;
cloud.destroy(&docker).await?; cloud.destroy(&docker, &config).await?;
return Ok(result.into()); return Ok(result.into());
} }
HazeArgs::Integration { options, mut args } => { HazeArgs::Integration { options, mut args } => {
@ -315,7 +328,7 @@ async fn main() -> Result<ExitCode> {
args.insert(0, "integration".to_string()); args.insert(0, "integration".to_string());
cloud.exec(&docker, args, false, get_forward_env()).await?; cloud.exec(&docker, args, false, get_forward_env()).await?;
cloud.destroy(&docker).await?; cloud.destroy(&docker, &config).await?;
} }
HazeArgs::Fmt { path } => { HazeArgs::Fmt { path } => {
let cloud = Cloud::create(&docker, CloudOptions::new(&config), &config).await?; let cloud = Cloud::create(&docker, CloudOptions::new(&config), &config).await?;
@ -334,7 +347,7 @@ async fn main() -> Result<ExitCode> {
.and_then(|c| c.to_result()) .and_then(|c| c.to_result())
{ {
eprintln!("{}", String::from_utf8_lossy(&out_buffer)); eprintln!("{}", String::from_utf8_lossy(&out_buffer));
cloud.destroy(&docker).await?; cloud.destroy(&docker, &config).await?;
return Err(e); return Err(e);
} }
out_buffer.clear(); out_buffer.clear();
@ -348,7 +361,7 @@ async fn main() -> Result<ExitCode> {
) )
.await .await
{ {
cloud.destroy(&docker).await?; cloud.destroy(&docker, &config).await?;
return Err(e); return Err(e);
} }
println!("Cleanup"); println!("Cleanup");
@ -363,7 +376,7 @@ async fn main() -> Result<ExitCode> {
.and_then(|c| c.to_result()) .and_then(|c| c.to_result())
{ {
eprintln!("{}", String::from_utf8_lossy(&out_buffer)); eprintln!("{}", String::from_utf8_lossy(&out_buffer));
cloud.destroy(&docker).await?; cloud.destroy(&docker, &config).await?;
return Err(e); return Err(e);
} }
if let Err(e) = cloud if let Err(e) = cloud
@ -377,10 +390,10 @@ async fn main() -> Result<ExitCode> {
.and_then(|c| c.to_result()) .and_then(|c| c.to_result())
{ {
eprintln!("{}", String::from_utf8_lossy(&out_buffer)); eprintln!("{}", String::from_utf8_lossy(&out_buffer));
cloud.destroy(&docker).await?; cloud.destroy(&docker, &config).await?;
return Err(e); return Err(e);
} }
cloud.destroy(&docker).await?; cloud.destroy(&docker, &config).await?;
} }
HazeArgs::Shell { command, options } => { HazeArgs::Shell { command, options } => {
let cloud = setup(&docker, options, &config).await?; let cloud = setup(&docker, options, &config).await?;
@ -396,7 +409,7 @@ async fn main() -> Result<ExitCode> {
get_forward_env(), get_forward_env(),
) )
.await?; .await?;
cloud.destroy(&docker).await?; cloud.destroy(&docker, &config).await?;
} }
HazeArgs::Pin { filter } => { HazeArgs::Pin { filter } => {
let cloud = Cloud::get_by_filter(&docker, filter, &config).await?; let cloud = Cloud::get_by_filter(&docker, filter, &config).await?;

View file

@ -81,7 +81,12 @@ impl<'a> Mapping<'a> {
} }
} }
pub async fn create(&self, id: &str, config: &HazeConfig) -> Result<()> { pub async fn create(
&self,
id: &str,
config: &HazeConfig,
source_root: &Utf8Path,
) -> Result<()> {
if !self.create { if !self.create {
return Ok(()); return Ok(());
} }
@ -96,6 +101,19 @@ impl<'a> Mapping<'a> {
MappingType::File => write(source, "").await.into_diagnostic()?, MappingType::File => write(source, "").await.into_diagnostic()?,
} }
// For worktrees, pre-create mountpoints within /var/www/html to prevent docker from creating them as root.
if self.target.starts_with("/var/www/html") {
let local_target = source_root.join(
self.target
.strip_prefix("/var/www/html")
.unwrap_or(&self.target),
);
match self.mapping_type {
MappingType::Folder => create_dir_all(local_target).await.into_diagnostic()?,
MappingType::File => write(local_target, "").await.into_diagnostic()?,
}
}
Ok(()) Ok(())
} }