@@ -26,9 +26,9 @@ struct ProductVersion {
2626
2727#[ derive( Deserialize , Serialize ) ]
2828struct ProductVersionConfig {
29- upstream : String ,
30- #[ serde( with = "utils::oid_serde " ) ]
31- base : Oid ,
29+ upstream : Option < String > ,
30+ #[ serde( with = "utils::option_oid_serde " ) ]
31+ base : Option < Oid > ,
3232 mirror : Option < String > ,
3333}
3434
@@ -39,15 +39,77 @@ struct ProductVersionContext {
3939
4040impl ProductVersionContext {
4141 fn load_config ( & self ) -> Result < ProductVersionConfig > {
42- let path = & self . config_path ( ) ;
43- tracing:: info!(
44- config. path = ?path,
45- "loading config"
46- ) ;
47- toml:: from_str :: < ProductVersionConfig > (
48- & std:: fs:: read_to_string ( path) . context ( LoadConfigSnafu { path } ) ?,
49- )
50- . context ( ParseConfigSnafu { path } )
42+ let version_config_path = & self . version_config_path ( ) ;
43+ let product_config_path = & self . product_config_path ( ) ;
44+
45+ // Load product-level config (required)
46+ let product_config: ProductVersionConfig = {
47+ tracing:: info!(
48+ config. path = ?product_config_path,
49+ "loading product-level config"
50+ ) ;
51+ toml:: from_str :: < ProductVersionConfig > (
52+ & std:: fs:: read_to_string ( product_config_path) . context ( LoadConfigSnafu {
53+ path : product_config_path,
54+ } ) ?,
55+ )
56+ . context ( ParseConfigSnafu {
57+ path : product_config_path,
58+ } ) ?
59+ } ;
60+
61+ // Load version-level config (optional)
62+ let version_config: Option < ProductVersionConfig > =
63+ match std:: fs:: read_to_string ( version_config_path) {
64+ Ok ( s) => {
65+ tracing:: info!(
66+ config. path = ?version_config_path,
67+ "loading version-level config"
68+ ) ;
69+ Some (
70+ toml:: from_str :: < ProductVersionConfig > ( & s) . context ( ParseConfigSnafu {
71+ path : version_config_path,
72+ } ) ?,
73+ )
74+ }
75+ Err ( e) if e. kind ( ) == std:: io:: ErrorKind :: NotFound => None ,
76+ Err ( e) => {
77+ return Err ( e) . context ( LoadConfigSnafu {
78+ path : version_config_path,
79+ } )
80+ }
81+ } ;
82+ // Merge: version-level overrides product-level, validate required fields afterwards
83+ let merged = ProductVersionConfig {
84+ upstream : version_config
85+ . as_ref ( )
86+ . and_then ( |c| c. upstream . clone ( ) )
87+ . or ( product_config. upstream . clone ( ) )
88+ . or_else ( || {
89+ Err ( ValidateConfigSnafu {
90+ reason : "upstream is required" . to_string ( ) ,
91+ paths : vec ! [ product_config_path, version_config_path] ,
92+ } )
93+ . ok ( )
94+ } ) ,
95+ base : version_config
96+ . as_ref ( )
97+ . and_then ( |c| c. base )
98+ . or ( product_config. base )
99+ . or_else ( || {
100+ Err ( ValidateConfigSnafu {
101+ reason : "base commit is required" . to_string ( ) ,
102+ paths : vec ! [ product_config_path, version_config_path] ,
103+ } )
104+ . ok ( )
105+ } ) ,
106+ mirror : version_config
107+ . as_ref ( )
108+ . and_then ( |c| c. mirror . clone ( ) )
109+ . or ( product_config. mirror . clone ( ) ) ,
110+ } ;
111+
112+ Ok ( merged)
51113 }
52114
53115 /// The root directory for files related to the product (across all versions).
@@ -63,10 +125,15 @@ impl ProductVersionContext {
63125 }
64126
65127 /// The patchable configuration file for the product version.
66- fn config_path ( & self ) -> PathBuf {
128+ fn version_config_path ( & self ) -> PathBuf {
67129 self . patch_dir ( ) . join ( "patchable.toml" )
68130 }
69131
132+ /// The product-level patchable configuration file
133+ fn product_config_path ( & self ) -> PathBuf {
134+ self . product_dir ( ) . join ( "stackable/patches/patchable.toml" )
135+ }
136+
70137 /// The directory containing all ephemeral data used by patchable for the product (across all versions).
71138 ///
72139 /// Should be gitignored, and can safely be deleted as long as all relevant versions have been `patchable export`ed.
@@ -190,6 +257,8 @@ pub enum Error {
190257 source : toml:: de:: Error ,
191258 path : PathBuf ,
192259 } ,
260+ #[ snafu( display( "failed to validate config: {reason:?} (used config files: {paths:?})" ) ) ]
261+ ValidateConfig { reason : String , paths : Vec < PathBuf > } ,
193262 #[ snafu( display( "failed to serialize config" ) ) ]
194263 SerializeConfig { source : toml:: ser:: Error } ,
195264 #[ snafu( display( "failed to create patch dir at {path:?}" ) ) ]
@@ -203,8 +272,6 @@ pub enum Error {
203272 path : PathBuf ,
204273 } ,
205274
206- #[ snafu( display( "failed to parse upstream URL {url:?} to extract repository name" ) ) ]
207- ParseUpstreamUrl { url : String } ,
208275 #[ snafu( display( "failed to add temporary mirror remote for {url:?}" ) ) ]
209276 AddMirrorRemote { source : git2:: Error , url : String } ,
210277 #[ snafu( display( "failed to push commit {commit} (as {refspec}) to mirror {url:?}" ) ) ]
@@ -302,10 +369,14 @@ fn main() -> Result<()> {
302369 let product_repo = repo:: ensure_bare_repo ( & product_repo_root)
303370 . context ( OpenProductRepoForCheckoutSnafu ) ?;
304371
372+ // Presence of `base` and `upstream` already validated by load_config, so unwrap is safe here
305373 let base_commit = repo:: resolve_and_fetch_commitish (
306374 & product_repo,
307- & config. base . to_string ( ) ,
308- config. mirror . as_deref ( ) . unwrap_or ( & config. upstream ) ,
375+ & config. base . unwrap ( ) . to_string ( ) ,
376+ config
377+ . mirror
378+ . as_deref ( )
379+ . unwrap_or ( config. upstream . as_ref ( ) . unwrap ( ) ) ,
309380 )
310381 . context ( FetchBaseCommitSnafu ) ?;
311382 let base_branch = ctx. base_branch ( ) ;
@@ -378,7 +449,8 @@ fn main() -> Result<()> {
378449 path : product_worktree_root,
379450 } ) ?;
380451
381- let base_commit = config. base ;
452+ // Presence of `base` already validated by load_config, so unwrap is safe here
453+ let base_commit = config. base . unwrap ( ) ;
382454 let original_leaf_commit = product_version_repo
383455 . head ( )
384456 . and_then ( |c| c. peel_to_commit ( ) )
@@ -437,43 +509,45 @@ fn main() -> Result<()> {
437509 // --base can be a reference, but patchable.toml should always have a resolved commit id,
438510 // so that it cannot be changed under our feet (without us knowing so, anyway...).
439511 tracing:: info!( ?base, "resolving base commit-ish" ) ;
440- let base_commit = repo:: resolve_and_fetch_commitish ( & product_repo, & base, & upstream) . context ( FetchBaseCommitSnafu ) ?;
512+ let base_commit = repo:: resolve_and_fetch_commitish ( & product_repo, & base, & upstream)
513+ . context ( FetchBaseCommitSnafu ) ?;
441514 let mut upstream_mirror = None ;
442515
443516 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" ) ;
517+ // Load mirror URL from product-level config
518+ let mirror_url = ctx . load_config ( ) ? . mirror . context ( ValidateConfigSnafu {
519+ reason : "mirror repository needs to be specified in product config when using --mirror flag" . to_string ( ) ,
520+ paths : vec ! [ ctx . product_config_path ( ) ] ,
521+ } ) ? ;
449522
450523 // Add mirror remote
451- let mut mirror_remote = product_repo
452- . remote_anonymous ( & mirror_url)
453- . context ( AddMirrorRemoteSnafu { url : mirror_url. clone ( ) } ) ?;
524+ let mut mirror_remote =
525+ product_repo
526+ . remote_anonymous ( & mirror_url)
527+ . context ( AddMirrorRemoteSnafu {
528+ url : mirror_url. clone ( ) ,
529+ } ) ?;
454530
455531 // Push the base commit to the mirror
456532 tracing:: info!( commit = %base_commit, base = base, url = mirror_url, "pushing commit to mirror" ) ;
457533 let mut callbacks = git2:: RemoteCallbacks :: new ( ) ;
458534 callbacks. credentials ( |url, username_from_url, _allowed_types| {
459535 git2:: Cred :: credential_helper (
460- & git2:: Config :: open_default ( ) . unwrap ( ) , // Use default git config
536+ & git2:: Config :: open_default ( )
537+ . expect ( "failed to open default Git configuration" ) , // Use default git config,
461538 url,
462539 username_from_url,
463540 )
464541 } ) ;
465542
466543 // Add progress tracking for push operation
467- let ( span_push, mut quant_push) = utils:: setup_progress_tracking ( tracing:: info_span!( "pushing" ) ) ;
544+ let ( span_push, mut quant_push) =
545+ utils:: setup_progress_tracking ( tracing:: info_span!( "pushing" ) ) ;
468546 let _ = span_push. enter ( ) ;
469547
470548 callbacks. push_transfer_progress ( move |current, total, _| {
471549 if total > 0 {
472- quant_push. update_span_progress (
473- current,
474- total,
475- & span_push,
476- ) ;
550+ quant_push. update_span_progress ( current, total, & span_push) ;
477551 }
478552 } ) ;
479553
@@ -486,7 +560,7 @@ fn main() -> Result<()> {
486560 mirror_remote
487561 . push ( & [ & refspec] , Some ( & mut push_options) )
488562 . context ( PushToMirrorSnafu {
489- url : & mirror_url,
563+ url : mirror_url. clone ( ) ,
490564 refspec : & refspec,
491565 commit : base_commit,
492566 } ) ?;
@@ -500,11 +574,11 @@ fn main() -> Result<()> {
500574
501575 tracing:: info!( "saving configuration" ) ;
502576 let config = ProductVersionConfig {
503- upstream,
577+ upstream : Some ( upstream ) ,
504578 mirror : upstream_mirror,
505- base : base_commit,
579+ base : Some ( base_commit) ,
506580 } ;
507- let config_path = ctx. config_path ( ) ;
581+ let config_path = ctx. version_config_path ( ) ;
508582 if let Some ( config_dir) = config_path. parent ( ) {
509583 std:: fs:: create_dir_all ( config_dir)
510584 . context ( CreatePatchDirSnafu { path : config_dir } ) ?;
0 commit comments