Skip to content

Commit 694e5c7

Browse files
committed
feat: separate product level configuration
1 parent fbf681f commit 694e5c7

File tree

2 files changed

+109
-61
lines changed

2 files changed

+109
-61
lines changed

rust/patchable/README.md

+22-6
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,36 @@ For more details, run `cargo patchable --help`.
3131

3232
## Configuration
3333

34-
Patchable stores a per-version file in `docker-images/<PRODUCT>/stackable/patches/<VERSION>/patchable.toml`.
35-
It currently recognizes the following keys:
34+
Patchable uses a two-level configuration system:
3635

36+
1. A product-level config file at `docker-images/<PRODUCT>/stackable/patches/patchable.toml`
37+
2. A version-level config file at `docker-images/<PRODUCT>/stackable/patches/<VERSION>/patchable.toml`
38+
39+
The product-level config typically contains:
3740
- `upstream` - the URL of the upstream repository (such as `https://github.com/apache/druid.git`)
38-
- `base` - the commit hash of the upstream base commit (such as `7cffb81a8e124d5f218f9af16ad685acf5e9c67c`)
41+
- `mirror` - optional URL of a mirror repository (such as `https://github.com/stackabletech/druid.git`)
3942

40-
### Template
43+
The version-level config typically contains:
44+
- `base` - the commit hash of the upstream base commit
4145

42-
Instead of creating this manually, run `patchable init`:
46+
Fields in the version-level config override those in the product-level config if both are specified.
47+
48+
### Template
4349

50+
If you're adding a completely new product, you need to create the product-level config once:
4451
```toml
45-
cargo patchable init druid 28.0.0 --upstream=https://github.com/apache/druid.git --base=druid-28.0.0
52+
# docker-images/druid/stackable/patches/patchable.toml
53+
upstream = "https://github.com/apache/druid.git"
54+
mirror = "https://github.com/stackabletech/druid.git"
4655
```
4756

57+
If you just want to add a new version, simply initiatilize it with patchable:
58+
```
59+
cargo patchable init druid 28.0.0 --base=druid-28.0.0 --mirror
60+
```
61+
62+
Using the `--mirror` flag will push the base ref to the mirror URL specified in the product-level config.
63+
4864
## Glossary
4965

5066
- Images repo/directory - The checkout of stackabletech/docker-images

rust/patchable/src/main.rs

+87-55
Original file line numberDiff line numberDiff line change
@@ -25,29 +25,72 @@ struct ProductVersion {
2525
}
2626

