Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • project/automatic_updates
  • issue/automatic_updates-3159719
  • issue/automatic_updates-3227923
  • issue/automatic_updates-3227927
  • issue/automatic_updates-3227122
  • issue/automatic_updates-3227940
  • issue/automatic_updates-3228095
  • issue/automatic_updates-3228101
  • issue/automatic_updates-3228115
  • issue/automatic_updates-3228125
  • issue/automatic_updates-3228359
  • issue/automatic_updates-3228392
  • issue/automatic_updates-3225753
  • issue/automatic_updates-3227588
  • issue/automatic_updates-3228521
  • issue/automatic_updates-3228539
  • issue/automatic_updates-3226570
  • issue/automatic_updates-3228806
  • issue/automatic_updates-3227575
  • issue/automatic_updates-3229485
  • issue/automatic_updates-3228955
  • issue/automatic_updates-3230024
  • issue/automatic_updates-3230250
  • issue/automatic_updates-3230249
  • issue/automatic_updates-3230264
  • issue/automatic_updates-3230510
  • issue/automatic_updates-3230235
  • issue/automatic_updates-3230050
  • issue/automatic_updates-3230934
  • issue/automatic_updates-3230049
  • issue/automatic_updates-3230507
  • issue/automatic_updates-3231153
  • issue/automatic_updates-3231992
  • issue/automatic_updates-3232003
  • issue/automatic_updates-3232004
  • issue/automatic_updates-3231996
  • issue/automatic_updates-3231999
  • issue/automatic_updates-3230668
  • issue/automatic_updates-3232000
  • issue/automatic_updates-3232420
  • issue/automatic_updates-3232729
  • issue/automatic_updates-3232761
  • issue/automatic_updates-3231997
  • issue/automatic_updates-3232927
  • issue/automatic_updates-3232959
  • issue/automatic_updates-3233124
  • issue/automatic_updates-3230856
  • issue/automatic_updates-3233493
  • issue/automatic_updates-3233587
  • issue/automatic_updates-3233521
  • issue/automatic_updates-3236405
  • issue/automatic_updates-3236299
  • issue/automatic_updates-3238586
  • issue/automatic_updates-3238647
  • issue/automatic_updates-3238717
  • issue/automatic_updates-3238846
  • issue/automatic_updates-3238852
  • issue/automatic_updates-3238866
  • issue/automatic_updates-3238714
  • issue/automatic_updates-3239103
  • issue/automatic_updates-3233138
  • issue/automatic_updates-3239466
  • issue/automatic_updates-3239645
  • issue/automatic_updates-3239673
  • issue/automatic_updates-3277775
  • issue/automatic_updates-3280923
  • issue/automatic_updates-3275315
  • issue/automatic_updates-3279527
  • issue/automatic_updates-3279733
  • issue/automatic_updates-3281405
  • issue/automatic_updates-3281397
  • issue/automatic_updates-3280204
  • issue/automatic_updates-3277817
  • issue/automatic_updates-3277230
  • issue/automatic_updates-3276534
  • issue/automatic_updates-3279538
  • issue/automatic_updates-3280168
  • issue/automatic_updates-3271502
  • issue/automatic_updates-3283644
  • issue/automatic_updates-3302524
  • issue/automatic_updates-3275883
  • issue/automatic_updates-3280180
  • issue/automatic_updates-3274837
  • issue/automatic_updates-3281473
  • issue/automatic_updates-3281634
  • issue/automatic_updates-3239889
  • issue/automatic_updates-3240753
  • issue/automatic_updates-3233103
  • issue/automatic_updates-3240971
  • issue/automatic_updates-3233564
  • issue/automatic_updates-3241380
  • issue/automatic_updates-3241105
  • issue/automatic_updates-3242626
  • issue/automatic_updates-3242724
  • issue/automatic_updates-3243057
  • issue/automatic_updates-3243348
  • issue/automatic_updates-3243339
  • issue/automatic_updates-3243422
  • issue/automatic_updates-3243436
  • issue/automatic_updates-3243600
  • issue/automatic_updates-3243851
  • issue/automatic_updates-3243405
  • issue/automatic_updates-3244338
  • issue/automatic_updates-3244337
  • issue/automatic_updates-3244360
  • issue/automatic_updates-3244358
  • issue/automatic_updates-3244679
  • issue/automatic_updates-3244939
  • issue/automatic_updates-3244976
  • issue/automatic_updates-3245376
  • issue/automatic_updates-3245388
  • issue/automatic_updates-3245428
  • issue/automatic_updates-3245655
  • issue/automatic_updates-3245766
  • issue/automatic_updates-3245810
  • issue/automatic_updates-3245846
  • issue/automatic_updates-3246036
  • issue/automatic_updates-3246194
  • issue/automatic_updates-3246203
  • issue/automatic_updates-3246283
  • issue/automatic_updates-3279064
  • issue/automatic_updates-3246420
  • issue/automatic_updates-3246638
  • issue/automatic_updates-3246660
  • issue/automatic_updates-3246673
  • issue/automatic_updates-3246695
  • issue/automatic_updates-3245996
  • issue/automatic_updates-3247308
  • issue/automatic_updates-3247478
  • issue/automatic_updates-3247479
  • issue/automatic_updates-3279086
  • issue/automatic_updates-3248312
  • issue/automatic_updates-3248523
  • issue/automatic_updates-3248527
  • issue/automatic_updates-3244412
  • issue/automatic_updates-3248976
  • issue/automatic_updates-3248909
  • issue/automatic_updates-3246507
  • issue/automatic_updates-3249531
  • issue/automatic_updates-3249517
  • issue/automatic_updates-3249983
  • issue/automatic_updates-3249135
  • issue/automatic_updates-3250136
  • issue/automatic_updates-3250386
  • issue/automatic_updates-3251701
  • issue/automatic_updates-3250696
  • issue/automatic_updates-3248911
  • issue/automatic_updates-3227420
  • issue/automatic_updates-3252071
  • issue/automatic_updates-3252097
  • issue/automatic_updates-3252109
  • issue/automatic_updates-3252126
  • issue/automatic_updates-3249130
  • issue/automatic_updates-3251972
  • issue/automatic_updates-3252187
  • issue/automatic_updates-3252328
  • issue/automatic_updates-3252324
  • issue/automatic_updates-3252533
  • issue/automatic_updates-3253055
  • issue/automatic_updates-3253186
  • issue/automatic_updates-3253395
  • issue/automatic_updates-3253400
  • issue/automatic_updates-3253624
  • issue/automatic_updates-3253647
  • issue/automatic_updates-3253649
  • issue/automatic_updates-3253858
  • issue/automatic_updates-3254166
  • issue/automatic_updates-3254207
  • issue/automatic_updates-3279108
  • issue/automatic_updates-3254606
  • issue/automatic_updates-3254616
  • issue/automatic_updates-3254911
  • issue/automatic_updates-3255011
  • issue/automatic_updates-3255014
  • issue/automatic_updates-3255320
  • issue/automatic_updates-3277562
  • issue/automatic_updates-3255678
  • issue/automatic_updates-3256958
  • issue/automatic_updates-3257115
  • issue/automatic_updates-3257432
  • issue/automatic_updates-3257446
  • issue/automatic_updates-3257473
  • issue/automatic_updates-3257134
  • issue/automatic_updates-3257849
  • issue/automatic_updates-3258065
  • issue/automatic_updates-3258056
  • issue/automatic_updates-3258048
  • issue/automatic_updates-3258045
  • issue/automatic_updates-3258464
  • issue/automatic_updates-3258611
  • issue/automatic_updates-3258646
  • issue/automatic_updates-3258661
  • issue/automatic_updates-3258667
  • issue/automatic_updates-3258590
  • issue/automatic_updates-3259228
  • issue/automatic_updates-3259656
  • issue/automatic_updates-3252299
  • issue/automatic_updates-3259664
  • issue/automatic_updates-3259822
  • issue/automatic_updates-3259810
  • issue/automatic_updates-3259814
  • issue/automatic_updates-3260368
  • issue/automatic_updates-3257845
  • issue/automatic_updates-3260664
  • issue/automatic_updates-3260672
  • issue/automatic_updates-3260698
  • issue/automatic_updates-3260662
  • issue/automatic_updates-3260669
  • issue/automatic_updates-3260668
  • issue/automatic_updates-3261637
  • issue/automatic_updates-3261772
  • issue/automatic_updates-3261847
  • issue/automatic_updates-3262016
  • issue/automatic_updates-3262244
  • issue/automatic_updates-3262303
  • issue/automatic_updates-3261642
  • issue/automatic_updates-3262359
  • issue/automatic_updates-3262542
  • issue/automatic_updates-3261098
  • issue/automatic_updates-3260666
  • issue/automatic_updates-3262587
  • issue/automatic_updates-3262965
  • issue/automatic_updates-3263155
  • issue/automatic_updates-3263223
  • issue/automatic_updates-3254755
  • issue/automatic_updates-3261758
  • issue/automatic_updates-3258059
  • issue/automatic_updates-3259196
  • issue/automatic_updates-3263865
  • issue/automatic_updates-3262284
  • issue/automatic_updates-3264554
  • issue/automatic_updates-3248928
  • issue/automatic_updates-3248929
  • issue/automatic_updates-3265072
  • issue/automatic_updates-3265057
  • issue/automatic_updates-3265873
  • issue/automatic_updates-3266092
  • issue/automatic_updates-3266633
  • issue/automatic_updates-3266640
  • issue/automatic_updates-3266687
  • issue/automatic_updates-3266981
  • issue/automatic_updates-3267387
  • issue/automatic_updates-3267389
  • issue/automatic_updates-3267411
  • issue/automatic_updates-3267603
  • issue/automatic_updates-3265874
  • issue/automatic_updates-3267577
  • issue/automatic_updates-3267386
  • issue/automatic_updates-3268363
  • issue/automatic_updates-3268868
  • issue/automatic_updates-3268612
  • issue/automatic_updates-3269097
  • issue/automatic_updates-3267632
  • issue/automatic_updates-3270736
  • issue/automatic_updates-3271144
  • issue/automatic_updates-3271226
  • issue/automatic_updates-3271078
  • issue/automatic_updates-3271371
  • issue/automatic_updates-3267646
  • issue/automatic_updates-3272060
  • issue/automatic_updates-3265031
  • issue/automatic_updates-3253591
  • issue/automatic_updates-3272520
  • issue/automatic_updates-3272061
  • issue/automatic_updates-3272785
  • issue/automatic_updates-3273011
  • issue/automatic_updates-3273008
  • issue/automatic_updates-3273364
  • issue/automatic_updates-3273407
  • issue/automatic_updates-3271235
  • issue/automatic_updates-3273006
  • issue/automatic_updates-3271240
  • issue/automatic_updates-3271468
  • issue/automatic_updates-3272326
  • issue/automatic_updates-3273693
  • issue/automatic_updates-3264849
  • issue/automatic_updates-3273807
  • issue/automatic_updates-3273017
  • issue/automatic_updates-3274269
  • issue/automatic_updates-3274273
  • issue/automatic_updates-3274292
  • issue/automatic_updates-3273812
  • issue/automatic_updates-3274323
  • issue/automatic_updates-3274633
  • issue/automatic_updates-3274858
  • issue/automatic_updates-3274830
  • issue/automatic_updates-3274892
  • issue/automatic_updates-3275075
  • issue/automatic_updates-3275256
  • issue/automatic_updates-3275282
  • issue/automatic_updates-3275320
  • issue/automatic_updates-3275323
  • issue/automatic_updates-3275324
  • issue/automatic_updates-3275317
  • issue/automatic_updates-3275313
  • issue/automatic_updates-3275357
  • issue/automatic_updates-3275474
  • issue/automatic_updates-3275311
  • issue/automatic_updates-3275546
  • issue/automatic_updates-3275508
  • issue/automatic_updates-3275810
  • issue/automatic_updates-3275860
  • issue/automatic_updates-3275865
  • issue/automatic_updates-3275886
  • issue/automatic_updates-3275887
  • issue/automatic_updates-3276031
  • issue/automatic_updates-3276041
  • issue/automatic_updates-3276072
  • issue/automatic_updates-3276159
  • issue/automatic_updates-3276255
  • issue/automatic_updates-3275825
  • issue/automatic_updates-3276661
  • issue/automatic_updates-3275369
  • issue/automatic_updates-3277014
  • issue/automatic_updates-3277035
  • issue/automatic_updates-3272813
  • issue/automatic_updates-3277211
  • issue/automatic_updates-3277229
  • issue/automatic_updates-3275316
  • issue/automatic_updates-3274047
  • issue/automatic_updates-3277815
  • issue/automatic_updates-3276662
  • issue/automatic_updates-3277235
  • issue/automatic_updates-3278411
  • issue/automatic_updates-3278435
  • issue/automatic_updates-3279229
  • issue/automatic_updates-3277007
  • issue/automatic_updates-3284346
  • issue/automatic_updates-3239759
  • issue/automatic_updates-3284495
  • issue/automatic_updates-3284530
  • issue/automatic_updates-3282677
  • issue/automatic_updates-3281379
  • issue/automatic_updates-3285408
  • issue/automatic_updates-3285631
  • issue/automatic_updates-3285669
  • issue/automatic_updates-3273014
  • issue/automatic_updates-3273369
  • issue/automatic_updates-3285491
  • issue/automatic_updates-3285898
  • issue/automatic_updates-3287251
  • issue/automatic_updates-3291147
  • issue/automatic_updates-3286650
  • issue/automatic_updates-3291770
  • issue/automatic_updates-3280403
  • issue/automatic_updates-3291959
  • issue/automatic_updates-3287398
  • issue/automatic_updates-3285145
  • issue/automatic_updates-3292027
  • issue/automatic_updates-3278445
  • issue/automatic_updates-3292933
  • issue/automatic_updates-3292958
  • issue/automatic_updates-3293157
  • issue/automatic_updates-3292956
  • issue/automatic_updates-3293146
  • issue/automatic_updates-3293381
  • issue/automatic_updates-3293449
  • issue/automatic_updates-3293427
  • issue/automatic_updates-3293422
  • issue/automatic_updates-3293685
  • issue/automatic_updates-3291730
  • issue/automatic_updates-3293866
  • issue/automatic_updates-3294338
  • issue/automatic_updates-3293656
  • issue/automatic_updates-3294600
  • issue/automatic_updates-3294335
  • issue/automatic_updates-3295013
  • issue/automatic_updates-3284936
  • issue/automatic_updates-3295596
  • issue/automatic_updates-3295758
  • issue/automatic_updates-3295791
  • issue/automatic_updates-3295830
  • issue/automatic_updates-3295874
  • issue/automatic_updates-3295897
  • issue/automatic_updates-3296074
  • issue/automatic_updates-3295965
  • issue/automatic_updates-3296181
  • issue/automatic_updates-3296065
  • issue/automatic_updates-3296178
  • issue/automatic_updates-3296261
  • issue/automatic_updates-3293148
  • issue/automatic_updates-3303953
  • issue/automatic_updates-3304367
  • issue/automatic_updates-3304640
  • issue/automatic_updates-3304836
  • issue/automatic_updates-3304142
  • issue/automatic_updates-3298349
  • issue/automatic_updates-3298444
  • issue/automatic_updates-3298510
  • issue/automatic_updates-3298431
  • issue/automatic_updates-3298863
  • issue/automatic_updates-3298877
  • issue/automatic_updates-3298904
  • issue/automatic_updates-3298951
  • issue/automatic_updates-3299101
  • issue/automatic_updates-3299417
  • issue/automatic_updates-3293417
  • issue/automatic_updates-3299612
  • issue/automatic_updates-3299093
  • issue/automatic_updates-3299087
  • issue/automatic_updates-3300006
  • issue/automatic_updates-3300036
  • issue/automatic_updates-3293150
  • issue/automatic_updates-3303807
  • issue/automatic_updates-3303929
  • issue/automatic_updates-3304417
  • issue/automatic_updates-3303727
  • issue/automatic_updates-3305527
  • issue/automatic_updates-3301844
  • issue/automatic_updates-3302527
  • issue/automatic_updates-3302673
  • issue/automatic_updates-3302897
  • issue/automatic_updates-3303143
  • issue/automatic_updates-3303168
  • issue/automatic_updates-3303174
  • issue/automatic_updates-3303185
  • issue/automatic_updates-3303200
  • issue/automatic_updates-3303113
  • issue/automatic_updates-3304036
  • issue/automatic_updates-3304165
  • issue/automatic_updates-3304583
  • issue/automatic_updates-3303902
  • issue/automatic_updates-3304651
  • issue/automatic_updates-3305312
  • issue/automatic_updates-3305612
  • issue/automatic_updates-3305568
  • issue/automatic_updates-3305773
  • issue/automatic_updates-3305874
  • issue/automatic_updates-3310901
  • issue/automatic_updates-3310929
  • issue/automatic_updates-3310936
  • issue/automatic_updates-3310972
  • issue/automatic_updates-3311001
  • issue/automatic_updates-3311020
  • issue/automatic_updates-3305240
  • issue/automatic_updates-3305994
  • issue/automatic_updates-3305564
  • issue/automatic_updates-3305998
  • issue/automatic_updates-3306600
  • issue/automatic_updates-3306631
  • issue/automatic_updates-3307086
  • issue/automatic_updates-3307103
  • issue/automatic_updates-3307168
  • issue/automatic_updates-3306163
  • issue/automatic_updates-3304617
  • issue/automatic_updates-3307163
  • issue/automatic_updates-3307398
  • issue/automatic_updates-3307478
  • issue/automatic_updates-3307369
  • issue/automatic_updates-3308404
  • issue/automatic_updates-3307611
  • issue/automatic_updates-3308372
  • issue/automatic_updates-3308686
  • issue/automatic_updates-3308886
  • issue/automatic_updates-3309220
  • issue/automatic_updates-3309270
  • issue/automatic_updates-3308711
  • issue/automatic_updates-3309486
  • issue/automatic_updates-3305167
  • issue/automatic_updates-3309676
  • issue/automatic_updates-3309025
  • issue/automatic_updates-3309891
  • issue/automatic_updates-3308365
  • issue/automatic_updates-3310000
  • issue/automatic_updates-3309205
  • issue/automatic_updates-3248975
  • issue/automatic_updates-3310702
  • issue/automatic_updates-3272313
  • issue/automatic_updates-3310990
  • issue/automatic_updates-3310997
  • issue/automatic_updates-3311200
  • issue/automatic_updates-3310696
  • issue/automatic_updates-3311265
  • issue/automatic_updates-3303167
  • issue/automatic_updates-3310946
  • issue/automatic_updates-3308828
  • issue/automatic_updates-3310666
  • issue/automatic_updates-3109082
  • issue/automatic_updates-3311534
  • issue/automatic_updates-3311436
  • issue/automatic_updates-3308843
  • issue/automatic_updates-3303900
  • issue/automatic_updates-3312085
  • issue/automatic_updates-3312420
  • issue/automatic_updates-3312373
  • issue/automatic_updates-3312421
  • issue/automatic_updates-3312619
  • issue/automatic_updates-3312779
  • issue/automatic_updates-3275991
  • issue/automatic_updates-3312938
  • issue/automatic_updates-3312937
  • issue/automatic_updates-3312960
  • issue/automatic_updates-3312669
  • issue/automatic_updates-3312981
  • issue/automatic_updates-3313319
  • issue/automatic_updates-3313346
  • issue/automatic_updates-3310914
  • issue/automatic_updates-3317220
  • issue/automatic_updates-3317232
  • issue/automatic_updates-3317278
  • issue/automatic_updates-3317409
  • issue/automatic_updates-3317267
  • issue/automatic_updates-3317599
  • issue/automatic_updates-3317988
  • issue/automatic_updates-3318313
  • issue/automatic_updates-3313630
  • issue/automatic_updates-3313717
  • issue/automatic_updates-3313947
  • issue/automatic_updates-3313507
  • issue/automatic_updates-3276645
  • issue/automatic_updates-3313349
  • issue/automatic_updates-3314137
  • issue/automatic_updates-3314143
  • issue/automatic_updates-3304365
  • issue/automatic_updates-3317796
  • issue/automatic_updates-3317996
  • issue/automatic_updates-3316484
  • issue/automatic_updates-3313414
  • issue/automatic_updates-3314764
  • issue/automatic_updates-3314771
  • issue/automatic_updates-3314787
  • issue/automatic_updates-3314805
  • issue/automatic_updates-3314734
  • issue/automatic_updates-3298889
  • issue/automatic_updates-3314803
  • issue/automatic_updates-3315139
  • issue/automatic_updates-3314946
  • issue/automatic_updates-3315449
  • issue/automatic_updates-3315798
  • issue/automatic_updates-3315834
  • issue/automatic_updates-3309602
  • issue/automatic_updates-3316115
  • issue/automatic_updates-3316131
  • issue/automatic_updates-3316293
  • issue/automatic_updates-3316318
  • issue/automatic_updates-3310729
  • issue/automatic_updates-3315700
  • issue/automatic_updates-3316668
  • issue/automatic_updates-3316721
  • issue/automatic_updates-3306283
  • issue/automatic_updates-3316611
  • issue/automatic_updates-3316895
  • issue/automatic_updates-3317385
  • issue/automatic_updates-3318625
  • issue/automatic_updates-3318770
  • issue/automatic_updates-3318846
  • issue/automatic_updates-3318927
  • issue/automatic_updates-3318933
  • issue/automatic_updates-3319044
  • issue/automatic_updates-3319045
  • issue/automatic_updates-3325716
  • issue/automatic_updates-3324421
  • issue/automatic_updates-3320486
  • issue/automatic_updates-3320558
  • issue/automatic_updates-3321206
  • issue/automatic_updates-3326934
  • issue/automatic_updates-3320755
  • issue/automatic_updates-3328765
  • issue/automatic_updates-3325654
  • issue/automatic_updates-3325869
  • issue/automatic_updates-3277034
  • issue/automatic_updates-3328516
  • issue/automatic_updates-3322546
  • issue/automatic_updates-3320487
  • issue/automatic_updates-3320792
  • issue/automatic_updates-3320815
  • issue/automatic_updates-3320638
  • issue/automatic_updates-3321256
  • issue/automatic_updates-3321684
  • issue/automatic_updates-3325522
  • issue/automatic_updates-3326334
  • issue/automatic_updates-3328234
  • issue/automatic_updates-3328740
  • issue/automatic_updates-3316932
  • issue/automatic_updates-3321386
  • issue/automatic_updates-3321933
  • issue/automatic_updates-3321282
  • issue/automatic_updates-3326801
  • issue/automatic_updates-3327753
  • issue/automatic_updates-3328742
  • issue/automatic_updates-3322313
  • issue/automatic_updates-3322203
  • issue/automatic_updates-3322150
  • issue/automatic_updates-3322404
  • issue/automatic_updates-3321236
  • issue/automatic_updates-3321994
  • issue/automatic_updates-3322589
  • issue/automatic_updates-3321904
  • issue/automatic_updates-3316855
  • issue/automatic_updates-3322931
  • issue/automatic_updates-3322913
  • issue/automatic_updates-3323037
  • issue/automatic_updates-3322918
  • issue/automatic_updates-3323211
  • issue/automatic_updates-3323565
  • issue/automatic_updates-3320824
  • issue/automatic_updates-3320836
  • issue/automatic_updates-3329002
  • issue/automatic_updates-3321971
  • issue/automatic_updates-3299094
  • issue/automatic_updates-3248544
  • issue/automatic_updates-3330139
  • issue/automatic_updates-3330365
  • issue/automatic_updates-3330712
  • issue/automatic_updates-3318065
  • issue/automatic_updates-3331310
  • issue/automatic_updates-3322917
  • issue/automatic_updates-3330140
  • issue/automatic_updates-3323003
  • issue/automatic_updates-3331355
  • issue/automatic_updates-3334054
  • issue/automatic_updates-3323706
  • issue/automatic_updates-3331168
  • issue/automatic_updates-3343430
  • issue/automatic_updates-3344562
  • issue/automatic_updates-3345039
  • issue/automatic_updates-3344039
  • issue/automatic_updates-3345313
  • issue/automatic_updates-3331471
  • issue/automatic_updates-3334994
  • issue/automatic_updates-3327229
  • issue/automatic_updates-3343889
  • issue/automatic_updates-3344124
  • issue/automatic_updates-3344689
  • issue/automatic_updates-3316368
  • issue/automatic_updates-3345767
  • issue/automatic_updates-3317815
  • issue/automatic_updates-3328600
  • issue/automatic_updates-3332256
  • issue/automatic_updates-3335766
  • issue/automatic_updates-3335802
  • issue/automatic_updates-3335908
  • issue/automatic_updates-3336243
  • issue/automatic_updates-3336247
  • issue/automatic_updates-3336255
  • issue/automatic_updates-3336259
  • issue/automatic_updates-3337062
  • issue/automatic_updates-3337068
  • issue/automatic_updates-3311229
  • issue/automatic_updates-3337697
  • issue/automatic_updates-3343768
  • issue/automatic_updates-3344127
  • issue/automatic_updates-3344595
  • issue/automatic_updates-3345763
  • issue/automatic_updates-3345765
  • issue/automatic_updates-3345771
  • issue/automatic_updates-3337760
  • issue/automatic_updates-3337049
  • issue/automatic_updates-3342430
  • issue/automatic_updates-3319507
  • issue/automatic_updates-3344583
  • issue/automatic_updates-3345764
  • issue/automatic_updates-3345768
  • issue/automatic_updates-3338667
  • issue/automatic_updates-3339016
  • issue/automatic_updates-3343827
  • issue/automatic_updates-3344556
  • issue/automatic_updates-3338666
  • issue/automatic_updates-3345649
  • issue/automatic_updates-3339657
  • issue/automatic_updates-3339719
  • issue/automatic_updates-3338789
  • issue/automatic_updates-3340022
  • issue/automatic_updates-3340284
  • issue/automatic_updates-3340638
  • issue/automatic_updates-3316617
  • issue/automatic_updates-3341224
  • issue/automatic_updates-3337953
  • issue/automatic_updates-3339714
  • issue/automatic_updates-3341708
  • issue/automatic_updates-3341841
  • issue/automatic_updates-3326486
  • issue/automatic_updates-3321474
  • issue/automatic_updates-3341974
  • issue/automatic_updates-3342120
  • issue/automatic_updates-3342227
  • issue/automatic_updates-3323461
  • issue/automatic_updates-3342137
  • issue/automatic_updates-3342460
  • issue/automatic_updates-3342364
  • issue/automatic_updates-3345028
  • issue/automatic_updates-3345549
  • issue/automatic_updates-3345881
  • issue/automatic_updates-3345762
  • issue/automatic_updates-3342726
  • issue/automatic_updates-3337667
  • issue/automatic_updates-3343463
  • issue/automatic_updates-3345766
  • issue/automatic_updates-3345754
  • issue/automatic_updates-3345633
  • issue/automatic_updates-3345772
  • issue/automatic_updates-3345769
  • issue/automatic_updates-3345761
  • issue/automatic_updates-3345755
  • issue/automatic_updates-3345760
  • issue/automatic_updates-3345757
  • issue/automatic_updates-3346520
  • issue/automatic_updates-3346545
  • issue/automatic_updates-3337054
  • issue/automatic_updates-3346628
  • issue/automatic_updates-3346717
  • issue/automatic_updates-3346594
  • issue/automatic_updates-3346659
  • issue/automatic_updates-3347031
  • issue/automatic_updates-3346547
  • issue/automatic_updates-3347164
  • issue/automatic_updates-3318306
  • issue/automatic_updates-3345646
  • issue/automatic_updates-3347165
  • issue/automatic_updates-3347959
  • issue/automatic_updates-3347267
  • issue/automatic_updates-3338346
  • issue/automatic_updates-3348122
  • issue/automatic_updates-3348129
  • issue/automatic_updates-3348162
  • issue/automatic_updates-3348276
  • issue/automatic_updates-3348441
  • issue/automatic_updates-3351604
  • issue/automatic_updates-3316843
  • issue/automatic_updates-3351908
  • issue/automatic_updates-3352731
  • issue/automatic_updates-3352898
  • issue/automatic_updates-3348866
  • issue/automatic_updates-3342817
  • issue/automatic_updates-3348159
  • issue/automatic_updates-3349142
  • issue/automatic_updates-3349966
  • issue/automatic_updates-3350909
  • issue/automatic_updates-3351594
  • issue/automatic_updates-3351883
  • issue/automatic_updates-3351925
  • issue/automatic_updates-3352198
  • issue/automatic_updates-3334552
  • issue/automatic_updates-3342790
  • issue/automatic_updates-3345222
  • issue/automatic_updates-3351212
  • issue/automatic_updates-3351093
  • issue/automatic_updates-3351247
  • issue/automatic_updates-3351962
  • issue/automatic_updates-3345641
  • issue/automatic_updates-3338940
  • issue/automatic_updates-3343721
  • issue/automatic_updates-3351069
  • issue/automatic_updates-3353219
  • issue/automatic_updates-3159920
  • issue/automatic_updates-3352355
  • issue/automatic_updates-3341469
  • issue/automatic_updates-3354099
  • issue/automatic_updates-3339659
  • issue/automatic_updates-3354312
  • issue/automatic_updates-3351895
  • issue/automatic_updates-3354249
  • issue/automatic_updates-3353270
  • issue/automatic_updates-3354003
  • issue/automatic_updates-3355074
  • issue/automatic_updates-3355094
  • issue/automatic_updates-3355105
  • issue/automatic_updates-3355162
  • issue/automatic_updates-3355446
  • issue/automatic_updates-3355045
  • issue/automatic_updates-3355558
  • issue/automatic_updates-3355553
  • issue/automatic_updates-3354594
  • issue/automatic_updates-3355609
  • issue/automatic_updates-3354325
  • issue/automatic_updates-3356395
  • issue/automatic_updates-3356638
  • issue/automatic_updates-3356640
  • issue/automatic_updates-3354701
  • issue/automatic_updates-3355463
  • issue/automatic_updates-3356804
  • issue/automatic_updates-3357721
  • issue/automatic_updates-3357578
  • issue/automatic_updates-3357657
  • issue/automatic_updates-3304108
  • issue/automatic_updates-3357941
  • issue/automatic_updates-3358012
  • issue/automatic_updates-3345535
  • issue/automatic_updates-3354827
  • issue/automatic_updates-3358570
  • issue/automatic_updates-3358878
  • issue/automatic_updates-3355628
  • issue/automatic_updates-3358504
  • issue/automatic_updates-3357969
  • issue/automatic_updates-3359609
  • issue/automatic_updates-3359727
  • issue/automatic_updates-3359825
  • issue/automatic_updates-3359820
  • issue/automatic_updates-3360548
  • issue/automatic_updates-3357480
  • issue/automatic_updates-3360656
  • issue/automatic_updates-3360763
  • issue/automatic_updates-3360655
  • issue/automatic_updates-3355618
  • issue/automatic_updates-3349351
  • issue/automatic_updates-3362110
  • issue/automatic_updates-3362143
  • issue/automatic_updates-3362589
  • issue/automatic_updates-3349004
  • issue/automatic_updates-3362507
  • issue/automatic_updates-3362746
  • issue/automatic_updates-3362978
  • issue/automatic_updates-3363259
  • issue/automatic_updates-3363725
  • issue/automatic_updates-3363926
  • issue/automatic_updates-3363937
  • issue/automatic_updates-3364514
  • issue/automatic_updates-3284443
  • issue/automatic_updates-3363938
  • issue/automatic_updates-3364731
  • issue/automatic_updates-3364748
  • issue/automatic_updates-3364735
  • issue/automatic_updates-3364725
  • issue/automatic_updates-3359670
  • issue/automatic_updates-3365133
  • issue/automatic_updates-3365151
  • issue/automatic_updates-3365177
  • issue/automatic_updates-3364958
  • issue/automatic_updates-3365390
  • issue/automatic_updates-3365414
  • issue/automatic_updates-3346644
  • issue/automatic_updates-3366267
  • issue/automatic_updates-3366289
  • issue/automatic_updates-3366499
  • issue/automatic_updates-3365437
  • issue/automatic_updates-3354914
  • issue/automatic_updates-3253828
  • issue/automatic_updates-3345484
  • issue/automatic_updates-3352846
  • issue/automatic_updates-3368741
  • issue/automatic_updates-3406008
  • issue/automatic_updates-3406122
  • issue/automatic_updates-3370197
  • issue/automatic_updates-3366271
  • issue/automatic_updates-3364135
  • issue/automatic_updates-3370603
  • issue/automatic_updates-3400146
  • issue/automatic_updates-3406010
  • issue/automatic_updates-3404429
  • issue/automatic_updates-3371212
  • issue/automatic_updates-3371076
  • issue/automatic_updates-3372673
  • issue/automatic_updates-3368808
  • issue/automatic_updates-3374175
  • issue/automatic_updates-3374739
  • issue/automatic_updates-3374753
  • issue/automatic_updates-3375679
  • issue/automatic_updates-3377237
  • issue/automatic_updates-3377245
  • issue/automatic_updates-3378774
  • issue/automatic_updates-3378793
  • issue/automatic_updates-3360485
  • issue/automatic_updates-3380698
  • issue/automatic_updates-3375940
  • issue/automatic_updates-3381294
  • issue/automatic_updates-3381484
  • issue/automatic_updates-3382942
  • issue/automatic_updates-3383462
  • issue/automatic_updates-3383451
  • issue/automatic_updates-3384359
  • issue/automatic_updates-3384637
  • issue/automatic_updates-3375640
  • issue/automatic_updates-3386135
  • issue/automatic_updates-3386533
  • issue/automatic_updates-3387610
  • issue/automatic_updates-3387637
  • issue/automatic_updates-3387656
  • issue/automatic_updates-3389157
  • issue/automatic_updates-3388965
  • issue/automatic_updates-3387379
  • issue/automatic_updates-3390493
  • issue/automatic_updates-3308235
  • issue/automatic_updates-3391715
  • issue/automatic_updates-3392246
  • issue/automatic_updates-3393633
  • issue/automatic_updates-3379344
  • issue/automatic_updates-3394413
  • issue/automatic_updates-3394705
  • issue/automatic_updates-3394936
  • issue/automatic_updates-3363497
  • issue/automatic_updates-3395782
  • issue/automatic_updates-3341406
  • issue/automatic_updates-3311472
  • issue/automatic_updates-3397228
  • issue/automatic_updates-3352654
  • issue/automatic_updates-3398782
  • issue/automatic_updates-3399147
  • issue/automatic_updates-3399155
  • issue/automatic_updates-3406812
  • issue/automatic_updates-3408483
  • issue/automatic_updates-3408488
  • issue/automatic_updates-3408560
  • issue/automatic_updates-3437633
  • issue/automatic_updates-3408835
  • issue/automatic_updates-3408937
  • issue/automatic_updates-3409491
  • issue/automatic_updates-3409519
  • issue/automatic_updates-3377458
  • issue/automatic_updates-3437951
  • issue/automatic_updates-3409774
  • issue/automatic_updates-3410047
  • issue/automatic_updates-3411110
  • issue/automatic_updates-3411241
  • issue/automatic_updates-3411276
  • issue/automatic_updates-3411392
  • issue/automatic_updates-3411240
  • issue/automatic_updates-3412630
  • issue/automatic_updates-3413866
  • issue/automatic_updates-3414168
  • issue/automatic_updates-3415291
  • issue/automatic_updates-3415389
  • issue/automatic_updates-3415403
  • issue/automatic_updates-3416768
  • issue/automatic_updates-3417905
  • issue/automatic_updates-3420188
  • issue/automatic_updates-3421641
  • issue/automatic_updates-3424290
  • issue/automatic_updates-3426229
  • issue/automatic_updates-3426666
  • issue/automatic_updates-3426716
  • issue/automatic_updates-3428199
  • issue/automatic_updates-3436741
  • issue/automatic_updates-3441923
  • issue/automatic_updates-3428651
  • issue/automatic_updates-3441799
  • issue/automatic_updates-3443249
  • issue/automatic_updates-3428922
  • issue/automatic_updates-3441817
  • issue/automatic_updates-3429248
  • issue/automatic_updates-3440646
  • issue/automatic_updates-3437877
  • issue/automatic_updates-3445149
  • issue/automatic_updates-3429268
  • issue/automatic_updates-3442153
  • issue/automatic_updates-3438156
  • issue/automatic_updates-3436993
  • issue/automatic_updates-3437704
  • issue/automatic_updates-3432496
  • issue/automatic_updates-3440006
  • issue/automatic_updates-3435893
  • issue/automatic_updates-3441577
  • issue/automatic_updates-3437023
  • issue/automatic_updates-3432391
  • issue/automatic_updates-3432447
  • issue/automatic_updates-3432472
  • issue/automatic_updates-3432489
  • issue/automatic_updates-3432476
  • issue/automatic_updates-3435918
  • issue/automatic_updates-3439067
  • issue/automatic_updates-3437409
  • issue/automatic_updates-3449093
  • issue/automatic_updates-3449480
  • issue/automatic_updates-3449519
  • issue/automatic_updates-3449631
  • issue/automatic_updates-3449636
  • issue/automatic_updates-3449689
  • issue/automatic_updates-3449693
  • issue/automatic_updates-3450884
  • issue/automatic_updates-3452870
  • issue/automatic_updates-3453030
  • issue/automatic_updates-3455161
  • issue/automatic_updates-3459557
  • issue/automatic_updates-3462138
  • issue/automatic_updates-3462168
  • issue/automatic_updates-3462883
  • issue/automatic_updates-3463662
  • issue/automatic_updates-3463813
  • issue/automatic_updates-3446371
  • issue/automatic_updates-3465155
  • issue/automatic_updates-3340355
  • issue/automatic_updates-3467749
  • issue/automatic_updates-3473410
  • issue/automatic_updates-3441926
  • issue/automatic_updates-3474603
  • issue/automatic_updates-3484802
  • issue/automatic_updates-3498586
  • issue/automatic_updates-3499481
  • issue/automatic_updates-3498018
  • issue/automatic_updates-3506632
  • issue/automatic_updates-3511959
  • issue/automatic_updates-3511943
