Skip to content

Commit 2c17e7e

Browse files
committed
feat: init product command for patchable
1 parent 037b97d commit 2c17e7e

File tree

2 files changed

+193
-121
lines changed

2 files changed

+193
-121
lines changed

rust/patchable/README.md

+6-7
Original file line numberDiff line numberDiff line change
@@ -48,21 +48,20 @@ The version-level config contains:
4848

4949
### Template
5050

51-
If you're adding a completely new product, you need to create the product-level config once:
51+
If you're adding a completely new product, you need to initialize the product-level config once using patchable:
5252

53-
```toml
54-
# docker-images/druid/stackable/patches/patchable.toml
55-
upstream = "https://github.com/apache/druid.git"
56-
mirror = "https://github.com/stackabletech/druid.git"
53+
```sh
54+
cargo patchable init product druid --upstream https://github.com/apache/druid.git --default-mirror https://github.com/stackabletech/druid.git
5755
```
56+
This will create the product-level configuration in `docker-images/druid/stackable/patches/patchable.toml`.
5857

5958
If you just want to add a new version, initialize the version-level config with patchable:
6059

6160
```sh
62-
cargo patchable init druid 28.0.0 --base=druid-28.0.0 --mirror
61+
cargo patchable init version druid 28.0.0 --base druid-28.0.0 --mirror
6362
```
6463

65-
This will initialize the version-level config with the base commit hash and the default mirror URL from the product-level config.
64+
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.
6665
You can optionally provide the `--ssh` flag to use SSH instead of HTTPS for Git operations.
6766

6867
## Glossary

rust/patchable/src/main.rs

+187-114
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ struct ProductConfig {
3939
///
4040
/// This value is _not_ used by `checkout`, that uses [`ProductVersionConfig::mirror`] instead.
4141
/// `init --mirror` copies this value into [`ProductVersionConfig::mirror`].
42+
#[serde(skip_serializing_if = "Option::is_none")]
4243
default_mirror: Option<String>,
4344
}
4445

@@ -180,24 +181,10 @@ enum Cmd {
180181
pv: ProductVersion,
181182
},
182183

183-
/// Creates a patchable.toml for a given product version
184+
/// Creates patchable.toml configuration files
184185
Init {
185-
#[clap(flatten)]
186-
pv: ProductVersion,
187-
188-
/// The upstream commit-ish (such as druid-28.0.0) that the patch series applies to
189-
///
190-
/// Refs (such as tags and branches) will be resolved to commit IDs.
191-
#[clap(long)]
192-
base: String,
193-
194-
/// Mirror the product version to the default mirror repository
195-
#[clap(long)]
196-
mirror: bool,
197-
198-
/// Use SSH for git operations
199-
#[clap(long)]
200-
ssh: bool,
186+
#[clap(subcommand)]
187+
init_type: InitType,
201188
},
202189

203190
/// Shows the patch directory for a given product version
@@ -218,6 +205,41 @@ enum Cmd {
218205
ImagesDir,
219206
}
220207

208+
#[derive(clap::Parser)]
209+
enum InitType {
210+
/// Creates a patchable.toml for a given product
211+
Product {
212+
/// The product name slug (such as druid)
213+
product: String,
214+
/// The upstream repository URL (e.g. https://github.com/apache/druid.git)
215+
#[clap(long)]
216+
upstream: String,
217+
/// The default mirror repository URL (e.g. https://github.com/stackabletech/druid.git)
218+
#[clap(long)]
219+
default_mirror: Option<String>,
220+
},
221+
222+
/// Creates a patchable.toml for a given product version
223+
Version {
224+
#[clap(flatten)]
225+
pv: ProductVersion,
226+
227+
/// The upstream commit-ish (such as druid-28.0.0) that the patch series applies to
228+
///
229+
/// Refs (such as tags and branches) will be resolved to commit IDs.
230+
#[clap(long)]
231+
base: String,
232+
233+
/// Mirror the product version to the default mirror repository
234+
#[clap(long)]
235+
mirror: bool,
236+
237+
/// Use SSH for git operations
238+
#[clap(long)]
239+
ssh: bool,
240+
},
241+
}
242+
221243
#[derive(Debug, Snafu)]
222244
pub enum Error {
223245
#[snafu(display("failed to configure git logging"))]
@@ -474,116 +496,167 @@ fn main() -> Result<()> {
474496
);
475497
}
476498

477-
Cmd::Init {
478-
pv,
479-
base,
480-
mirror,
481-
ssh,
482-
} => {
483-
let ctx = ProductVersionContext {
484-
pv,
485-
images_repo_root,
486-
};
499+
Cmd::Init { init_type } => match init_type {
500+
InitType::Product {
501+
product,
502+
upstream,
503+
default_mirror,
504+
} => {
505+
let product_config_path = ProductVersionContext {
506+
pv: ProductVersion {
507+
product: product.clone(),
508+
version: "".to_string(),
509+
},
510+
images_repo_root,
511+
}
512+
.product_config_path();
487513

488-
let product_repo_root = ctx.product_repo();
489-
let product_repo = tracing::info_span!(
490-
"finding product repository",
491-
product.repository = ?product_repo_root,
492-
)
493-
.in_scope(|| repo::ensure_bare_repo(&product_repo_root))
494-
.context(OpenProductRepoForCheckoutSnafu)?;
514+
tracing::info!(
515+
path = ?product_config_path,
516+
"creating product configuration directory and file"
517+
);
495518

496-
let config = ctx.load_product_config()?;
497-
let upstream = if ssh {
498-
utils::rewrite_git_https_url_to_ssh(&config.upstream).context(UrlRewriteSnafu)?
499-
} else {
500-
config.upstream
501-
};
519+
let product_config_dir = product_config_path
520+
.parent()
521+
.expect("product config should have a hard-coded parent");
522+
523+
std::fs::create_dir_all(product_config_dir).context(CreatePatchDirSnafu {
524+
path: product_config_dir,
525+
})?;
502526

503-
// --base can be a reference, but patchable.toml should always have a resolved commit id,
504-
// so that it cannot be changed under our feet (without us knowing so, anyway...).
505-
tracing::info!(?base, "resolving base commit-ish");
506-
let base_commit = repo::resolve_and_fetch_commitish(&product_repo, &base, &upstream)
507-
.context(FetchBaseCommitSnafu)?;
508-
tracing::info!(?base, base.commit = ?base_commit, "resolved base commit");
509-
510-
let mirror_url = if mirror {
511-
let mut mirror_url = config
512-
.default_mirror
513-
.context(InitMirrorNotConfiguredSnafu)?;
514-
if ssh {
515-
mirror_url =
516-
utils::rewrite_git_https_url_to_ssh(&mirror_url).context(UrlRewriteSnafu)?
527+
let product_config = ProductConfig {
528+
upstream,
529+
default_mirror,
517530
};
518-
// Add mirror remote
519-
let mut mirror_remote =
520-
product_repo
521-
.remote_anonymous(&mirror_url)
522-
.context(AddMirrorRemoteSnafu {
523-
url: mirror_url.clone(),
524-
})?;
525531

526-
// Push the base commit to the mirror
527-
tracing::info!(commit = %base_commit, base = base, url = mirror_url, "pushing commit to mirror");
528-
let mut callbacks = setup_git_credentials();
532+
let config_toml =
533+
toml::to_string_pretty(&product_config).context(SerializeConfigSnafu)?;
534+
File::create_new(&product_config_path)
535+
.and_then(|mut f| f.write_all(config_toml.as_bytes()))
536+
.context(SaveConfigSnafu {
537+
path: &product_config_path,
538+
})?;
539+
540+
tracing::info!(
541+
config.path = ?product_config_path,
542+
product = product,
543+
"created configuration for product"
544+
);
545+
}
529546

530-
// Add progress tracking for push operation
531-
let (span_push, mut quant_push) =
532-
utils::setup_progress_tracking(tracing::info_span!("pushing"));
533-
let _ = span_push.enter();
547+
InitType::Version {
548+
pv,
549+
base,
550+
mirror,
551+
ssh,
552+
} => {
553+
let ctx = ProductVersionContext {
554+
pv,
555+
images_repo_root,
556+
};
534557

535-
callbacks.push_transfer_progress(move |current, total, _| {
536-
if total > 0 {
537-
quant_push.update_span_progress(current, total, &span_push);
538-
}
539-
});
558+
let product_repo_root = ctx.product_repo();
559+
let product_repo = tracing::info_span!(
560+
"finding product repository",
561+
product.repository = ?product_repo_root,
562+
)
563+
.in_scope(|| repo::ensure_bare_repo(&product_repo_root))
564+
.context(OpenProductRepoForCheckoutSnafu)?;
540565

541-
let mut push_options = git2::PushOptions::new();
542-
push_options.remote_callbacks(callbacks);
566+
let config = ctx.load_product_config()?;
567+
let upstream = if ssh {
568+
utils::rewrite_git_https_url_to_ssh(&config.upstream)
569+
.context(UrlRewriteSnafu)?
570+
} else {
571+
config.upstream
572+
};
543573

544-
// Always push the commit as a Git tag named like the value of `base`
545-
let refspec = format!("{base_commit}:refs/tags/{base}");
546-
tracing::info!(refspec, "constructed push refspec");
574+
// --base can be a reference, but patchable.toml should always have a resolved commit id,
575+
// so that it cannot be changed under our feet (without us knowing so, anyway...).
576+
tracing::info!(?base, "resolving base commit-ish");
577+
let base_commit =
578+
repo::resolve_and_fetch_commitish(&product_repo, &base, &upstream)
579+
.context(FetchBaseCommitSnafu)?;
580+
tracing::info!(?base, base.commit = ?base_commit, "resolved base commit");
581+
582+
let mirror_url = if mirror {
583+
let mut mirror_url = config
584+
.default_mirror
585+
.context(InitMirrorNotConfiguredSnafu)?;
586+
if ssh {
587+
mirror_url = utils::rewrite_git_https_url_to_ssh(&mirror_url)
588+
.context(UrlRewriteSnafu)?
589+
};
590+
// Add mirror remote
591+
let mut mirror_remote = product_repo.remote_anonymous(&mirror_url).context(
592+
AddMirrorRemoteSnafu {
593+
url: mirror_url.clone(),
594+
},
595+
)?;
596+
597+
// Push the base commit to the mirror
598+
tracing::info!(commit = %base_commit, base = base, url = mirror_url, "pushing commit to mirror");
599+
let mut callbacks = setup_git_credentials();
600+
601+
// Add progress tracking for push operation
602+
let (span_push, mut quant_push) =
603+
utils::setup_progress_tracking(tracing::info_span!("pushing"));
604+
let _ = span_push.enter();
605+
606+
callbacks.push_transfer_progress(move |current, total, _| {
607+
if total > 0 {
608+
quant_push.update_span_progress(current, total, &span_push);
609+
}
610+
});
611+
612+
let mut push_options = git2::PushOptions::new();
613+
push_options.remote_callbacks(callbacks);
614+
615+
// Always push the commit as a Git tag named like the value of `base`
616+
let refspec = format!("{base_commit}:refs/tags/{base}");
617+
tracing::info!(refspec, "constructed push refspec");
618+
619+
mirror_remote
620+
.push(&[&refspec], Some(&mut push_options))
621+
.context(PushToMirrorSnafu {
622+
url: &mirror_url,
623+
refspec: &refspec,
624+
commit: base_commit,
625+
})?;
547626

548-
mirror_remote
549-
.push(&[&refspec], Some(&mut push_options))
550-
.context(PushToMirrorSnafu {
551-
url: &mirror_url,
552-
refspec: &refspec,
553-
commit: base_commit,
554-
})?;
627+
tracing::info!("successfully pushed base ref to mirror");
628+
Some(mirror_url)
629+
} else {
630+
tracing::warn!(
631+
"this version is not mirrored, re-run with --mirror before merging into main"
632+
);
633+
None
634+
};
555635

556-
tracing::info!("successfully pushed base ref to mirror");
557-
Some(mirror_url)
558-
} else {
559-
tracing::warn!(
560-
"this version is not mirrored, re-run with --mirror before merging into main"
561-
);
562-
None
563-
};
636+
tracing::info!("saving version-level configuration");
637+
let config = ProductVersionConfig {
638+
base: base_commit,
639+
mirror: mirror_url,
640+
};
641+
let config_path = ctx.version_config_path();
642+
if let Some(config_dir) = config_path.parent() {
643+
std::fs::create_dir_all(config_dir)
644+
.context(CreatePatchDirSnafu { path: config_dir })?;
645+
}
564646

565-
tracing::info!("saving version-level configuration");
566-
let config = ProductVersionConfig {
567-
base: base_commit,
568-
mirror: mirror_url,
569-
};
570-
let config_path = ctx.version_config_path();
571-
if let Some(config_dir) = config_path.parent() {
572-
std::fs::create_dir_all(config_dir)
573-
.context(CreatePatchDirSnafu { path: config_dir })?;
574-
}
575-
let config_toml = toml::to_string_pretty(&config).context(SerializeConfigSnafu)?;
576-
File::create_new(&config_path)
577-
.and_then(|mut f| f.write_all(config_toml.as_bytes()))
578-
.context(SaveConfigSnafu { path: &config_path })?;
647+
let config_toml = toml::to_string_pretty(&config).context(SerializeConfigSnafu)?;
648+
File::create_new(&config_path)
649+
.and_then(|mut f| f.write_all(config_toml.as_bytes()))
650+
.context(SaveConfigSnafu { path: &config_path })?;
579651

580-
tracing::info!(
581-
config.path = ?config_path,
582-
product = ctx.pv.product,
583-
version = ctx.pv.version,
584-
"created configuration for product version"
585-
);
586-
}
652+
tracing::info!(
653+
config.path = ?config_path,
654+
product = ctx.pv.product,
655+
version = ctx.pv.version,
656+
"created configuration for product version"
657+
);
658+
}
659+
},
587660

588661
Cmd::PatchDir { pv } => {
589662
let ctx = ProductVersionContext {

0 commit comments

Comments
 (0)