From 618159c7d574b4b3ce95751b4d6c58baa04e8ff8 Mon Sep 17 00:00:00 2001 From: dervoeti Date: Fri, 9 May 2025 16:26:13 +0200 Subject: [PATCH 1/5] feat: init product command for patchable --- rust/patchable/README.md | 14 +- rust/patchable/src/main.rs | 301 +++++++++++++++++++++++-------------- 2 files changed, 194 insertions(+), 121 deletions(-) diff --git a/rust/patchable/README.md b/rust/patchable/README.md index 76c90b1fd..80bbe59d5 100644 --- a/rust/patchable/README.md +++ b/rust/patchable/README.md @@ -48,21 +48,21 @@ The version-level config contains: ### Template -If you're adding a completely new product, you need to create the product-level config once: +If you're adding a completely new product, you need to initialize the product-level config once using patchable: -```toml -# docker-images/druid/stackable/patches/patchable.toml -upstream = "https://github.com/apache/druid.git" -mirror = "https://github.com/stackabletech/druid.git" +```sh +cargo patchable init product druid --upstream https://github.com/apache/druid.git --default-mirror https://github.com/stackabletech/druid.git ``` +This will create the product-level configuration in `docker-images/druid/stackable/patches/patchable.toml`. + If you just want to add a new version, initialize the version-level config with patchable: ```sh -cargo patchable init druid 28.0.0 --base=druid-28.0.0 --mirror +cargo patchable init version druid 28.0.0 --base druid-28.0.0 --mirror ``` -This will initialize the version-level config with the base commit hash and the default mirror URL from the product-level config. +This will initialize the version-level config in `docker-images/druid/stackable/patches/28.0.0/patchable.toml` with the base commit hash and the default mirror URL from the product-level config. You can optionally provide the `--ssh` flag to use SSH instead of HTTPS for Git operations. ## Glossary diff --git a/rust/patchable/src/main.rs b/rust/patchable/src/main.rs index 65983daf9..1037b5781 100644 --- a/rust/patchable/src/main.rs +++ b/rust/patchable/src/main.rs @@ -39,6 +39,7 @@ struct ProductConfig { /// /// This value is _not_ used by `checkout`, that uses [`ProductVersionConfig::mirror`] instead. /// `init --mirror` copies this value into [`ProductVersionConfig::mirror`]. + #[serde(skip_serializing_if = "Option::is_none")] default_mirror: Option, } @@ -180,24 +181,10 @@ enum Cmd { pv: ProductVersion, }, - /// Creates a patchable.toml for a given product version + /// Creates patchable.toml configuration files Init { - #[clap(flatten)] - pv: ProductVersion, - - /// The upstream commit-ish (such as druid-28.0.0) that the patch series applies to - /// - /// Refs (such as tags and branches) will be resolved to commit IDs. - #[clap(long)] - base: String, - - /// Mirror the product version to the default mirror repository - #[clap(long)] - mirror: bool, - - /// Use SSH for git operations - #[clap(long)] - ssh: bool, + #[clap(subcommand)] + init_type: InitType, }, /// Shows the patch directory for a given product version @@ -218,6 +205,41 @@ enum Cmd { ImagesDir, } +#[derive(clap::Parser)] +enum InitType { + /// Creates a patchable.toml for a given product + Product { + /// The product name slug (such as druid) + product: String, + /// The upstream repository URL (e.g. https://github.com/apache/druid.git) + #[clap(long)] + upstream: String, + /// The default mirror repository URL (e.g. https://github.com/stackabletech/druid.git) + #[clap(long)] + default_mirror: Option, + }, + + /// Creates a patchable.toml for a given product version + Version { + #[clap(flatten)] + pv: ProductVersion, + + /// The upstream commit-ish (such as druid-28.0.0) that the patch series applies to + /// + /// Refs (such as tags and branches) will be resolved to commit IDs. + #[clap(long)] + base: String, + + /// Mirror the product version to the default mirror repository + #[clap(long)] + mirror: bool, + + /// Use SSH for git operations + #[clap(long)] + ssh: bool, + }, +} + #[derive(Debug, Snafu)] pub enum Error { #[snafu(display("failed to configure git logging"))] @@ -474,116 +496,167 @@ fn main() -> Result<()> { ); } - Cmd::Init { - pv, - base, - mirror, - ssh, - } => { - let ctx = ProductVersionContext { - pv, - images_repo_root, - }; + Cmd::Init { init_type } => match init_type { + InitType::Product { + product, + upstream, + default_mirror, + } => { + let product_config_path = ProductVersionContext { + pv: ProductVersion { + product: product.clone(), + version: "".to_string(), + }, + images_repo_root, + } + .product_config_path(); - let product_repo_root = ctx.product_repo(); - let product_repo = tracing::info_span!( - "finding product repository", - product.repository = ?product_repo_root, - ) - .in_scope(|| repo::ensure_bare_repo(&product_repo_root)) - .context(OpenProductRepoForCheckoutSnafu)?; + tracing::info!( + path = ?product_config_path, + "creating product configuration directory and file" + ); - let config = ctx.load_product_config()?; - let upstream = if ssh { - utils::rewrite_git_https_url_to_ssh(&config.upstream).context(UrlRewriteSnafu)? - } else { - config.upstream - }; + let product_config_dir = product_config_path + .parent() + .expect("product config should have a hard-coded parent"); + + std::fs::create_dir_all(product_config_dir).context(CreatePatchDirSnafu { + path: product_config_dir, + })?; - // --base can be a reference, but patchable.toml should always have a resolved commit id, - // so that it cannot be changed under our feet (without us knowing so, anyway...). - tracing::info!(?base, "resolving base commit-ish"); - let base_commit = repo::resolve_and_fetch_commitish(&product_repo, &base, &upstream) - .context(FetchBaseCommitSnafu)?; - tracing::info!(?base, base.commit = ?base_commit, "resolved base commit"); - - let mirror_url = if mirror { - let mut mirror_url = config - .default_mirror - .context(InitMirrorNotConfiguredSnafu)?; - if ssh { - mirror_url = - utils::rewrite_git_https_url_to_ssh(&mirror_url).context(UrlRewriteSnafu)? + let product_config = ProductConfig { + upstream, + default_mirror, }; - // Add mirror remote - let mut mirror_remote = - product_repo - .remote_anonymous(&mirror_url) - .context(AddMirrorRemoteSnafu { - url: mirror_url.clone(), - })?; - // Push the base commit to the mirror - tracing::info!(commit = %base_commit, base = base, url = mirror_url, "pushing commit to mirror"); - let mut callbacks = setup_git_credentials(); + let config_toml = + toml::to_string_pretty(&product_config).context(SerializeConfigSnafu)?; + File::create_new(&product_config_path) + .and_then(|mut f| f.write_all(config_toml.as_bytes())) + .context(SaveConfigSnafu { + path: &product_config_path, + })?; + + tracing::info!( + config.path = ?product_config_path, + product = product, + "created configuration for product" + ); + } - // Add progress tracking for push operation - let (span_push, mut quant_push) = - utils::setup_progress_tracking(tracing::info_span!("pushing")); - let _ = span_push.enter(); + InitType::Version { + pv, + base, + mirror, + ssh, + } => { + let ctx = ProductVersionContext { + pv, + images_repo_root, + }; - callbacks.push_transfer_progress(move |current, total, _| { - if total > 0 { - quant_push.update_span_progress(current, total, &span_push); - } - }); + let product_repo_root = ctx.product_repo(); + let product_repo = tracing::info_span!( + "finding product repository", + product.repository = ?product_repo_root, + ) + .in_scope(|| repo::ensure_bare_repo(&product_repo_root)) + .context(OpenProductRepoForCheckoutSnafu)?; - let mut push_options = git2::PushOptions::new(); - push_options.remote_callbacks(callbacks); + let config = ctx.load_product_config()?; + let upstream = if ssh { + utils::rewrite_git_https_url_to_ssh(&config.upstream) + .context(UrlRewriteSnafu)? + } else { + config.upstream + }; - // Always push the commit as a Git tag named like the value of `base` - let refspec = format!("{base_commit}:refs/tags/{base}"); - tracing::info!(refspec, "constructed push refspec"); + // --base can be a reference, but patchable.toml should always have a resolved commit id, + // so that it cannot be changed under our feet (without us knowing so, anyway...). + tracing::info!(?base, "resolving base commit-ish"); + let base_commit = + repo::resolve_and_fetch_commitish(&product_repo, &base, &upstream) + .context(FetchBaseCommitSnafu)?; + tracing::info!(?base, base.commit = ?base_commit, "resolved base commit"); + + let mirror_url = if mirror { + let mut mirror_url = config + .default_mirror + .context(InitMirrorNotConfiguredSnafu)?; + if ssh { + mirror_url = utils::rewrite_git_https_url_to_ssh(&mirror_url) + .context(UrlRewriteSnafu)? + }; + // Add mirror remote + let mut mirror_remote = product_repo.remote_anonymous(&mirror_url).context( + AddMirrorRemoteSnafu { + url: mirror_url.clone(), + }, + )?; + + // Push the base commit to the mirror + tracing::info!(commit = %base_commit, base = base, url = mirror_url, "pushing commit to mirror"); + let mut callbacks = setup_git_credentials(); + + // Add progress tracking for push operation + let (span_push, mut quant_push) = + utils::setup_progress_tracking(tracing::info_span!("pushing")); + let _ = span_push.enter(); + + callbacks.push_transfer_progress(move |current, total, _| { + if total > 0 { + quant_push.update_span_progress(current, total, &span_push); + } + }); + + let mut push_options = git2::PushOptions::new(); + push_options.remote_callbacks(callbacks); + + // Always push the commit as a Git tag named like the value of `base` + let refspec = format!("{base_commit}:refs/tags/{base}"); + tracing::info!(refspec, "constructed push refspec"); + + mirror_remote + .push(&[&refspec], Some(&mut push_options)) + .context(PushToMirrorSnafu { + url: &mirror_url, + refspec: &refspec, + commit: base_commit, + })?; - mirror_remote - .push(&[&refspec], Some(&mut push_options)) - .context(PushToMirrorSnafu { - url: &mirror_url, - refspec: &refspec, - commit: base_commit, - })?; + tracing::info!("successfully pushed base ref to mirror"); + Some(mirror_url) + } else { + tracing::warn!( + "this version is not mirrored, re-run with --mirror before merging into main" + ); + None + }; - tracing::info!("successfully pushed base ref to mirror"); - Some(mirror_url) - } else { - tracing::warn!( - "this version is not mirrored, re-run with --mirror before merging into main" - ); - None - }; + tracing::info!("saving version-level configuration"); + let config = ProductVersionConfig { + base: base_commit, + mirror: mirror_url, + }; + let config_path = ctx.version_config_path(); + if let Some(config_dir) = config_path.parent() { + std::fs::create_dir_all(config_dir) + .context(CreatePatchDirSnafu { path: config_dir })?; + } - tracing::info!("saving version-level configuration"); - let config = ProductVersionConfig { - base: base_commit, - mirror: mirror_url, - }; - let config_path = ctx.version_config_path(); - if let Some(config_dir) = config_path.parent() { - std::fs::create_dir_all(config_dir) - .context(CreatePatchDirSnafu { path: config_dir })?; - } - let config_toml = toml::to_string_pretty(&config).context(SerializeConfigSnafu)?; - File::create_new(&config_path) - .and_then(|mut f| f.write_all(config_toml.as_bytes())) - .context(SaveConfigSnafu { path: &config_path })?; + let config_toml = toml::to_string_pretty(&config).context(SerializeConfigSnafu)?; + File::create_new(&config_path) + .and_then(|mut f| f.write_all(config_toml.as_bytes())) + .context(SaveConfigSnafu { path: &config_path })?; - tracing::info!( - config.path = ?config_path, - product = ctx.pv.product, - version = ctx.pv.version, - "created configuration for product version" - ); - } + tracing::info!( + config.path = ?config_path, + product = ctx.pv.product, + version = ctx.pv.version, + "created configuration for product version" + ); + } + }, Cmd::PatchDir { pv } => { let ctx = ProductVersionContext { From ad6a1add83b387baca4a7badb80cdce57e2ba8c4 Mon Sep 17 00:00:00 2001 From: Lukas Krug Date: Wed, 14 May 2025 16:31:56 +0200 Subject: [PATCH 2/5] Update rust/patchable/src/main.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Natalie Klestrup Röijezon --- rust/patchable/src/main.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/rust/patchable/src/main.rs b/rust/patchable/src/main.rs index 1037b5781..e5f3b55ad 100644 --- a/rust/patchable/src/main.rs +++ b/rust/patchable/src/main.rs @@ -516,13 +516,11 @@ fn main() -> Result<()> { "creating product configuration directory and file" ); - let product_config_dir = product_config_path - .parent() - .expect("product config should have a hard-coded parent"); - - std::fs::create_dir_all(product_config_dir).context(CreatePatchDirSnafu { - path: product_config_dir, - })?; + if let Some(product_config_dir) = product_config_path.parent() { + std::fs::create_dir_all(product_config_dir).context(CreatePatchDirSnafu { + path: product_config_dir, + })?; + } let product_config = ProductConfig { upstream, From c623c3178d803238daf70ee5b9c32969db67d62a Mon Sep 17 00:00:00 2001 From: dervoeti Date: Wed, 14 May 2025 16:36:19 +0200 Subject: [PATCH 3/5] chore: un-nest match blocks --- rust/patchable/src/main.rs | 285 +++++++++++++++++++------------------ 1 file changed, 144 insertions(+), 141 deletions(-) diff --git a/rust/patchable/src/main.rs b/rust/patchable/src/main.rs index e5f3b55ad..a1387db74 100644 --- a/rust/patchable/src/main.rs +++ b/rust/patchable/src/main.rs @@ -496,165 +496,168 @@ fn main() -> Result<()> { ); } - Cmd::Init { init_type } => match init_type { - InitType::Product { - product, - upstream, - default_mirror, - } => { - let product_config_path = ProductVersionContext { - pv: ProductVersion { - product: product.clone(), - version: "".to_string(), - }, - images_repo_root, - } - .product_config_path(); - - tracing::info!( - path = ?product_config_path, - "creating product configuration directory and file" - ); - - if let Some(product_config_dir) = product_config_path.parent() { - std::fs::create_dir_all(product_config_dir).context(CreatePatchDirSnafu { - path: product_config_dir, - })?; - } - - let product_config = ProductConfig { + Cmd::Init { + init_type: + InitType::Product { + product, upstream, default_mirror, - }; + }, + } => { + let product_config_path = ProductVersionContext { + pv: ProductVersion { + product: product.clone(), + version: "".to_string(), + }, + images_repo_root, + } + .product_config_path(); - let config_toml = - toml::to_string_pretty(&product_config).context(SerializeConfigSnafu)?; - File::create_new(&product_config_path) - .and_then(|mut f| f.write_all(config_toml.as_bytes())) - .context(SaveConfigSnafu { - path: &product_config_path, - })?; + tracing::info!( + path = ?product_config_path, + "creating product configuration directory and file" + ); - tracing::info!( - config.path = ?product_config_path, - product = product, - "created configuration for product" - ); + if let Some(product_config_dir) = product_config_path.parent() { + std::fs::create_dir_all(product_config_dir).context(CreatePatchDirSnafu { + path: product_config_dir, + })?; } - InitType::Version { - pv, - base, - mirror, - ssh, - } => { - let ctx = ProductVersionContext { + let product_config = ProductConfig { + upstream, + default_mirror, + }; + + let config_toml = + toml::to_string_pretty(&product_config).context(SerializeConfigSnafu)?; + File::create_new(&product_config_path) + .and_then(|mut f| f.write_all(config_toml.as_bytes())) + .context(SaveConfigSnafu { + path: &product_config_path, + })?; + + tracing::info!( + config.path = ?product_config_path, + product = product, + "created configuration for product" + ); + } + + Cmd::Init { + init_type: + InitType::Version { pv, - images_repo_root, - }; + base, + mirror, + ssh, + }, + } => { + let ctx = ProductVersionContext { + pv, + images_repo_root, + }; - let product_repo_root = ctx.product_repo(); - let product_repo = tracing::info_span!( - "finding product repository", - product.repository = ?product_repo_root, - ) - .in_scope(|| repo::ensure_bare_repo(&product_repo_root)) - .context(OpenProductRepoForCheckoutSnafu)?; + let product_repo_root = ctx.product_repo(); + let product_repo = tracing::info_span!( + "finding product repository", + product.repository = ?product_repo_root, + ) + .in_scope(|| repo::ensure_bare_repo(&product_repo_root)) + .context(OpenProductRepoForCheckoutSnafu)?; - let config = ctx.load_product_config()?; - let upstream = if ssh { - utils::rewrite_git_https_url_to_ssh(&config.upstream) - .context(UrlRewriteSnafu)? - } else { - config.upstream - }; + let config = ctx.load_product_config()?; + let upstream = if ssh { + utils::rewrite_git_https_url_to_ssh(&config.upstream).context(UrlRewriteSnafu)? + } else { + config.upstream + }; - // --base can be a reference, but patchable.toml should always have a resolved commit id, - // so that it cannot be changed under our feet (without us knowing so, anyway...). - tracing::info!(?base, "resolving base commit-ish"); - let base_commit = - repo::resolve_and_fetch_commitish(&product_repo, &base, &upstream) - .context(FetchBaseCommitSnafu)?; - tracing::info!(?base, base.commit = ?base_commit, "resolved base commit"); - - let mirror_url = if mirror { - let mut mirror_url = config - .default_mirror - .context(InitMirrorNotConfiguredSnafu)?; - if ssh { - mirror_url = utils::rewrite_git_https_url_to_ssh(&mirror_url) - .context(UrlRewriteSnafu)? - }; - // Add mirror remote - let mut mirror_remote = product_repo.remote_anonymous(&mirror_url).context( - AddMirrorRemoteSnafu { + // --base can be a reference, but patchable.toml should always have a resolved commit id, + // so that it cannot be changed under our feet (without us knowing so, anyway...). + tracing::info!(?base, "resolving base commit-ish"); + let base_commit = repo::resolve_and_fetch_commitish(&product_repo, &base, &upstream) + .context(FetchBaseCommitSnafu)?; + tracing::info!(?base, base.commit = ?base_commit, "resolved base commit"); + + let mirror_url = if mirror { + let mut mirror_url = config + .default_mirror + .context(InitMirrorNotConfiguredSnafu)?; + if ssh { + mirror_url = + utils::rewrite_git_https_url_to_ssh(&mirror_url).context(UrlRewriteSnafu)? + }; + // Add mirror remote + let mut mirror_remote = + product_repo + .remote_anonymous(&mirror_url) + .context(AddMirrorRemoteSnafu { url: mirror_url.clone(), - }, - )?; - - // Push the base commit to the mirror - tracing::info!(commit = %base_commit, base = base, url = mirror_url, "pushing commit to mirror"); - let mut callbacks = setup_git_credentials(); - - // Add progress tracking for push operation - let (span_push, mut quant_push) = - utils::setup_progress_tracking(tracing::info_span!("pushing")); - let _ = span_push.enter(); - - callbacks.push_transfer_progress(move |current, total, _| { - if total > 0 { - quant_push.update_span_progress(current, total, &span_push); - } - }); - - let mut push_options = git2::PushOptions::new(); - push_options.remote_callbacks(callbacks); - - // Always push the commit as a Git tag named like the value of `base` - let refspec = format!("{base_commit}:refs/tags/{base}"); - tracing::info!(refspec, "constructed push refspec"); - - mirror_remote - .push(&[&refspec], Some(&mut push_options)) - .context(PushToMirrorSnafu { - url: &mirror_url, - refspec: &refspec, - commit: base_commit, })?; - tracing::info!("successfully pushed base ref to mirror"); - Some(mirror_url) - } else { - tracing::warn!( - "this version is not mirrored, re-run with --mirror before merging into main" - ); - None - }; + // Push the base commit to the mirror + tracing::info!(commit = %base_commit, base = base, url = mirror_url, "pushing commit to mirror"); + let mut callbacks = setup_git_credentials(); - tracing::info!("saving version-level configuration"); - let config = ProductVersionConfig { - base: base_commit, - mirror: mirror_url, - }; - let config_path = ctx.version_config_path(); - if let Some(config_dir) = config_path.parent() { - std::fs::create_dir_all(config_dir) - .context(CreatePatchDirSnafu { path: config_dir })?; - } + // Add progress tracking for push operation + let (span_push, mut quant_push) = + utils::setup_progress_tracking(tracing::info_span!("pushing")); + let _ = span_push.enter(); - let config_toml = toml::to_string_pretty(&config).context(SerializeConfigSnafu)?; - File::create_new(&config_path) - .and_then(|mut f| f.write_all(config_toml.as_bytes())) - .context(SaveConfigSnafu { path: &config_path })?; + callbacks.push_transfer_progress(move |current, total, _| { + if total > 0 { + quant_push.update_span_progress(current, total, &span_push); + } + }); + + let mut push_options = git2::PushOptions::new(); + push_options.remote_callbacks(callbacks); + + // Always push the commit as a Git tag named like the value of `base` + let refspec = format!("{base_commit}:refs/tags/{base}"); + tracing::info!(refspec, "constructed push refspec"); + + mirror_remote + .push(&[&refspec], Some(&mut push_options)) + .context(PushToMirrorSnafu { + url: &mirror_url, + refspec: &refspec, + commit: base_commit, + })?; - tracing::info!( - config.path = ?config_path, - product = ctx.pv.product, - version = ctx.pv.version, - "created configuration for product version" + tracing::info!("successfully pushed base ref to mirror"); + Some(mirror_url) + } else { + tracing::warn!( + "this version is not mirrored, re-run with --mirror before merging into main" ); + None + }; + + tracing::info!("saving version-level configuration"); + let config = ProductVersionConfig { + base: base_commit, + mirror: mirror_url, + }; + let config_path = ctx.version_config_path(); + if let Some(config_dir) = config_path.parent() { + std::fs::create_dir_all(config_dir) + .context(CreatePatchDirSnafu { path: config_dir })?; } - }, + + let config_toml = toml::to_string_pretty(&config).context(SerializeConfigSnafu)?; + File::create_new(&config_path) + .and_then(|mut f| f.write_all(config_toml.as_bytes())) + .context(SaveConfigSnafu { path: &config_path })?; + + tracing::info!( + config.path = ?config_path, + product = ctx.pv.product, + version = ctx.pv.version, + "created configuration for product version" + ); + } Cmd::PatchDir { pv } => { let ctx = ProductVersionContext { From 74ff55db9da75c6a391aaa2996698bf2b2d70478 Mon Sep 17 00:00:00 2001 From: dervoeti Date: Wed, 14 May 2025 16:38:40 +0200 Subject: [PATCH 4/5] chore: rename InitType to InitCmd --- rust/patchable/src/main.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rust/patchable/src/main.rs b/rust/patchable/src/main.rs index a1387db74..94ceaebc4 100644 --- a/rust/patchable/src/main.rs +++ b/rust/patchable/src/main.rs @@ -184,7 +184,7 @@ enum Cmd { /// Creates patchable.toml configuration files Init { #[clap(subcommand)] - init_type: InitType, + init_type: InitCmd, }, /// Shows the patch directory for a given product version @@ -206,7 +206,7 @@ enum Cmd { } #[derive(clap::Parser)] -enum InitType { +enum InitCmd { /// Creates a patchable.toml for a given product Product { /// The product name slug (such as druid) @@ -498,7 +498,7 @@ fn main() -> Result<()> { Cmd::Init { init_type: - InitType::Product { + InitCmd::Product { product, upstream, default_mirror, @@ -546,7 +546,7 @@ fn main() -> Result<()> { Cmd::Init { init_type: - InitType::Version { + InitCmd::Version { pv, base, mirror, From 06e6acd1504e0923e230c0561bca427f5dd664e4 Mon Sep 17 00:00:00 2001 From: dervoeti Date: Wed, 14 May 2025 16:54:26 +0200 Subject: [PATCH 5/5] feat: allow explicitly empty mirror URLs --- rust/patchable/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/patchable/src/main.rs b/rust/patchable/src/main.rs index 94ceaebc4..10b5ae988 100644 --- a/rust/patchable/src/main.rs +++ b/rust/patchable/src/main.rs @@ -39,7 +39,6 @@ struct ProductConfig { /// /// This value is _not_ used by `checkout`, that uses [`ProductVersionConfig::mirror`] instead. /// `init --mirror` copies this value into [`ProductVersionConfig::mirror`]. - #[serde(skip_serializing_if = "Option::is_none")] default_mirror: Option, } @@ -583,6 +582,7 @@ fn main() -> Result<()> { let mirror_url = if mirror { let mut mirror_url = config .default_mirror + .filter(|s| !s.is_empty()) .context(InitMirrorNotConfiguredSnafu)?; if ssh { mirror_url =