@@ -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( default , 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.
@@ -141,20 +208,11 @@ enum Cmd {
141
208
#[ clap( flatten) ]
142
209
pv : ProductVersion ,
143
210
144
- /// The upstream URL (such as https://github.com/apache/druid.git)
145
- #[ clap( long) ]
146
- upstream : String ,
147
-
148
211
/// The upstream commit-ish (such as druid-28.0.0) that the patch series applies to
149
212
///
150
213
/// Refs (such as tags and branches) will be resolved to commit IDs.
151
214
#[ clap( long) ]
152
215
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 ,
158
216
} ,
159
217
160
218
/// Shows the patch directory for a given product version
@@ -190,6 +248,8 @@ pub enum Error {
190
248
source : toml:: de:: Error ,
191
249
path : PathBuf ,
192
250
} ,
251
+ #[ snafu( display( "failed to validate config: {reason:?} (used config files: {paths:?})" ) ) ]
252
+ ValidateConfig { reason : String , paths : Vec < PathBuf > } ,
193
253
#[ snafu( display( "failed to serialize config" ) ) ]
194
254
SerializeConfig { source : toml:: ser:: Error } ,
195
255
#[ snafu( display( "failed to create patch dir at {path:?}" ) ) ]
@@ -203,8 +263,6 @@ pub enum Error {
203
263
path : PathBuf ,
204
264
} ,
205
265
206
- #[ snafu( display( "failed to parse upstream URL {url:?} to extract repository name" ) ) ]
207
- ParseUpstreamUrl { url : String } ,
208
266
#[ snafu( display( "failed to add temporary mirror remote for {url:?}" ) ) ]
209
267
AddMirrorRemote { source : git2:: Error , url : String } ,
210
268
#[ snafu( display( "failed to push commit {commit} (as {refspec}) to mirror {url:?}" ) ) ]
@@ -302,10 +360,14 @@ fn main() -> Result<()> {
302
360
let product_repo = repo:: ensure_bare_repo ( & product_repo_root)
303
361
. context ( OpenProductRepoForCheckoutSnafu ) ?;
304
362
363
+ // Presence of `base` and `upstream` already validated by load_config, so unwrap is safe here
305
364
let base_commit = repo:: resolve_and_fetch_commitish (
306
365
& product_repo,
307
- & config. base . to_string ( ) ,
308
- config. mirror . as_deref ( ) . unwrap_or ( & config. upstream ) ,
366
+ & config. base . unwrap ( ) . to_string ( ) ,
367
+ config
368
+ . mirror
369
+ . as_deref ( )
370
+ . unwrap_or ( config. upstream . as_ref ( ) . unwrap ( ) ) ,
309
371
)
310
372
. context ( FetchBaseCommitSnafu ) ?;
311
373
let base_branch = ctx. base_branch ( ) ;
@@ -378,7 +440,8 @@ fn main() -> Result<()> {
378
440
path : product_worktree_root,
379
441
} ) ?;
380
442
381
- let base_commit = config. base ;
443
+ // Presence of `base` already validated by load_config, so unwrap is safe here
444
+ let base_commit = config. base . unwrap ( ) ;
382
445
let original_leaf_commit = product_version_repo
383
446
. head ( )
384
447
. and_then ( |c| c. peel_to_commit ( ) )
@@ -417,9 +480,7 @@ fn main() -> Result<()> {
417
480
418
481
Cmd :: Init {
419
482
pv,
420
- upstream,
421
483
base,
422
- mirror,
423
484
} => {
424
485
let ctx = ProductVersionContext {
425
486
pv,
@@ -434,77 +495,75 @@ fn main() -> Result<()> {
434
495
. in_scope ( || repo:: ensure_bare_repo ( & product_repo_root) )
435
496
. context ( OpenProductRepoForCheckoutSnafu ) ?;
436
497
498
+ let config = ctx. load_config ( ) ?;
499
+ // Presence of `upstream` already validated by load_config, so unwrap is safe here
500
+ let upstream = config. upstream . unwrap ( ) ;
501
+
437
502
// --base can be a reference, but patchable.toml should always have a resolved commit id,
438
503
// so that it cannot be changed under our feet (without us knowing so, anyway...).
439
504
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" ) ;
505
+ let base_commit = repo:: resolve_and_fetch_commitish ( & product_repo, & base, & upstream)
506
+ . context ( FetchBaseCommitSnafu ) ?;
449
507
508
+ if let Some ( mirror_url) = config. mirror {
450
509
// Add mirror remote
451
- let mut mirror_remote = product_repo
452
- . remote_anonymous ( & mirror_url)
453
- . context ( AddMirrorRemoteSnafu { url : mirror_url. clone ( ) } ) ?;
510
+ let mut mirror_remote =
511
+ product_repo
512
+ . remote_anonymous ( & mirror_url)
513
+ . context ( AddMirrorRemoteSnafu {
514
+ url : mirror_url. clone ( ) ,
515
+ } ) ?;
454
516
455
517
// Push the base commit to the mirror
456
518
tracing:: info!( commit = %base_commit, base = base, url = mirror_url, "pushing commit to mirror" ) ;
457
519
let mut callbacks = git2:: RemoteCallbacks :: new ( ) ;
458
520
callbacks. credentials ( |url, username_from_url, _allowed_types| {
459
521
git2:: Cred :: credential_helper (
460
- & git2:: Config :: open_default ( ) . unwrap ( ) , // Use default git config
522
+ & git2:: Config :: open_default ( )
523
+ . expect ( "failed to open default Git configuration" ) , // Use default git config,
461
524
url,
462
525
username_from_url,
463
526
)
464
527
} ) ;
465
528
466
529
// Add progress tracking for push operation
467
- let ( span_push, mut quant_push) = utils:: setup_progress_tracking ( tracing:: info_span!( "pushing" ) ) ;
530
+ let ( span_push, mut quant_push) =
531
+ utils:: setup_progress_tracking ( tracing:: info_span!( "pushing" ) ) ;
468
532
let _ = span_push. enter ( ) ;
469
533
470
534
callbacks. push_transfer_progress ( move |current, total, _| {
471
535
if total > 0 {
472
- quant_push. update_span_progress (
473
- current,
474
- total,
475
- & span_push,
476
- ) ;
536
+ quant_push. update_span_progress ( current, total, & span_push) ;
477
537
}
478
538
} ) ;
479
539
480
540
let mut push_options = git2:: PushOptions :: new ( ) ;
481
541
push_options. remote_callbacks ( callbacks) ;
482
542
543
+ // Always push the commit as a Git tag named like the value of `base`
483
544
let refspec = format ! ( "{base_commit}:refs/tags/{base}" ) ;
484
545
tracing:: info!( refspec, "constructed push refspec" ) ;
485
546
486
547
mirror_remote
487
548
. push ( & [ & refspec] , Some ( & mut push_options) )
488
549
. context ( PushToMirrorSnafu {
489
- url : & mirror_url,
550
+ url : mirror_url. clone ( ) ,
490
551
refspec : & refspec,
491
552
commit : base_commit,
492
553
} ) ?;
493
554
494
555
tracing:: info!( "successfully pushed base ref to mirror" ) ;
495
-
496
- upstream_mirror = Some ( mirror_url) ;
497
556
} ;
498
557
499
558
tracing:: info!( ?base, base. commit = ?base_commit, "resolved base commit" ) ;
500
559
501
- tracing:: info!( "saving configuration" ) ;
560
+ tracing:: info!( "saving version-level configuration" ) ;
502
561
let config = ProductVersionConfig {
503
- upstream,
504
- mirror : upstream_mirror ,
505
- base : base_commit,
562
+ upstream : None ,
563
+ mirror : None ,
564
+ base : Some ( base_commit) ,
506
565
} ;
507
- let config_path = ctx. config_path ( ) ;
566
+ let config_path = ctx. version_config_path ( ) ;
508
567
if let Some ( config_dir) = config_path. parent ( ) {
509
568
std:: fs:: create_dir_all ( config_dir)
510
569
. context ( CreatePatchDirSnafu { path : config_dir } ) ?;
0 commit comments