2727
#[derive(Deserialize, Serialize)]
28-
struct ProductVersionConfig {
28+
struct ProductConfig {
2929
upstream: String,
30+
mirror: Option<String>,
31+
}
32+
33+
#[derive(Deserialize, Serialize)]
34+
struct ProductVersionConfig {
35+
upstream: Option<String>,
3036
#[serde(with = "utils::oid_serde")]
3137
base: Oid,
3238
mirror: Option<String>,
3339
}
3440

41+
struct MergedProductVersionConfig {
42+
upstream: String,
43+
base: Oid,
44+
mirror: Option<String>,
45+
}
46+
3547
struct ProductVersionContext {
3648
pv: ProductVersion,
3749
images_repo_root: PathBuf,
3850
}
3951

4052
impl ProductVersionContext {
41-
fn load_config(&self) -> Result<ProductVersionConfig> {
42-
let path = &self.config_path();
53+
fn load_product_config(&self) -> Result<ProductConfig> {
54+
let product_config_path = &self.product_config_path();
55+
4356
tracing::info!(
44-
config.path = ?path,
45-
"loading config"
57+
config.path = ?product_config_path,
58+
"loading product-level config"
4659
);
47-
toml::from_str::<ProductVersionConfig>(
48-
&std::fs::read_to_string(path).context(LoadConfigSnafu { path })?,
60+
61+
toml::from_str::<ProductConfig>(&std::fs::read_to_string(product_config_path).context(
62+
LoadConfigSnafu {
63+
path: product_config_path,
64+
},
65+
)?)
66+
.context(ParseConfigSnafu {
67+
path: product_config_path,
68+
})
69+
}
70+
71+
fn load_version_config(&self) -> Result<MergedProductVersionConfig> {
72+
// Load product-level config (required)
73+
let product_config = self.load_product_config()?;
74+
75+
// Load version-level config (optional)
76+
let version_config_path = &self.version_config_path();
77+
let loaded_version_config = toml::from_str::<ProductVersionConfig>(
78+
&std::fs::read_to_string(version_config_path).context(LoadConfigSnafu {
79+
path: version_config_path,
80+
})?,
4981
)
50-
.context(ParseConfigSnafu { path })
82+
.context(ParseConfigSnafu {
83+
path: version_config_path,
84+
})?;
85+
86+
// Inherit `upstream` and `mirror` from product-level config if not set in loaded version-level config
87+
Ok(MergedProductVersionConfig {
88+
upstream: loaded_version_config
89+
.upstream
90+
.unwrap_or(product_config.upstream),
91+
base: loaded_version_config.base,
92+
mirror: loaded_version_config.mirror.or(product_config.mirror),
93+
})
5194
}
5295

5396
/// The root directory for files related to the product (across all versions).
@@ -63,10 +106,15 @@ impl ProductVersionContext {
63106
}
64107

65108
/// The patchable configuration file for the product version.
66-
fn config_path(&self) -> PathBuf {
109+
fn version_config_path(&self) -> PathBuf {
67110
self.patch_dir().join("patchable.toml")
68111
}
69112

113+
/// The product-level patchable configuration file
114+
fn product_config_path(&self) -> PathBuf {
115+
self.product_dir().join("stackable/patches/patchable.toml")
116+
}
117+
70118
/// The directory containing all ephemeral data used by patchable for the product (across all versions).
71119
///
72120
/// Should be gitignored, and can safely be deleted as long as all relevant versions have been `patchable export`ed.
@@ -141,20 +189,11 @@ enum Cmd {
141189
#[clap(flatten)]
142190
pv: ProductVersion,
143191

144-
/// The upstream URL (such as https://github.com/apache/druid.git)
145-
#[clap(long)]
146-
upstream: String,
147-
148192
/// The upstream commit-ish (such as druid-28.0.0) that the patch series applies to
149193
///
150194
/// Refs (such as tags and branches) will be resolved to commit IDs.
151195
#[clap(long)]
152196
base: String,
153-
154-
/// Assume a mirror exists at stackabletech/<repo_name> and push the base ref to it.
155-
/// The mirror URL will be stored in patchable.toml, and used instead of the original upstream.
156-
#[clap(long)]
157-
mirror: bool,
158197
},
159198

160199
/// Shows the patch directory for a given product version
@@ -203,8 +242,6 @@ pub enum Error {
203242
path: PathBuf,
204243
},
205244

206-
#[snafu(display("failed to parse upstream URL {url:?} to extract repository name"))]
207-
ParseUpstreamUrl { url: String },
208245
#[snafu(display("failed to add temporary mirror remote for {url:?}"))]
209246
AddMirrorRemote { source: git2::Error, url: String },
210247
#[snafu(display("failed to push commit {commit} (as {refspec}) to mirror {url:?}"))]
@@ -297,15 +334,18 @@ fn main() -> Result<()> {
297334
pv,
298335
images_repo_root,
299336
};
300-
let config = ctx.load_config()?;
337+
let config = ctx.load_version_config()?;
301338
let product_repo_root = ctx.product_repo();
302339
let product_repo = repo::ensure_bare_repo(&product_repo_root)
303340
.context(OpenProductRepoForCheckoutSnafu)?;
304341

305342
let base_commit = repo::resolve_and_fetch_commitish(
306343
&product_repo,
307344
&config.base.to_string(),
308-
config.mirror.as_deref().unwrap_or(&config.upstream),
345+
config
346+
.mirror
347+
.as_deref()
348+
.unwrap_or(&config.upstream),
309349
)
310350
.context(FetchBaseCommitSnafu)?;
311351
let base_branch = ctx.base_branch();
@@ -366,7 +406,7 @@ fn main() -> Result<()> {
366406
pv,
367407
images_repo_root,
368408
};
369-
let config = ctx.load_config()?;
409+
let config = ctx.load_version_config()?;
370410

371411
let product_worktree_root = ctx.worktree_root();
372412
tracing::info!(
@@ -415,12 +455,7 @@ fn main() -> Result<()> {
415455
);
416456
}
417457

418-
Cmd::Init {
419-
pv,
420-
upstream,
421-
base,
422-
mirror,
423-
} => {
458+
Cmd::Init { pv, base } => {
424459
let ctx = ProductVersionContext {
425460
pv,
426461
images_repo_root,
@@ -434,77 +469,74 @@ fn main() -> Result<()> {
434469
.in_scope(|| repo::ensure_bare_repo(&product_repo_root))
435470
.context(OpenProductRepoForCheckoutSnafu)?;
436471

472+
let config = ctx.load_product_config()?;
473+
let upstream = config.upstream;
474+
437475
// --base can be a reference, but patchable.toml should always have a resolved commit id,
438476
// so that it cannot be changed under our feet (without us knowing so, anyway...).
439477
tracing::info!(?base, "resolving base commit-ish");
440-
let base_commit = repo::resolve_and_fetch_commitish(&product_repo, &base, &upstream).context(FetchBaseCommitSnafu)?;
441-
let mut upstream_mirror = None;
442-
443-
if mirror {
444-
// Parse e.g. "https://github.com/apache/druid.git" into "druid"
445-
let repo_name = upstream.split('/').last().map(|repo| repo.trim_end_matches(".git")).filter(|name| !name.is_empty()).context(ParseUpstreamUrlSnafu { url: &upstream })?;
446-
447-
let mirror_url = format!("https://github.com/stackabletech/{repo_name}.git");
448-
tracing::info!(mirror_url, "using mirror repository");
478+
let base_commit = repo::resolve_and_fetch_commitish(&product_repo, &base, &upstream)
479+
.context(FetchBaseCommitSnafu)?;
449480

481+
if let Some(mirror_url) = config.mirror {
450482
// Add mirror remote
451-
let mut mirror_remote = product_repo
452-
.remote_anonymous(&mirror_url)
453-
.context(AddMirrorRemoteSnafu { url: mirror_url.clone() })?;
483+
let mut mirror_remote =
484+
product_repo
485+
.remote_anonymous(&mirror_url)
486+
.context(AddMirrorRemoteSnafu {
487+
url: mirror_url.clone(),
488+
})?;
454489

455490
// Push the base commit to the mirror
456491
tracing::info!(commit = %base_commit, base = base, url = mirror_url, "pushing commit to mirror");
457492
let mut callbacks = git2::RemoteCallbacks::new();
458493
callbacks.credentials(|url, username_from_url, _allowed_types| {
459494
git2::Cred::credential_helper(
460-
&git2::Config::open_default().unwrap(), // Use default git config
495+
&git2::Config::open_default()
496+
.expect("failed to open default Git configuration"), // Use default git config,
461497
url,
462498
username_from_url,
463499
)
464500
});
465501

466502
// Add progress tracking for push operation
467-
let (span_push, mut quant_push) = utils::setup_progress_tracking(tracing::info_span!("pushing"));
503+
let (span_push, mut quant_push) =
504+
utils::setup_progress_tracking(tracing::info_span!("pushing"));
468505
let _ = span_push.enter();
469506

470507
callbacks.push_transfer_progress(move |current, total, _| {
471508
if total > 0 {
472-
quant_push.update_span_progress(
473-
current,
474-
total,
475-
&span_push,
476-
);
509+
quant_push.update_span_progress(current, total, &span_push);
477510
}
478511
});
479512

480513
let mut push_options = git2::PushOptions::new();
481514
push_options.remote_callbacks(callbacks);
482515

516+
// Always push the commit as a Git tag named like the value of `base`
483517
let refspec = format!("{base_commit}:refs/tags/{base}");
484518
tracing::info!(refspec, "constructed push refspec");
485519

486520
mirror_remote
487521
.push(&[&refspec], Some(&mut push_options))
488522
.context(PushToMirrorSnafu {
489-
url: &mirror_url,
523+
url: mirror_url.clone(),
490524
refspec: &refspec,
491525
commit: base_commit,
492526
})?;
493527

494528
tracing::info!("successfully pushed base ref to mirror");
495-
496-
upstream_mirror = Some(mirror_url);
497529
};
498530

499531
tracing::info!(?base, base.commit = ?base_commit, "resolved base commit");
500532

501-
tracing::info!("saving configuration");
533+
tracing::info!("saving version-level configuration");
502534
let config = ProductVersionConfig {
503-
upstream,
504-
mirror: upstream_mirror,
535+
upstream: None,
536+
mirror: None,
505537
base: base_commit,
506538
};
507-
let config_path = ctx.config_path();
539+
let config_path = ctx.version_config_path();
508540
if let Some(config_dir) = config_path.parent() {
509541
std::fs::create_dir_all(config_dir)
510542
.context(CreatePatchDirSnafu { path: config_dir })?;

0 commit comments

Comments
 (0)