981 results
Show changes
Showing
with 791 additions and 339 deletions
......@@ -4,6 +4,7 @@ declare(strict_types = 1);
namespace Drupal\package_manager;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
......@@ -48,18 +49,24 @@ final class PackageManagerUpdateProcessor extends UpdateProcessor {
* The key/value factory.
* @param \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface $key_value_expirable_factory
* The expirable key/value factory.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
*/
public function __construct(ConfigFactoryInterface $config_factory, QueueFactory $queue_factory, UpdateFetcherInterface $update_fetcher, StateInterface $state_store, PrivateKey $private_key, KeyValueFactoryInterface $key_value_factory, KeyValueExpirableFactoryInterface $key_value_expirable_factory) {
$this->updateFetcher = $update_fetcher;
$this->updateSettings = $config_factory->get('update.settings');
public function __construct(
ConfigFactoryInterface $config_factory,
QueueFactory $queue_factory,
UpdateFetcherInterface $update_fetcher,
StateInterface $state_store,
PrivateKey $private_key,
KeyValueFactoryInterface $key_value_factory,
KeyValueExpirableFactoryInterface $key_value_expirable_factory,
TimeInterface $time,
) {
parent::__construct(...func_get_args());
$this->fetchQueue = $queue_factory->get('package_manager.update_fetch_tasks');
$this->tempStore = $key_value_expirable_factory->get('package_manager.update');
$this->fetchTaskStore = $key_value_factory->get('package_manager.update_fetch_task');
$this->availableReleasesTempStore = $key_value_expirable_factory->get('package_manager.update_available_releases');
$this->stateStore = $state_store;
$this->privateKey = $private_key;
$this->fetchTasks = [];
$this->failed = [];
}
/**
......
......@@ -5,7 +5,7 @@ declare(strict_types = 1);
namespace Drupal\package_manager\PathExcluder;
use Drupal\package_manager\ComposerInspector;
use Drupal\package_manager\Event\CollectIgnoredPathsEvent;
use Drupal\package_manager\Event\CollectPathsToExcludeEvent;
use Drupal\package_manager\PathLocator;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
......@@ -19,39 +19,38 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
*/
final class GitExcluder implements EventSubscriberInterface {
use PathExclusionsTrait;
/**
* Constructs a GitExcluder object.
*
* @param \Drupal\package_manager\PathLocator $path_locator
* @param \Drupal\package_manager\PathLocator $pathLocator
* The path locator service.
* @param \Drupal\package_manager\ComposerInspector $composerInspector
* The Composer inspector service.
*/
public function __construct(PathLocator $path_locator, private ComposerInspector $composerInspector) {
$this->pathLocator = $path_locator;
}
public function __construct(
private readonly PathLocator $pathLocator,
private readonly ComposerInspector $composerInspector
) {}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
return [
CollectIgnoredPathsEvent::class => 'excludeGitDirectories',
CollectPathsToExcludeEvent::class => 'excludeGitDirectories',
];
}
/**
* Excludes .git directories from stage operations.
*
* @param \Drupal\package_manager\Event\CollectIgnoredPathsEvent $event
* @param \Drupal\package_manager\Event\CollectPathsToExcludeEvent $event
* The event object.
*
* @throws \Exception
* See \Drupal\package_manager\ComposerInspector::validate().
*/
public function excludeGitDirectories(CollectIgnoredPathsEvent $event): void {
public function excludeGitDirectories(CollectPathsToExcludeEvent $event): void {
$project_root = $this->pathLocator->getProjectRoot();
// To determine which .git directories to exclude, the installed packages
......@@ -73,7 +72,7 @@ final class GitExcluder implements EventSubscriberInterface {
$installed_paths[] = $package->path;
}
}
$paths = $this->scanForDirectoriesByName('.git');
$paths = $event->scanForDirectoriesByName('.git');
foreach ($paths as $git_directory) {
// Don't exclude any `.git` directory that is directly under an installed
// package's path, since it means Composer probably installed that package
......@@ -83,7 +82,7 @@ final class GitExcluder implements EventSubscriberInterface {
$paths_to_exclude[] = $git_directory;
}
}
$this->excludeInProjectRoot($event, $paths_to_exclude);
$event->addPathsRelativeToProjectRoot($paths_to_exclude);
}
}
......@@ -4,8 +4,7 @@ declare(strict_types = 1);
namespace Drupal\package_manager\PathExcluder;
use Drupal\package_manager\Event\CollectIgnoredPathsEvent;
use Drupal\package_manager\PathLocator;
use Drupal\package_manager\Event\CollectPathsToExcludeEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
......@@ -18,27 +17,14 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
*/
class NodeModulesExcluder implements EventSubscriberInterface {
use PathExclusionsTrait;
/**
* Constructs a NodeModulesExcluder object.
*
* @param \Drupal\package_manager\PathLocator $path_locator
* The path locator service.
*/
public function __construct(PathLocator $path_locator) {
$this->pathLocator = $path_locator;
}
/**
* Excludes node_modules directories from stage operations.
*
* @param \Drupal\package_manager\Event\CollectIgnoredPathsEvent $event
* @param \Drupal\package_manager\Event\CollectPathsToExcludeEvent $event
* The event object.
*/
public function excludeNodeModulesFiles(CollectIgnoredPathsEvent $event): void {
$paths = $this->scanForDirectoriesByName('node_modules');
$this->excludeInProjectRoot($event, $paths);
public function excludeNodeModulesFiles(CollectPathsToExcludeEvent $event): void {
$event->addPathsRelativeToProjectRoot($event->scanForDirectoriesByName('node_modules'));
}
/**
......@@ -46,7 +32,7 @@ class NodeModulesExcluder implements EventSubscriberInterface {
*/
public static function getSubscribedEvents(): array {
return [
CollectIgnoredPathsEvent::class => 'excludeNodeModulesFiles',
CollectPathsToExcludeEvent::class => 'excludeNodeModulesFiles',
];
}
......
......@@ -4,7 +4,11 @@ declare(strict_types = 1);
namespace Drupal\package_manager\PathExcluder;
use Drupal\package_manager\Event\CollectIgnoredPathsEvent;
use Drupal\Core\File\Exception\FileException;
use Drupal\Core\File\FileSystemInterface;
use Drupal\package_manager\Event\CollectPathsToExcludeEvent;
use Drupal\package_manager\Event\PostCreateEvent;
use Drupal\package_manager\Event\PreApplyEvent;
use Drupal\package_manager\PathLocator;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
......@@ -18,31 +22,36 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
*/
class SiteConfigurationExcluder implements EventSubscriberInterface {
use PathExclusionsTrait;
/**
* Constructs an ExcludedPathsSubscriber.
* Constructs an SiteConfigurationExcluder.
*
* @param string $sitePath
* The current site path, relative to the Drupal root.
* @param \Drupal\package_manager\PathLocator $path_locator
* @param \Drupal\package_manager\PathLocator $pathLocator
* The path locator service.
* @param \Drupal\Core\File\FileSystemInterface $fileSystem
* The file system service.
*/
public function __construct(protected string $sitePath, PathLocator $path_locator) {
$this->pathLocator = $path_locator;
}
public function __construct(
protected string $sitePath,
private readonly PathLocator $pathLocator,
private readonly FileSystemInterface $fileSystem
) {}
/**
* Excludes site configuration files from stage operations.
*
* @param \Drupal\package_manager\Event\CollectIgnoredPathsEvent $event
* @param \Drupal\package_manager\Event\CollectPathsToExcludeEvent $event
* The event object.
*/
public function excludeSiteConfiguration(CollectIgnoredPathsEvent $event): void {
// Site configuration files are always excluded relative to the web root.
$paths = [];
public function excludeSiteConfiguration(CollectPathsToExcludeEvent $event): void {
// These two files are never relevant to existing sites.
$paths = [
'sites/default/default.settings.php',
'sites/default/default.services.yml',
];
// Ignore site-specific settings files, which are always in the web root.
// Exclude site-specific settings files, which are always in the web root.
// By default, Drupal core will always try to write-protect these files.
$settings_files = [
'settings.php',
......@@ -53,7 +62,82 @@ class SiteConfigurationExcluder implements EventSubscriberInterface {
$paths[] = $this->sitePath . '/' . $settings_file;
$paths[] = 'sites/default/' . $settings_file;
}
$this->excludeInWebRoot($event, $paths);
// Site configuration files are always excluded relative to the web root.
$event->addPathsRelativeToWebRoot($paths);
}
/**
* Makes the staged `sites/default` directory owner-writable.
*
* This is done to allow the core scaffold plugin to make changes in
* `sites/default`, if necessary, without breaking if `sites/default` is not
* writable (this can happen because rsync preserves directory permissions,
* and Drupal will try to harden the site directory's permissions as much as
* possible). We specifically exclude the `default.settings.php` and
* `default.services.yml` files from Package Manager operations, so we want to
* allow the scaffold plugin to make whatever changes it wants to those files
* in the stage directory.
*
* @param \Drupal\package_manager\Event\PostCreateEvent $event
* The event being handled.
*
* @see ::excludeSiteConfiguration()
*/
public function makeDefaultSiteDirectoryWritable(PostCreateEvent $event): void {
$dir = $this->getDefaultSiteDirectoryPath($event->stage->getStageDirectory());
// If the directory doesn't even exist, there's nothing to do here.
if (!is_dir($dir)) {
return;
}
if (!$this->fileSystem->chmod($dir, 0700)) {
throw new FileException("Could not change permissions on '$dir'.");
}
}
/**
* Makes `sites/default` permissions the same in live and stage directories.
*
* @param \Drupal\package_manager\Event\PreApplyEvent $event
* The event being handled.
*
* @throws \Drupal\Core\File\Exception\FileException
* If the permissions of the live `sites/default` cannot be determined, or
* cannot be changed on the staged `sites/default`.
*/
public function syncDefaultSiteDirectoryPermissions(PreApplyEvent $event): void {
$staged_dir = $this->getDefaultSiteDirectoryPath($event->stage->getStageDirectory());
// If the directory doesn't even exist, there's nothing to do here.
if (!is_dir($staged_dir)) {
return;
}
$live_dir = $this->getDefaultSiteDirectoryPath($this->pathLocator->getProjectRoot());
$permissions = fileperms($live_dir);
if ($permissions === FALSE) {
throw new FileException("Could not determine permissions for '$live_dir'.");
}
if (!$this->fileSystem->chmod($staged_dir, $permissions)) {
throw new FileException("Could not change permissions on '$staged_dir'.");
}
}
/**
* Returns the full path to `sites/default`, relative to a root directory.
*
* @param string $root_dir
* The root directory.
*
* @return string
* The full path to `sites/default` within the given root directory.
*/
private function getDefaultSiteDirectoryPath(string $root_dir): string {
$dir = [$root_dir];
$web_root = $this->pathLocator->getWebRoot();
if ($web_root) {
$dir[] = $web_root;
}
return implode(DIRECTORY_SEPARATOR, [...$dir, 'sites', 'default']);
}
/**
......@@ -61,7 +145,9 @@ class SiteConfigurationExcluder implements EventSubscriberInterface {
*/
public static function getSubscribedEvents(): array {
return [
CollectIgnoredPathsEvent::class => 'excludeSiteConfiguration',
CollectPathsToExcludeEvent::class => 'excludeSiteConfiguration',
PostCreateEvent::class => 'makeDefaultSiteDirectoryWritable',
PreApplyEvent::class => 'syncDefaultSiteDirectoryPermissions',
];
}
......
......@@ -6,8 +6,7 @@ namespace Drupal\package_manager\PathExcluder;
use Drupal\Core\StreamWrapper\LocalStream;
use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
use Drupal\package_manager\Event\CollectIgnoredPathsEvent;
use Drupal\package_manager\PathLocator;
use Drupal\package_manager\Event\CollectPathsToExcludeEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Filesystem\Filesystem;
......@@ -21,39 +20,36 @@ use Symfony\Component\Filesystem\Filesystem;
*/
final class SiteFilesExcluder implements EventSubscriberInterface {
use PathExclusionsTrait;
/**
* Constructs a SiteFilesExcluder object.
*
* @param \Drupal\package_manager\PathLocator $path_locator
* The path locator service.
* @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $streamWrapperManager
* The stream wrapper manager service.
* @param \Symfony\Component\Filesystem\Filesystem $fileSystem
* The Symfony file system service.
*/
public function __construct(PathLocator $path_locator, protected StreamWrapperManagerInterface $streamWrapperManager, protected Filesystem $fileSystem) {
$this->pathLocator = $path_locator;
}
public function __construct(
private readonly StreamWrapperManagerInterface $streamWrapperManager,
private readonly Filesystem $fileSystem
) {}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
return [
CollectIgnoredPathsEvent::class => 'excludeSiteFiles',
CollectPathsToExcludeEvent::class => 'excludeSiteFiles',
];
}
/**
* Excludes public and private files from stage operations.
*
* @param \Drupal\package_manager\Event\CollectIgnoredPathsEvent $event
* @param \Drupal\package_manager\Event\CollectPathsToExcludeEvent $event
* The event object.
*/
public function excludeSiteFiles(CollectIgnoredPathsEvent $event): void {
// Ignore public and private files. These paths could be either absolute or
public function excludeSiteFiles(CollectPathsToExcludeEvent $event): void {
// Exclude public and private files. These paths could be either absolute or
// relative, depending on site settings. If they are absolute, treat them
// as relative to the project root. Otherwise, treat them as relative to
// the web root.
......@@ -63,10 +59,12 @@ final class SiteFilesExcluder implements EventSubscriberInterface {
$path = $wrapper->getDirectoryPath();
if ($this->fileSystem->isAbsolutePath($path)) {
$this->excludeInProjectRoot($event, [realpath($path)]);
if ($path = realpath($path)) {
$event->addPathsRelativeToProjectRoot([$path]);
}
}
else {
$this->excludeInWebRoot($event, [$path]);
$event->addPathsRelativeToWebRoot([$path]);
}
}
}
......
......@@ -5,8 +5,8 @@ declare(strict_types = 1);
namespace Drupal\package_manager\PathExcluder;
use Drupal\Core\Database\Connection;
use Drupal\package_manager\Event\CollectIgnoredPathsEvent;
use Drupal\package_manager\PathLocator;
use Drupal\package_manager\Event\CollectPathsToExcludeEvent;
use PhpTuf\ComposerStager\API\Path\Factory\PathFactoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
......@@ -19,49 +19,56 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
*/
class SqliteDatabaseExcluder implements EventSubscriberInterface {
use PathExclusionsTrait;
/**
* Constructs a SqliteDatabaseExcluder object.
*
* @param \Drupal\package_manager\PathLocator $path_locator
* The path locator service.
* @param \PhpTuf\ComposerStager\API\Path\Factory\PathFactoryInterface $pathFactory
* The path factory service.
* @param \Drupal\Core\Database\Connection $database
* The database connection.
*/
public function __construct(PathLocator $path_locator, protected Connection $database) {
$this->pathLocator = $path_locator;
}
public function __construct(
private readonly PathFactoryInterface $pathFactory,
private readonly Connection $database,
) {}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
return [
CollectIgnoredPathsEvent::class => 'excludeDatabaseFiles',
CollectPathsToExcludeEvent::class => 'excludeDatabaseFiles',
];
}
/**
* Excludes SQLite database files from stage operations.
*
* @param \Drupal\package_manager\Event\CollectIgnoredPathsEvent $event
* @param \Drupal\package_manager\Event\CollectPathsToExcludeEvent $event
* The event object.
*/
public function excludeDatabaseFiles(CollectIgnoredPathsEvent $event): void {
// If the database is SQLite, it might be located in the active directory
// and we should ignore it. Always treat it as relative to the project root.
public function excludeDatabaseFiles(CollectPathsToExcludeEvent $event): void {
// If the database is SQLite, it might be located in the project directory
// and we should exclude it.
if ($this->database->driver() === 'sqlite') {
$options = $this->database->getConnectionOptions();
// Nothing to exclude if the database lives outside the project root.
if (str_starts_with($options['database'], '/') && !str_starts_with($options['database'], $this->pathLocator->getProjectRoot())) {
return;
$db_path = $this->database->getConnectionOptions()['database'];
// Exclude the database file and auxiliary files created by SQLite.
$paths = [$db_path, "$db_path-shm", "$db_path-wal"];
// If the database path is absolute, it might be outside the project root,
// in which case we don't need to do anything.
if ($this->pathFactory->create($db_path)->isAbsolute()) {
try {
$event->addPathsRelativeToProjectRoot($paths);
}
catch (\LogicException) {
// The database is outside of the project root, so we're done.
}
}
else {
// The database is in the web root, and must be excluded relative to it.
$event->addPathsRelativeToWebRoot($paths);
}
$this->excludeInProjectRoot($event, [
$options['database'],
$options['database'] . '-shm',
$options['database'] . '-wal',
]);
}
}
......
......@@ -4,8 +4,7 @@ declare(strict_types = 1);
namespace Drupal\package_manager\PathExcluder;
use Drupal\package_manager\Event\CollectIgnoredPathsEvent;
use Drupal\package_manager\PathLocator;
use Drupal\package_manager\Event\CollectPathsToExcludeEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
......@@ -18,37 +17,25 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
*/
final class TestSiteExcluder implements EventSubscriberInterface {
use PathExclusionsTrait;
/**
* Constructs a TestSiteExcluder object.
*
* @param \Drupal\package_manager\PathLocator $path_locator
* The path locator service.
*/
public function __construct(PathLocator $path_locator) {
$this->pathLocator = $path_locator;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
return [
CollectIgnoredPathsEvent::class => 'excludeTestSites',
CollectPathsToExcludeEvent::class => 'excludeTestSites',
];
}
/**
* Excludes sites/simpletest from stage operations.
*
* @param \Drupal\package_manager\Event\CollectIgnoredPathsEvent $event
* @param \Drupal\package_manager\Event\CollectPathsToExcludeEvent $event
* The event object.
*/
public function excludeTestSites(CollectIgnoredPathsEvent $event): void {
// Always ignore automated test directories. If they exist, they will be in
public function excludeTestSites(CollectPathsToExcludeEvent $event): void {
// Always exclude automated test directories. If they exist, they will be in
// the web root.
$this->excludeInWebRoot($event, ['sites/simpletest']);
$event->addPathsRelativeToWebRoot(['sites/simpletest']);
}
}
......@@ -5,9 +5,15 @@ declare(strict_types = 1);
namespace Drupal\package_manager\PathExcluder;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\package_manager\ComposerInspector;
use Drupal\package_manager\Event\CollectIgnoredPathsEvent;
use Drupal\package_manager\Event\CollectPathsToExcludeEvent;
use Drupal\package_manager\Event\StatusCheckEvent;
use Drupal\package_manager\PathLocator;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
......@@ -23,25 +29,39 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
*
* If web root and project root are the same, nothing is excluded.
*
* This excluder can be disabled by changing the config setting
* `package_manager.settings:include_unknown_files_in_project_root` to TRUE.
* This may be needed for sites that have files outside the web root (besides
* the vendor directory) which are nonetheless needed in order for Composer to
* assemble the code base correctly; a classic example would be a directory of
* patch files used by `cweagans/composer-patches`.
*
* @internal
* This is an internal part of Package Manager and may be changed or removed
* at any time without warning. External code should not interact with this
* class.
*/
final class UnknownPathExcluder implements EventSubscriberInterface {
final class UnknownPathExcluder implements EventSubscriberInterface, LoggerAwareInterface {
use PathExclusionsTrait;
use LoggerAwareTrait;
use StringTranslationTrait;
/**
* Constructs a UnknownPathExcluder object.
*
* @param \Drupal\package_manager\ComposerInspector $composerInspector
* The Composer inspector service.
* @param \Drupal\package_manager\PathLocator $path_locator
* @param \Drupal\package_manager\PathLocator $pathLocator
* The path locator service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* The config factory service.
*/
public function __construct(private ComposerInspector $composerInspector, PathLocator $path_locator) {
$this->pathLocator = $path_locator;
public function __construct(
private readonly ComposerInspector $composerInspector,
private readonly PathLocator $pathLocator,
private readonly ConfigFactoryInterface $configFactory,
) {
$this->setLogger(new NullLogger());
}
/**
......@@ -49,54 +69,111 @@ final class UnknownPathExcluder implements EventSubscriberInterface {
*/
public static function getSubscribedEvents(): array {
return [
CollectIgnoredPathsEvent::class => 'excludeUnknownPaths',
CollectPathsToExcludeEvent::class => 'excludeUnknownPaths',
StatusCheckEvent::class => 'logExcludedPaths',
];
}
/**
* Excludes unknown paths from stage operations.
* Returns the paths to exclude from stage operations.
*
* @param \Drupal\package_manager\Event\CollectIgnoredPathsEvent $event
* The event object.
* @return string[]
* The paths that should be excluded from stage operations, relative to the
* project root.
*
* @throws \Exception
* See \Drupal\package_manager\ComposerInspector::validate().
*/
public function excludeUnknownPaths(CollectIgnoredPathsEvent $event): void {
$project_root = $this->pathLocator->getProjectRoot();
$web_root = $project_root . DIRECTORY_SEPARATOR . $this->pathLocator->getWebRoot();
if (realpath($web_root) === $project_root) {
return;
private function getExcludedPaths(): array {
// If this excluder is disabled, or the project root and web root are the
// same, we are not excluding any paths.
$is_disabled = $this->configFactory->get('package_manager.settings')
->get('include_unknown_files_in_project_root');
$web_root = $this->pathLocator->getWebRoot();
if ($is_disabled || empty($web_root)) {
return [];
}
// To determine the scaffold files to exclude, the installed packages must
// be known, and that requires Composer commands to be able to run. This
// intentionally does not catch exceptions: failed Composer validation in
// the project root implies that this excluder cannot function correctly.
// Note: the call to ComposerInspector::getInstalledPackagesList() would
// To determine the files to include, the installed packages must be known,
// and that requires Composer commands to be able to run. This intentionally
// does not catch exceptions: failed Composer validation in the project root
// implies that this excluder cannot function correctly.
// Note: the call to ComposerInspector::getConfig() would
// also have triggered this, but explicitness is preferred here.
// @see \Drupal\package_manager\StatusCheckTrait::runStatusCheck()
$project_root = $this->pathLocator->getProjectRoot();
$this->composerInspector->validate($project_root);
$vendor_dir = $this->pathLocator->getVendorDirectory();
$scaffold_files_paths = $this->getScaffoldFiles();
// Search for all files (including hidden ones) in project root.
$paths_in_project_root = glob("$project_root/{,.}*", GLOB_BRACE);
$paths = [];
$known_paths = array_merge([$vendor_dir, $web_root, "$project_root/composer.json", "$project_root/composer.lock"], $scaffold_files_paths);
foreach ($paths_in_project_root as $path_in_project_root) {
if (!in_array($path_in_project_root, $known_paths, TRUE)) {
$paths[] = $path_in_project_root;
// The vendor directory and web root are always included in staging
// operations, along with `composer.json`, `composer.lock`, and any scaffold
// files provided by Drupal core.
$always_include = [
$this->composerInspector->getConfig('vendor-dir', $project_root),
$web_root,
'composer.json',
'composer.lock',
];
foreach ($this->getScaffoldFiles() as $scaffold_file_path) {
// The web root is always included in staging operations, so we don't need
// to do anything special for scaffold files that live in it.
if (str_starts_with($scaffold_file_path, '[web-root]')) {
continue;
}
$always_include[] = ltrim($scaffold_file_path, '/');
}
// Search for all files (including hidden ones) in the project root. We need
// to use readdir() and friends here, rather than glob(), since certain
// glob() flags aren't supported on all systems. We also can't use
// \Drupal\Core\File\FileSystemInterface::scanDirectory(), because it
// unconditionally ignores hidden files and directories.
$files_in_project_root = [];
$handle = opendir($project_root);
if (empty($handle)) {
throw new \RuntimeException("Could not scan for files in the project root.");
}
while ($entry = readdir($handle)) {
$files_in_project_root[] = $entry;
}
closedir($handle);
return array_diff($files_in_project_root, $always_include, ['.', '..']);
}
/**
* Excludes unknown paths from stage operations.
*
* @param \Drupal\package_manager\Event\CollectPathsToExcludeEvent $event
* The event object.
*/
public function excludeUnknownPaths(CollectPathsToExcludeEvent $event): void {
// We can exclude the paths as-is; they are already relative to the project
// root.
$event->add(...$this->getExcludedPaths());
}
/**
* Logs the paths that will be excluded from stage operations.
*/
public function logExcludedPaths(): void {
$excluded_paths = $this->getExcludedPaths();
if ($excluded_paths) {
sort($excluded_paths);
$message = $this->t("The following paths in @project_root aren't recognized as part of your Drupal site, so to be safe, Package Manager is excluding them from all stage operations. If these files are not needed for Composer to work properly in your site, no action is needed. Otherwise, you can disable this behavior by setting the <code>package_manager.settings:include_unknown_files_in_project_root</code> config setting to <code>TRUE</code>.\n\n@list", [
'@project_root' => $this->pathLocator->getProjectRoot(),
'@list' => implode("\n", $excluded_paths),
]);
$this->logger->info($message);
}
$this->excludeInProjectRoot($event, $paths);
}
/**
* Gets the path of scaffold files, for example 'index.php' and 'robots.txt'.
*
* @return array
* The array of scaffold file paths.
* @return string[]
* The paths of scaffold files provided by `drupal/core`, relative to the
* project root.
*
* @todo Intelligently load scaffold files in https://drupal.org/i/3343802.
*/
......@@ -104,10 +181,9 @@ final class UnknownPathExcluder implements EventSubscriberInterface {
$project_root = $this->pathLocator->getProjectRoot();
$packages = $this->composerInspector->getInstalledPackagesList($project_root);
$extra = Json::decode($this->composerInspector->getConfig('extra', $packages['drupal/core']->path . '/composer.json'));
if (isset($extra['drupal-scaffold']['file-mapping'])) {
return array_keys($extra['drupal-scaffold']['file-mapping']);
}
return [];
$scaffold_files = $extra['drupal-scaffold']['file-mapping'] ?? [];
return str_replace('[project-root]', '', array_keys($scaffold_files));
}
}
......@@ -4,7 +4,7 @@ declare(strict_types = 1);
namespace Drupal\package_manager\PathExcluder;
use Drupal\package_manager\Event\CollectIgnoredPathsEvent;
use Drupal\package_manager\Event\CollectPathsToExcludeEvent;
use Drupal\package_manager\PathLocator;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
......@@ -18,39 +18,35 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
*/
final class VendorHardeningExcluder implements EventSubscriberInterface {
use PathExclusionsTrait;
/**
* Constructs a VendorHardeningExcluder object.
*
* @param \Drupal\package_manager\PathLocator $path_locator
* @param \Drupal\package_manager\PathLocator $pathLocator
* The path locator service.
*/
public function __construct(PathLocator $path_locator) {
$this->pathLocator = $path_locator;
}
public function __construct(private readonly PathLocator $pathLocator) {}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
return [
CollectIgnoredPathsEvent::class => 'excludeVendorHardeningFiles',
CollectPathsToExcludeEvent::class => 'excludeVendorHardeningFiles',
];
}
/**
* Excludes vendor hardening files from stage operations.
*
* @param \Drupal\package_manager\Event\CollectIgnoredPathsEvent $event
* @param \Drupal\package_manager\Event\CollectPathsToExcludeEvent $event
* The event object.
*/
public function excludeVendorHardeningFiles(CollectIgnoredPathsEvent $event): void {
public function excludeVendorHardeningFiles(CollectPathsToExcludeEvent $event): void {
// If the core-vendor-hardening plugin (used in the legacy-project template)
// is present, it may have written security hardening files in the vendor
// directory. They should always be ignored.
// directory. They should always be excluded.
$vendor_dir = $this->pathLocator->getVendorDirectory();
$this->excludeInProjectRoot($event, [
$event->addPathsRelativeToProjectRoot([
$vendor_dir . '/web.config',
$vendor_dir . '/.htaccess',
]);
......
<?php
namespace Drupal\package_manager\Plugin\QueueWorker;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Queue\QueueWorkerBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Processes a queue of defunct stage directories, deleting them.
*
* @QueueWorker(
* id = "package_manager_cleanup",
* title = @Translation("Stage directory cleaner"),
* cron = {"time" = 30}
* )
*/
final class Cleaner extends QueueWorkerBase implements ContainerFactoryPluginInterface {
/**
* Constructs a new Cleaner instance.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\File\FileSystemInterface $fileSystem
* The file system service.
*/
public function __construct(array $configuration, string $plugin_id, mixed $plugin_definition, private readonly FileSystemInterface $fileSystem) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get(FileSystemInterface::class),
);
}
/**
* {@inheritdoc}
*/
public function processItem($dir) {
assert(is_string($dir));
if (file_exists($dir)) {
$this->fileSystem->deleteRecursive($dir, function (string $path): void {
$this->fileSystem->chmod($path, is_dir($path) ? 0700 : 0600);
});
}
}
}
......@@ -6,9 +6,8 @@ namespace Drupal\package_manager;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\File\FileSystemInterface;
use PhpTuf\ComposerStager\Infrastructure\Factory\Process\ProcessFactoryInterface;
use PhpTuf\ComposerStager\Infrastructure\Factory\Process\ProcessFactory as StagerProcessFactory;
use Symfony\Component\Process\Process;
use PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface;
use PhpTuf\ComposerStager\API\Process\Service\ProcessInterface;
// cspell:ignore BINDIR
......@@ -22,13 +21,6 @@ use Symfony\Component\Process\Process;
*/
final class ProcessFactory implements ProcessFactoryInterface {
/**
* The decorated process factory.
*
* @var \PhpTuf\ComposerStager\Infrastructure\Factory\Process\ProcessFactoryInterface
*/
private $decorated;
/**
* Constructs a ProcessFactory object.
*
......@@ -36,10 +28,14 @@ final class ProcessFactory implements ProcessFactoryInterface {
* The file system service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* The config factory service.
* @param \PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface $decorated
* The decorated process factory service.
*/
public function __construct(private FileSystemInterface $fileSystem, private ConfigFactoryInterface $configFactory) {
$this->decorated = new StagerProcessFactory();
}
public function __construct(
private readonly FileSystemInterface $fileSystem,
private readonly ConfigFactoryInterface $configFactory,
private readonly ProcessFactoryInterface $decorated,
) {}
/**
* Returns the value of an environment variable.
......@@ -60,30 +56,12 @@ final class ProcessFactory implements ProcessFactoryInterface {
/**
* {@inheritdoc}
*/
public function create(array $command): Process {
public function create(array $command): ProcessInterface {
$process = $this->decorated->create($command);
$env = $process->getEnv();
if ($this->isComposerCommand($command)) {
if ($command && $this->isComposerCommand($command)) {
$env['COMPOSER_HOME'] = $this->getComposerHomePath();
// Work around Composer not being designed to be run massively in parallel
// which it may in the context of Package Manager, at least for tests. It
// is trivial to work around though: create a unique temporary directory
// per process.
// @see https://www.drupal.org/i/3338789#comment-14961390
// @see https://github.com/composer/composer/commit/28e9193e9ebde743c19f334a7294830fc6429d06
// @see https://github.com/composer/composer/commit/43eb471ec293822d377b618a4a14d8d3651f5d13
// @todo Remove this once Composer 2.5.5 is required in https://www.drupal.org/i/3350568 (2.5.5 is the first release to contain the upstream fix: https://github.com/composer/composer/releases/tag/2.5.5)
static $race_condition_proof_tmpdir;
if (!isset($race_condition_proof_tmpdir)) {
$race_condition_proof_tmpdir = sys_get_temp_dir() . '/' . getmypid();
// The same PHP process may run multiple tests: create the directory
// only once.
if (!is_dir($race_condition_proof_tmpdir)) {
mkdir($race_condition_proof_tmpdir);
}
}
$env['TMPDIR'] = $race_condition_proof_tmpdir;
}
// Ensure that the current PHP installation is the first place that will be
// searched when looking for the PHP interpreter.
......@@ -102,7 +80,7 @@ final class ProcessFactory implements ProcessFactoryInterface {
* @see php_sapi_name()
* @see https://www.php.net/manual/en/reserved.constants.php
*/
protected static function getPhpDirectory(): string {
private static function getPhpDirectory(): string {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'cli-server') {
return dirname(PHP_BINARY);
}
......
<?php
declare(strict_types = 1);
namespace Drupal\package_manager;
use PhpTuf\ComposerStager\API\Process\Service\OutputCallbackInterface;
use PhpTuf\ComposerStager\API\Process\Value\OutputTypeEnum;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
/**
* A process callback for capturing output.
*
* @see \Symfony\Component\Process\Process
*
* @internal
* This is an internal part of Package Manager and may be changed or removed
* at any time without warning. External code should not interact with this
* class.
*/
final class ProcessOutputCallback implements OutputCallbackInterface, LoggerAwareInterface {
use LoggerAwareTrait;
/**
* The output buffer.
*
* @var string
*/
private string $outBuffer = '';
/**
* The error buffer.
*
* @var string
*/
private string $errorBuffer = '';
/**
* Constructs a ProcessOutputCallback object.
*/
public function __construct() {
$this->setLogger(new NullLogger());
}
/**
* {@inheritdoc}
*/
public function __invoke(OutputTypeEnum $type, string $buffer): void {
if ($type === OutputTypeEnum::OUT) {
$this->outBuffer .= $buffer;
}
elseif ($type === OutputTypeEnum::ERR) {
$this->errorBuffer .= $buffer;
}
}
/**
* Gets the output.
*
* If there is anything in the error buffer, it will be logged as a warning.
*
* @return string|null
* The output or NULL if there is none.
*/
public function getOutput(): ?string {
$error_output = $this->getErrorOutput();
if ($error_output) {
$this->logger->warning($error_output);
}
return trim($this->outBuffer) !== '' ? $this->outBuffer : NULL;
}
/**
* Gets the parsed JSON output.
*
* @return mixed
* The decoded JSON output or NULL if there isn't any.
*/
public function parseJsonOutput(): mixed {
$output = $this->getOutput();
if ($output !== NULL) {
return json_decode($output, TRUE, flags: JSON_THROW_ON_ERROR);
}
return NULL;
}
/**
* Gets the error output.
*
* @return string|null
* The error output or NULL if there isn't any.
*/
public function getErrorOutput(): ?string {
return trim($this->errorBuffer) !== '' ? $this->errorBuffer : NULL;
}
/**
* Resets buffers.
*
* @return self
*/
public function reset(): self {
$this->errorBuffer = '';
$this->outBuffer = '';
return $this;
}
}
......@@ -27,7 +27,7 @@ final class ProjectInfo {
* @param string $name
* The project name.
*/
public function __construct(protected string $name) {
public function __construct(private readonly string $name) {
}
/**
......@@ -192,7 +192,7 @@ final class ProjectInfo {
// update processor service.
if (!isset($available_projects[$this->name])) {
/** @var \Drupal\package_manager\PackageManagerUpdateProcessor $update_processor */
$update_processor = \Drupal::service('package_manager.update_processor');
$update_processor = \Drupal::service(PackageManagerUpdateProcessor::class);
if ($project_data = $update_processor->getProjectData($this->name)) {
$available_projects[$this->name] = $project_data;
}
......
......@@ -6,21 +6,19 @@ namespace Drupal\package_manager;
use Composer\Semver\VersionParser;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\File\Exception\FileException;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Component\Utility\Random;
use Drupal\Core\Queue\QueueFactory;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TempStore\SharedTempStore;
use Drupal\Core\TempStore\SharedTempStoreFactory;
use Drupal\package_manager\Event\CollectIgnoredPathsEvent;
use Drupal\Core\Utility\Error;
use Drupal\package_manager\Event\CollectPathsToExcludeEvent;
use Drupal\package_manager\Event\PostApplyEvent;
use Drupal\package_manager\Event\PostCreateEvent;
use Drupal\package_manager\Event\PostDestroyEvent;
use Drupal\package_manager\Event\PostRequireEvent;
use Drupal\package_manager\Event\PreApplyEvent;
use Drupal\package_manager\Event\PreCreateEvent;
use Drupal\package_manager\Event\PreDestroyEvent;
use Drupal\package_manager\Event\PreOperationStageEvent;
use Drupal\package_manager\Event\PreRequireEvent;
use Drupal\package_manager\Event\StageEvent;
......@@ -28,13 +26,13 @@ use Drupal\package_manager\Exception\ApplyFailedException;
use Drupal\package_manager\Exception\StageEventException;
use Drupal\package_manager\Exception\StageException;
use Drupal\package_manager\Exception\StageOwnershipException;
use PhpTuf\ComposerStager\Domain\Core\Beginner\BeginnerInterface;
use PhpTuf\ComposerStager\Domain\Core\Committer\CommitterInterface;
use PhpTuf\ComposerStager\Domain\Core\Stager\StagerInterface;
use PhpTuf\ComposerStager\Domain\Exception\InvalidArgumentException;
use PhpTuf\ComposerStager\Domain\Exception\PreconditionException;
use PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface;
use PhpTuf\ComposerStager\Infrastructure\Value\PathList\PathList;
use PhpTuf\ComposerStager\API\Core\BeginnerInterface;
use PhpTuf\ComposerStager\API\Core\CommitterInterface;
use PhpTuf\ComposerStager\API\Core\StagerInterface;
use PhpTuf\ComposerStager\API\Exception\InvalidArgumentException;
use PhpTuf\ComposerStager\API\Exception\PreconditionException;
use PhpTuf\ComposerStager\API\Path\Factory\PathFactoryInterface;
use PhpTuf\ComposerStager\API\Path\Value\PathListInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
......@@ -64,7 +62,7 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
* site (e.g. `/tmp/.package_managerSITE_UUID`), which is deleted when any stage
* created by that site is destroyed.
*/
class Stage implements LoggerAwareInterface {
abstract class StageBase implements LoggerAwareInterface {
use LoggerAwareTrait;
use StringTranslationTrait;
......@@ -161,46 +159,82 @@ class Stage implements LoggerAwareInterface {
*/
protected SharedTempStore $tempStore;
/**
* The stage type.
*
* To ensure that stage classes do not unintentionally use another stage's
* type, all concrete subclasses MUST explicitly define this property.
* The recommended pattern is `MODULE:TYPE`.
*
* @var string
*/
protected string $type;
/**
* Constructs a new Stage object.
*
* @param \Drupal\package_manager\PathLocator $pathLocator
* The path locator service.
* @param \PhpTuf\ComposerStager\Domain\Core\Beginner\BeginnerInterface $beginner
* @param \PhpTuf\ComposerStager\API\Core\BeginnerInterface $beginner
* The beginner service.
* @param \PhpTuf\ComposerStager\Domain\Core\Stager\StagerInterface $stager
* @param \PhpTuf\ComposerStager\API\Core\StagerInterface $stager
* The stager service.
* @param \PhpTuf\ComposerStager\Domain\Core\Committer\CommitterInterface $committer
* @param \PhpTuf\ComposerStager\API\Core\CommitterInterface $committer
* The committer service.
* @param \Drupal\Core\File\FileSystemInterface $fileSystem
* The file system service.
* @param \Drupal\Core\Queue\QueueFactory $queueFactory
* The queue factory.
* @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $eventDispatcher
* The event dispatcher service.
* @param \Drupal\Core\TempStore\SharedTempStoreFactory $tempStoreFactory
* The shared tempstore factory.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
* @param \PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface $pathFactory
* @param \PhpTuf\ComposerStager\API\Path\Factory\PathFactoryInterface $pathFactory
* The path factory service.
* @param \Drupal\package_manager\FailureMarker $failureMarker
* The failure marker service.
*/
public function __construct(
protected PathLocator $pathLocator,
protected BeginnerInterface $beginner,
protected StagerInterface $stager,
protected CommitterInterface $committer,
protected FileSystemInterface $fileSystem,
protected readonly PathLocator $pathLocator,
protected readonly BeginnerInterface $beginner,
protected readonly StagerInterface $stager,
protected readonly CommitterInterface $committer,
protected readonly QueueFactory $queueFactory,
protected EventDispatcherInterface $eventDispatcher,
protected SharedTempStoreFactory $tempStoreFactory,
protected TimeInterface $time,
protected PathFactoryInterface $pathFactory,
protected FailureMarker $failureMarker,
protected readonly SharedTempStoreFactory $tempStoreFactory,
protected readonly TimeInterface $time,
protected readonly PathFactoryInterface $pathFactory,
protected readonly FailureMarker $failureMarker,
) {
$this->tempStore = $tempStoreFactory->get('package_manager_stage');
$this->setLogger(new NullLogger());
}
/**
* Gets the stage type.
*
* The stage type can be used by stage event subscribers to implement logic
* specific to certain stages, without relying on the class name (which may
* not be part of module's public API).
*
* @return string
* The stage type.
*
* @throws \LogicException
* Thrown if $this->type is not explicitly overridden.
*/
final public function getType(): string {
$reflector = new \ReflectionProperty($this, 'type');
// The $type property must ALWAYS be overridden. This means that different
// subclasses can return the same value (thus allowing one stage to
// impersonate another one), but if that happens, it is intentional.
if ($reflector->getDeclaringClass()->getName() === static::class) {
return $this->type;
}
throw new \LogicException(static::class . ' must explicitly override the $type property.');
}
/**
* Determines if the stage directory can be created.
*
......@@ -223,7 +257,7 @@ class Stage implements LoggerAwareInterface {
* @return mixed
* The metadata value, or NULL if it is not set.
*/
protected function getMetadata(string $key) {
public function getMetadata(string $key) {
$this->checkOwnership();
$metadata = $this->tempStore->get(static::TEMPSTORE_METADATA_KEY) ?: [];
......@@ -237,11 +271,13 @@ class Stage implements LoggerAwareInterface {
* claimed by its owner, or created during the current request.
*
* @param string $key
* The key under which to store the metadata.
* The key under which to store the metadata. To prevent conflicts, it is
* strongly recommended that this be prefixed with the name of the module
* storing the data.
* @param mixed $data
* The metadata to store.
*/
protected function setMetadata(string $key, $data): void {
public function setMetadata(string $key, $data): void {
$this->checkOwnership();
$metadata = $this->tempStore->get(static::TEMPSTORE_METADATA_KEY);
......@@ -250,27 +286,26 @@ class Stage implements LoggerAwareInterface {
}
/**
* Collects paths that Composer Stager should ignore.
* Collects paths that Composer Stager should exclude.
*
* @return string[]
* A list of paths that Composer Stager should ignore when creating the
* @return \PhpTuf\ComposerStager\API\Path\Value\PathListInterface
* A list of paths that Composer Stager should exclude when creating the
* stage directory and applying staged changes to the active directory.
*
* @throws \Drupal\package_manager\Exception\StageException
* Thrown if an exception occurs while collecting ignored paths.
* Thrown if an exception occurs while collecting paths to exclude.
*
* @see ::create()
* @see ::apply()
*/
protected function getIgnoredPaths(): array {
$event = new CollectIgnoredPathsEvent($this);
protected function getPathsToExclude(): PathListInterface {
$event = new CollectPathsToExcludeEvent($this, $this->pathLocator, $this->pathFactory);
try {
$this->eventDispatcher->dispatch($event);
return $this->eventDispatcher->dispatch($event);
}
catch (\Throwable $e) {
$this->rethrowAsStageException($e);
}
return $event->getAll();
}
/**
......@@ -309,26 +344,40 @@ class Stage implements LoggerAwareInterface {
// to create a stage directory at around the same time. If an error occurs
// while the event is being processed, the stage is marked as available.
// @see ::dispatch()
$id = Crypt::randomBytesBase64();
// We specifically generate a random 32-character alphanumeric name in order
// to guarantee that the the stage ID won't start with -, which could cause
// it to be interpreted as an option if it's used as a command-line
// argument. (For example,
// \Drupal\Component\Utility\Crypt::randomBytesBase64() would be vulnerable
// to this; the stage ID needs to be unique, but not cryptographically so.)
$id = (new Random())->name(32);
// Re-acquire the tempstore to ensure that the lock is written by whoever is
// actually logged in (or not) right now, since it's possible that the stage
// was instantiated (i.e., __construct() was called) by a different session,
// which would result in the lock having the wrong owner and the stage not
// being claimable by whoever is actually creating it.
$this->tempStore = $this->tempStoreFactory->get('package_manager_stage');
$this->tempStore->set(static::TEMPSTORE_LOCK_KEY, [$id, static::class]);
// For the lock value, we use both the stage's class and its type in order
// to prevent a stage from being manipulated by two different classes during
// a single life cycle.
$this->tempStore->set(static::TEMPSTORE_LOCK_KEY, [
$id,
static::class,
$this->getType(),
]);
$this->claim($id);
$active_dir = $this->pathFactory->create($this->pathLocator->getProjectRoot());
$stage_dir = $this->pathFactory->create($this->getStageDirectory());
$event = new PreCreateEvent($this, $this->getIgnoredPaths());
$excluded_paths = $this->getPathsToExclude();
$event = new PreCreateEvent($this, $excluded_paths);
// If an error occurs and we won't be able to create the stage, mark it as
// available.
$this->dispatch($event, [$this, 'markAsAvailable']);
try {
$this->beginner->begin($active_dir, $stage_dir, new PathList($event->getExcludedPaths()), NULL, $timeout);
$this->beginner->begin($active_dir, $stage_dir, $excluded_paths, NULL, $timeout);
}
catch (\Throwable $error) {
$this->destroy();
......@@ -407,7 +456,7 @@ class Stage implements LoggerAwareInterface {
// If constraints were changed, update those packages.
if ($runtime || $dev) {
$command = array_merge(['update', '--with-all-dependencies'], $runtime, $dev);
$command = array_merge(['update', '--with-all-dependencies', '--optimize-autoloader'], $runtime, $dev);
$do_stage($command);
}
$this->dispatch(new PostRequireEvent($this, $runtime, $dev));
......@@ -440,27 +489,24 @@ class Stage implements LoggerAwareInterface {
$active_dir = $this->pathFactory->create($this->pathLocator->getProjectRoot());
$stage_dir = $this->pathFactory->create($this->getStageDirectory());
$excluded_paths = $this->getPathsToExclude();
$event = new PreApplyEvent($this, $excluded_paths);
// If an error occurs while dispatching the events, ensure that ::destroy()
// doesn't think we're in the middle of applying the staged changes to the
// active directory.
$event = new PreApplyEvent($this, $this->getIgnoredPaths());
$this->tempStore->set(self::TEMPSTORE_APPLY_TIME_KEY, $this->time->getRequestTime());
$this->dispatch($event, $this->setNotApplying());
$this->dispatch($event, $this->setNotApplying(...));
// Create a marker file so that we can tell later on if the commit failed.
$this->failureMarker->write($this, $this->getFailureMarkerMessage());
// Exclude the failure file from the commit operation.
$ignored_paths = new PathList($event->getExcludedPaths());
$ignored_paths->add([
$this->failureMarker->getPath(),
]);
try {
$this->committer->commit($stage_dir, $active_dir, $ignored_paths, NULL, $timeout);
$this->committer->commit($stage_dir, $active_dir, $excluded_paths, NULL, $timeout);
}
catch (InvalidArgumentException | PreconditionException $e) {
// The commit operation has not started yet, so we can clear the failure
// marker.
// marker and release the flag that says we're applying.
$this->setNotApplying();
$this->failureMarker->clear();
$this->rethrowAsStageException($e);
}
......@@ -469,8 +515,10 @@ class Stage implements LoggerAwareInterface {
// is in an indeterminate state. Release the flag which says we're still
// applying, because in this situation, the site owner should probably
// restore everything from a backup.
$this->setNotApplying()();
throw new ApplyFailedException($this, (string) $this->getFailureMarkerMessage(), $throwable->getCode(), $throwable);
$this->setNotApplying();
// Update the marker file with the information from the throwable.
$this->failureMarker->write($this, $this->getFailureMarkerMessage(), $throwable);
throw new ApplyFailedException($this, $this->failureMarker->getMessage(), $throwable->getCode(), $throwable);
}
$this->failureMarker->clear();
$this->setMetadata(self::TEMPSTORE_CHANGES_APPLIED, TRUE);
......@@ -478,15 +526,9 @@ class Stage implements LoggerAwareInterface {
/**
* Returns a closure that marks this stage as no longer being applied.
*
* @return \Closure
* A closure that, when called, marks this stage as no longer in the process
* of being applied to the active directory.
*/
private function setNotApplying(): \Closure {
return function (): void {
$this->tempStore->delete(self::TEMPSTORE_APPLY_TIME_KEY);
};
private function setNotApplying(): void {
$this->tempStore->delete(self::TEMPSTORE_APPLY_TIME_KEY);
}
/**
......@@ -511,7 +553,7 @@ class Stage implements LoggerAwareInterface {
// unlikely to call newly added code during the current request.
$this->eventDispatcher = \Drupal::service('event_dispatcher');
$release_apply = $this->setNotApplying();
$release_apply = $this->setNotApplying(...);
$this->dispatch(new PostApplyEvent($this), $release_apply);
$release_apply();
}
......@@ -537,26 +579,16 @@ class Stage implements LoggerAwareInterface {
throw new StageException($this, 'Cannot destroy the stage directory while it is being applied to the active directory.');
}
$this->dispatch(new PreDestroyEvent($this));
$staging_root = $this->getStagingRoot();
// If the stage root directory exists, delete it and everything in it.
if (file_exists($staging_root)) {
try {
$this->fileSystem->deleteRecursive($staging_root, function (string $path): void {
$this->fileSystem->chmod($path, 0777);
});
}
catch (FileException) {
// Deliberately swallow the exception so that the stage will be marked
// as available and the post-destroy event will be fired, even if the
// stage directory can't actually be deleted. The file system service
// logs the exception, so we don't need to do anything else here.
}
// If the stage directory exists, queue it to be automatically cleaned up
// later by a queue (which may or may not happen during cron).
// @see \Drupal\package_manager\Plugin\QueueWorker\Cleaner
if ($this->stageDirectoryExists()) {
$this->queueFactory->get('package_manager_cleanup')
->createItem($this->getStageDirectory());
}
$this->storeDestroyInfo($force, $message);
$this->markAsAvailable();
$this->dispatch(new PostDestroyEvent($this));
}
/**
......@@ -596,6 +628,8 @@ class Stage implements LoggerAwareInterface {
}
if (isset($error)) {
// Ensure the error is logged for post-mortem diagnostics.
Error::logException($this->logger, $error);
if ($on_error) {
$on_error();
}
......@@ -649,7 +683,7 @@ class Stage implements LoggerAwareInterface {
)->render());
}
if ($stored_lock === [$unique_id, static::class]) {
if ($stored_lock === [$unique_id, static::class, $this->getType()]) {
$this->lock = $stored_lock;
return $this;
}
......@@ -738,6 +772,21 @@ class Stage implements LoggerAwareInterface {
return $dir;
}
/**
* Determines if the stage directory exists.
*
* @return bool
* TRUE if the directory exists, otherwise FALSE.
*/
public function stageDirectoryExists(): bool {
try {
return is_dir($this->getStageDirectory());
}
catch (\LogicException) {
return FALSE;
}
}
/**
* Checks if staged changes are being applied to the active directory.
*
......
......@@ -4,8 +4,9 @@ declare(strict_types = 1);
namespace Drupal\package_manager;
use Drupal\package_manager\Event\CollectIgnoredPathsEvent;
use Drupal\package_manager\Event\CollectPathsToExcludeEvent;
use Drupal\package_manager\Event\StatusCheckEvent;
use PhpTuf\ComposerStager\API\Path\Factory\PathFactoryInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
......@@ -21,36 +22,32 @@ trait StatusCheckTrait {
/**
* Runs a status check for a stage and returns the results, if any.
*
* @param \Drupal\package_manager\Stage $stage
* @param \Drupal\package_manager\StageBase $stage
* The stage to run the status check for.
* @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher
* (optional) The event dispatcher service.
* @param \Drupal\package_manager\PathLocator $path_locator
* (optional) The path locator service.
* @param \PhpTuf\ComposerStager\API\Path\Factory\PathFactoryInterface $path_factory
* (optional) The path factory service.
*
* @return \Drupal\package_manager\ValidationResult[]
* The results of the status check. If a readiness check was also done,
* its results will be included.
*/
protected function runStatusCheck(Stage $stage, EventDispatcherInterface $event_dispatcher = NULL): array {
protected function runStatusCheck(StageBase $stage, EventDispatcherInterface $event_dispatcher = NULL, PathLocator $path_locator = NULL, PathFactoryInterface $path_factory = NULL): array {
$event_dispatcher ??= \Drupal::service('event_dispatcher');
$path_locator ??= \Drupal::service(PathLocator::class);
$path_factory ??= \Drupal::service(PathFactoryInterface::class);
try {
$ignored_paths_event = new CollectIgnoredPathsEvent($stage);
$event_dispatcher->dispatch($ignored_paths_event);
$event = new StatusCheckEvent($stage, $ignored_paths_event->getAll());
$paths_to_exclude_event = new CollectPathsToExcludeEvent($stage, $path_locator, $path_factory);
$event_dispatcher->dispatch($paths_to_exclude_event);
}
catch (\Throwable $throwable) {
// We can dispatch the status check event without the ignored paths, but
// it must be set explicitly to NULL, to allow those status checks to run
// that do not need the ignored paths.
$event = new StatusCheckEvent($stage, NULL);
// Add the error that was encountered so that regardless of any other
// validation errors BaseRequirementsFulfilledValidator will stop the
// event propagation after the base requirement validators have run.
// @see \Drupal\package_manager\Validator\BaseRequirementsFulfilledValidator
$event->addErrorFromThrowable($throwable, t('Unable to collect the ignored paths.'));
$paths_to_exclude_event = $throwable;
}
$event_dispatcher->dispatch($event);
return $event->getResults();
$event = new StatusCheckEvent($stage, $paths_to_exclude_event);
return $event_dispatcher->dispatch($event)->getResults();
}
}
<?php
declare(strict_types = 1);
namespace Drupal\package_manager;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use PhpTuf\ComposerStager\API\Translation\Service\TranslatorInterface;
use PhpTuf\ComposerStager\API\Translation\Value\TranslatableInterface;
use PhpTuf\ComposerStager\API\Translation\Value\TranslationParametersInterface;
/**
* An adapter for interoperable string translation.
*
* This class is designed to adapt Drupal's style of string translation so it
* can be used with the Symfony-inspired architecture used by Composer Stager.
*
* If this object is cast to a string, it will be translated by Drupal's
* translation system. It will ONLY be translated by Composer Stager if the
* trans() method is explicitly called.
*
* @internal
* This is an internal part of Package Manager and may be changed or removed
* at any time without warning. External code should not interact with this
* class.
*/
final class TranslatableStringAdapter extends TranslatableMarkup implements TranslatableInterface, TranslationParametersInterface {
/**
* {@inheritdoc}
*/
public function getAll(): array {
return $this->getArguments();
}
/**
* {@inheritdoc}
*/
public function trans(?TranslatorInterface $translator = NULL, ?string $locale = NULL): string {
// This method is NEVER used by Drupal to translate the underlying string;
// it exists solely for Composer Stager's translation system to
// transparently translate Drupal strings using its own architecture.
return $translator->trans(
$this->getUntranslatedString(),
$this,
// The 'context' option is the closest analogue to the Symfony-inspired
// concept of translation domains.
$this->getOption('context'),
$locale ?? $this->getOption('langcode'),
);
}
}
<?php
declare(strict_types = 1);
namespace Drupal\package_manager;
use Drupal\Core\StringTranslation\TranslationInterface;
use PhpTuf\ComposerStager\API\Translation\Factory\TranslatableFactoryInterface;
use PhpTuf\ComposerStager\API\Translation\Service\DomainOptionsInterface;
use PhpTuf\ComposerStager\API\Translation\Value\TranslatableInterface;
use PhpTuf\ComposerStager\API\Translation\Value\TranslationParametersInterface;
/**
* Creates translatable strings that can interoperate with Composer Stager.
*
* @internal
* This is an internal part of Package Manager and may be changed or removed
* at any time without warning. External code should not interact with this
* class.
*/
final class TranslatableStringFactory implements TranslatableFactoryInterface {
/**
* Constructs a TranslatableStringFactory object.
*
* @param \PhpTuf\ComposerStager\API\Translation\Factory\TranslatableFactoryInterface $decorated
* The decorated translatable factory service.
* @param \Drupal\Core\StringTranslation\TranslationInterface $translation
* The string translation service.
*/
public function __construct(
private readonly TranslatableFactoryInterface $decorated,
private readonly TranslationInterface $translation,
) {}
/**
* {@inheritdoc}
*/
public function createDomainOptions(): DomainOptionsInterface {
return $this->decorated->createDomainOptions();
}
/**
* {@inheritdoc}
*/
public function createTranslatableMessage(string $message, ?TranslationParametersInterface $parameters = NULL, ?string $domain = NULL,): TranslatableInterface {
return new TranslatableStringAdapter(
$message,
$parameters?->getAll() ?? [],
// TranslatableMarkup's 'context' option is the closest analogue to the
// $domain parameter.
['context' => $domain ?? ''],
$this->translation,
);
}
/**
* {@inheritdoc}
*/
public function createTranslationParameters(array $parameters = []): TranslationParametersInterface {
return $this->decorated->createTranslationParameters($parameters);
}
}
......@@ -7,9 +7,12 @@ namespace Drupal\package_manager;
use Drupal\Component\Assertion\Inspector;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\system\SystemManager;
use PhpTuf\ComposerStager\API\Exception\ExceptionInterface;
/**
* A value object to contain the results of a validation.
*
* @property \Drupal\Core\StringTranslation\TranslatableMarkup[] $messages
*/
final class ValidationResult {
......@@ -20,9 +23,9 @@ final class ValidationResult {
* The severity of the result. Should be one of the
* SystemManager::REQUIREMENT_* constants.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup[]|string[] $messages
* The error messages.
* The result messages.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $summary
* A summary of the result messages.
* A succinct summary of the result.
* @param bool $assert_translatable
* Whether to assert the messages are translatable. Internal use only.
*
......@@ -30,7 +33,12 @@ final class ValidationResult {
* Thrown if $messages is empty, or if it has 2 or more items but $summary
* is NULL.
*/
private function __construct(protected int $severity, protected array $messages, protected ?TranslatableMarkup $summary, bool $assert_translatable) {
private function __construct(
public readonly int $severity,
private array $messages,
public readonly ?TranslatableMarkup $summary,
bool $assert_translatable
) {
if ($assert_translatable) {
assert(Inspector::assertAll(fn ($message) => $message instanceof TranslatableMarkup, $messages));
}
......@@ -42,6 +50,18 @@ final class ValidationResult {
}
}
/**
* Implements magic ::__get() method.
*/
public function __get(string $name): mixed {
return match ($name) {
// The messages must be private so that they cannot be mutated by external
// code, but we want to allow callers to access them in the same way as
// $this->summary and $this->severity.
'messages' => $this->messages,
};
}
/**
* Creates an error ValidationResult object from a throwable.
*
......@@ -52,8 +72,11 @@ final class ValidationResult {
*
* @return static
*/
public static function createErrorFromThrowable(\Throwable $throwable, ?TranslatableMarkup $summary = NULL): self {
return new static(SystemManager::REQUIREMENT_ERROR, [$throwable->getMessage()], $summary, FALSE);
public static function createErrorFromThrowable(\Throwable $throwable, ?TranslatableMarkup $summary = NULL): static {
// All Composer Stager exceptions are translatable.
$is_translatable = $throwable instanceof ExceptionInterface;
$message = $is_translatable ? $throwable->getTranslatableMessage() : $throwable->getMessage();
return new static(SystemManager::REQUIREMENT_ERROR, [$message], $summary, $is_translatable);
}
/**
......@@ -66,7 +89,7 @@ final class ValidationResult {
*
* @return static
*/
public static function createError(array $messages, ?TranslatableMarkup $summary = NULL): self {
public static function createError(array $messages, ?TranslatableMarkup $summary = NULL): static {
return new static(SystemManager::REQUIREMENT_ERROR, $messages, $summary, TRUE);
}
......@@ -80,41 +103,10 @@ final class ValidationResult {
*
* @return static
*/
public static function createWarning(array $messages, ?TranslatableMarkup $summary = NULL): self {
public static function createWarning(array $messages, ?TranslatableMarkup $summary = NULL): static {
return new static(SystemManager::REQUIREMENT_WARNING, $messages, $summary, TRUE);
}
/**
* Gets the summary.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup|null
* The summary.
*/
public function getSummary(): ?TranslatableMarkup {
return $this->summary;
}
/**
* Gets the messages.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup[]|string[]
* The error or warning messages.
*/
public function getMessages(): array {
return $this->messages;
}
/**
* The severity of the result.
*
* @return int
* Either SystemManager::REQUIREMENT_ERROR or
* SystemManager::REQUIREMENT_WARNING.
*/
public function getSeverity(): int {
return $this->severity;
}
/**
* Returns the overall severity for a set of validation results.
*
......@@ -127,7 +119,7 @@ final class ValidationResult {
*/
public static function getOverallSeverity(array $results): int {
foreach ($results as $result) {
if ($result->getSeverity() === SystemManager::REQUIREMENT_ERROR) {
if ($result->severity === SystemManager::REQUIREMENT_ERROR) {
return SystemManager::REQUIREMENT_ERROR;
}
}
......@@ -149,9 +141,9 @@ final class ValidationResult {
*/
public static function isEqual(self $a, self $b): bool {
return (
$a->getSeverity() === $b->getSeverity() &&
strval($a->getSummary()) === strval($b->getSummary()) &&
array_map('strval', $a->getMessages()) === array_map('strval', $b->getMessages())
$a->severity === $b->severity &&
strval($a->summary) === strval($b->summary) &&
array_map('strval', $a->messages) === array_map('strval', $b->messages)
);
}
......
......@@ -4,6 +4,7 @@ declare(strict_types = 1);
namespace Drupal\package_manager\Validator;
use Drupal\Component\Serialization\Json;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\package_manager\Event\StatusCheckEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
......@@ -34,8 +35,8 @@ final class AllowedScaffoldPackagesValidator implements EventSubscriberInterface
* The path locator service.
*/
public function __construct(
private ComposerInspector $composerInspector,
private PathLocator $pathLocator,
private readonly ComposerInspector $composerInspector,
private readonly PathLocator $pathLocator,
) {}
/**
......@@ -52,7 +53,7 @@ final class AllowedScaffoldPackagesValidator implements EventSubscriberInterface
"drupal/legacy-scaffold-assets",
"drupal/core",
];
$extra = json_decode($this->composerInspector->getConfig('extra', $path . '/composer.json'), TRUE);
$extra = Json::decode($this->composerInspector->getConfig('extra', $path . '/composer.json'));
$allowed_packages = $extra['drupal-scaffold']['allowed-packages'] ?? [];
$extra_packages = array_diff($allowed_packages, $implicitly_allowed_packages);
if (!empty($extra_packages)) {
......
......@@ -7,6 +7,7 @@ namespace Drupal\package_manager\Validator;
use Drupal\package_manager\Event\PreApplyEvent;
use Drupal\package_manager\Event\PreCreateEvent;
use Drupal\package_manager\Event\PreOperationStageEvent;
use Drupal\package_manager\Event\PreRequireEvent;
use Drupal\package_manager\Event\StatusCheckEvent;
/**
......@@ -40,6 +41,7 @@ trait BaseRequirementValidatorTrait {
return [
PreCreateEvent::class => ['validate', $priority],
PreRequireEvent::class => ['validate', $priority],
PreApplyEvent::class => ['validate', $priority],
StatusCheckEvent::class => ['validate', $priority],
];
......