@@ -26,9 +26,9 @@ struct ProductVersion {
26
26
27
27
#[ derive( Deserialize , Serialize ) ]
28
28
struct 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 > ,
32
32
mirror : Option < String > ,
33
33
}
34
34
@@ -39,15 +39,77 @@ struct ProductVersionContext {
39
39
40
40
impl ProductVersionContext {
41
41
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)
51
113
}
52
114
53
115
/// The root directory for files related to the product (across all versions).
@@ -63,10 +125,15 @@ impl ProductVersionContext {
63
125
}
64
126
65
127
/// The patchable configuration file for the product version.
66
- fn config_path ( & self ) -> PathBuf {
128
+ fn version_config_path ( & self ) -> PathBuf {
67
129
self . patch_dir ( ) . join ( "patchable.toml" )
68
130
}
69
131
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
+
70
137
/// The directory containing all ephemeral data used by patchable for the product (across all versions).
71
138
///
72
139
/// 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 {
190
257
source : toml:: de:: Error ,
191
258
path : PathBuf ,
192
259
} ,
260
+ #[ snafu( display( "failed to validate config: {reason:?} (used config files: {paths:?})" ) ) ]
261
+ ValidateConfig { reason : String , paths : Vec < PathBuf > } ,
193
262
#[ snafu( display( "failed to serialize config" ) ) ]
194
263
SerializeConfig { source : toml:: ser:: Error } ,
195
264
#[ snafu( display( "failed to create patch dir at {path:?}" ) ) ]
@@ -203,8 +272,6 @@ pub enum Error {
203
272
path : PathBuf ,
204
273
} ,
205
274
206
- #[ snafu( display( "failed to parse upstream URL {url:?} to extract repository name" ) ) ]
207
- ParseUpstreamUrl { url : String } ,
208
275
#[ snafu( display( "failed to add temporary mirror remote for {url:?}" ) ) ]
209
276
AddMirrorRemote { source : git2:: Error , url : String } ,
210
277
#[ snafu( display( "failed to push commit {commit} (as {refspec}) to mirror {url:?}" ) ) ]
@@ -302,10 +369,14 @@ fn main() -> Result<()> {
302
369
let product_repo = repo:: ensure_bare_repo ( & product_repo_root)
303
370
. context ( OpenProductRepoForCheckoutSnafu ) ?;
304
371
372
+ // Presence of `base` and `upstream` already validated by load_config, so unwrap is safe here
305
373
let base_commit = repo:: resolve_and_fetch_commitish (
306
374
& 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 ( ) ) ,
309
380
)
310
381
. context ( FetchBaseCommitSnafu ) ?;
311
382
let base_branch = ctx. base_branch ( ) ;
@@ -378,7 +449,8 @@ fn main() -> Result<()> {
378
449
path : product_worktree_root,
379
450
} ) ?;
380
451
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 ( ) ;
382
454
let original_leaf_commit = product_version_repo
383
455
. head ( )
384
456
. and_then ( |c| c. peel_to_commit ( ) )
@@ -437,43 +509,45 @@ fn main() -> Result<()> {
437
509
// --base can be a reference, but patchable.toml should always have a resolved commit id,
438
510
// so that it cannot be changed under our feet (without us knowing so, anyway...).
439
511
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 ) ?;
441
514
let mut upstream_mirror = None ;
442
515
443
516
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
+ } ) ? ;
449
522
450
523
// 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
+ } ) ?;
454
530
455
531
// Push the base commit to the mirror
456
532
tracing:: info!( commit = %base_commit, base = base, url = mirror_url, "pushing commit to mirror" ) ;
457
533
let mut callbacks = git2:: RemoteCallbacks :: new ( ) ;
458
534
callbacks. credentials ( |url, username_from_url, _allowed_types| {
459
535
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,
461
538
url,
462
539
username_from_url,
463
540
)
464
541
} ) ;
465
542
466
543
// 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" ) ) ;
468
546
let _ = span_push. enter ( ) ;
469
547
470
548
callbacks. push_transfer_progress ( move |current, total, _| {
471
549
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) ;
477
551
}
478
552
} ) ;
479
553
@@ -486,7 +560,7 @@ fn main() -> Result<()> {
486
560
mirror_remote
487
561
. push ( & [ & refspec] , Some ( & mut push_options) )
488
562
. context ( PushToMirrorSnafu {
489
- url : & mirror_url,
563
+ url : mirror_url. clone ( ) ,
490
564
refspec : & refspec,
491
565
commit : base_commit,
492
566
} ) ?;
@@ -500,11 +574,11 @@ fn main() -> Result<()> {
500
574
501
575
tracing:: info!( "saving configuration" ) ;
502
576
let config = ProductVersionConfig {
503
- upstream,
577
+ upstream : Some ( upstream ) ,
504
578
mirror : upstream_mirror,
505
- base : base_commit,
579
+ base : Some ( base_commit) ,
506
580
} ;
507
- let config_path = ctx. config_path ( ) ;
581
+ let config_path = ctx. version_config_path ( ) ;
508
582
if let Some ( config_dir) = config_path. parent ( ) {
509
583
std:: fs:: create_dir_all ( config_dir)
510
584
. context ( CreatePatchDirSnafu { path : config_dir } ) ?;
0 commit comments