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 696 additions and 209 deletions
<?php
declare(strict_types = 1);
namespace Drupal\package_manager;
use PhpTuf\ComposerStager\API\Path\Value\PathListInterface;
/**
* Defines a path list that cannot be changed.
*
* @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 ImmutablePathList implements PathListInterface {
/**
* Constructs an ImmutablePathList object.
*
* @param \PhpTuf\ComposerStager\API\Path\Value\PathListInterface $decorated
* The decorated path list.
*/
public function __construct(private readonly PathListInterface $decorated) {}
/**
* {@inheritdoc}
*/
public function add(string ...$paths): never {
throw new \LogicException('Immutable path lists cannot be changed.');
}
/**
* {@inheritdoc}
*/
public function getAll(): array {
return $this->decorated->getAll();
}
}
...@@ -40,9 +40,12 @@ final class InstalledPackage { ...@@ -40,9 +40,12 @@ final class InstalledPackage {
*/ */
public static function createFromArray(array $data): static { public static function createFromArray(array $data): static {
$path = isset($data['path']) ? realpath($data['path']) : NULL; $path = isset($data['path']) ? realpath($data['path']) : NULL;
assert(($data['type'] === 'metapackage') === is_null($path), 'Metapackage install path must be NULL.'); // Fall back to `library`.
// @see https://getcomposer.org/doc/04-schema.md#type
$type = $data['type'] ?? 'library';
assert(($type === 'metapackage') === is_null($path), 'Metapackage install path must be NULL.');
return new static($data['name'], $data['version'], $path, $data['type']); return new static($data['name'], $data['version'], $path, $type);
} }
/** /**
......
...@@ -6,9 +6,11 @@ namespace Drupal\package_manager; ...@@ -6,9 +6,11 @@ namespace Drupal\package_manager;
use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\StringTranslation\StringTranslationTrait;
use PhpTuf\ComposerStager\Domain\Service\Precondition\NoSymlinksPointToADirectoryInterface; use PhpTuf\ComposerStager\API\Path\Value\PathInterface;
use PhpTuf\ComposerStager\Domain\Value\Path\PathInterface; use PhpTuf\ComposerStager\API\Path\Value\PathListInterface;
use PhpTuf\ComposerStager\Domain\Value\PathList\PathListInterface; use PhpTuf\ComposerStager\API\Precondition\Service\NoSymlinksPointToADirectoryInterface;
use PhpTuf\ComposerStager\API\Process\Service\ProcessInterface;
use PhpTuf\ComposerStager\API\Translation\Value\TranslatableInterface;
/** /**
* Checks if the code base contains any symlinks that point to a directory. * Checks if the code base contains any symlinks that point to a directory.
...@@ -29,7 +31,7 @@ final class NoSymlinksPointToADirectory implements NoSymlinksPointToADirectoryIn ...@@ -29,7 +31,7 @@ final class NoSymlinksPointToADirectory implements NoSymlinksPointToADirectoryIn
/** /**
* Constructs a NoSymlinksPointToADirectory object. * Constructs a NoSymlinksPointToADirectory object.
* *
* @param \PhpTuf\ComposerStager\Domain\Service\Precondition\NoSymlinksPointToADirectoryInterface $decorated * @param \PhpTuf\ComposerStager\API\Precondition\Service\NoSymlinksPointToADirectoryInterface $decorated
* The decorated precondition. * The decorated precondition.
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* The config factory. * The config factory.
...@@ -42,21 +44,21 @@ final class NoSymlinksPointToADirectory implements NoSymlinksPointToADirectoryIn ...@@ -42,21 +44,21 @@ final class NoSymlinksPointToADirectory implements NoSymlinksPointToADirectoryIn
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function getName(): string { public function getName(): TranslatableInterface {
return $this->decorated->getName(); return $this->decorated->getName();
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function getDescription(): string { public function getDescription(): TranslatableInterface {
return $this->decorated->getDescription(); return $this->decorated->getDescription();
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function getStatusMessage(PathInterface $activeDir, PathInterface $stagingDir, ?PathListInterface $exclusions = NULL,): string { public function getStatusMessage(PathInterface $activeDir, PathInterface $stagingDir, ?PathListInterface $exclusions = NULL, int $timeout = ProcessInterface::DEFAULT_TIMEOUT,): TranslatableInterface {
if ($this->isUsingRsync()) { if ($this->isUsingRsync()) {
return $this->t('Symlinks to directories are supported by the rsync file syncer.'); return $this->t('Symlinks to directories are supported by the rsync file syncer.');
} }
...@@ -66,14 +68,14 @@ final class NoSymlinksPointToADirectory implements NoSymlinksPointToADirectoryIn ...@@ -66,14 +68,14 @@ final class NoSymlinksPointToADirectory implements NoSymlinksPointToADirectoryIn
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function isFulfilled(PathInterface $activeDir, PathInterface $stagingDir, ?PathListInterface $exclusions = NULL,): bool { public function isFulfilled(PathInterface $activeDir, PathInterface $stagingDir, ?PathListInterface $exclusions = NULL, int $timeout = ProcessInterface::DEFAULT_TIMEOUT,): bool {
return $this->isUsingRsync() || $this->decorated->isFulfilled($activeDir, $stagingDir, $exclusions); return $this->isUsingRsync() || $this->decorated->isFulfilled($activeDir, $stagingDir, $exclusions);
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function assertIsFulfilled(PathInterface $activeDir, PathInterface $stagingDir, ?PathListInterface $exclusions = NULL,): void { public function assertIsFulfilled(PathInterface $activeDir, PathInterface $stagingDir, ?PathListInterface $exclusions = NULL, int $timeout = ProcessInterface::DEFAULT_TIMEOUT,): void {
if ($this->isUsingRsync()) { if ($this->isUsingRsync()) {
return; return;
} }
...@@ -94,4 +96,11 @@ final class NoSymlinksPointToADirectory implements NoSymlinksPointToADirectoryIn ...@@ -94,4 +96,11 @@ final class NoSymlinksPointToADirectory implements NoSymlinksPointToADirectoryIn
return $syncer === 'rsync'; return $syncer === 'rsync';
} }
/**
* {@inheritdoc}
*/
public function getLeaves(): array {
return [$this];
}
} }
...@@ -6,8 +6,7 @@ namespace Drupal\package_manager; ...@@ -6,8 +6,7 @@ namespace Drupal\package_manager;
use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase; use Drupal\Core\DependencyInjection\ServiceProviderBase;
use PhpTuf\ComposerStager\Domain\Core\Beginner\BeginnerInterface; use PhpTuf\ComposerStager\API\Core\BeginnerInterface;
use PhpTuf\ComposerStager\Domain\Service\Precondition\NoSymlinksPointToADirectoryInterface;
/** /**
* Defines dynamic container services for Package Manager. * Defines dynamic container services for Package Manager.
...@@ -33,13 +32,15 @@ final class PackageManagerServiceProvider extends ServiceProviderBase { ...@@ -33,13 +32,15 @@ final class PackageManagerServiceProvider extends ServiceProviderBase {
// Use an interface that we know exists to determine the absolute path where // Use an interface that we know exists to determine the absolute path where
// Composer Stager is installed. // Composer Stager is installed.
$mirror = new \ReflectionClass(BeginnerInterface::class); $mirror = new \ReflectionClass(BeginnerInterface::class);
$path = dirname($mirror->getFileName(), 4); $path = dirname($mirror->getFileName(), 3);
// Certain subdirectories of Composer Stager shouldn't be scanned for // Certain subdirectories of Composer Stager shouldn't be scanned for
// services. // services.
$ignore_directories = [ $ignore_directories = [
$path . '/Domain/Exception', $path . '/API/Exception',
$path . '/Infrastructure/Value', $path . '/Internal/Helper',
$path . '/Internal/Path/Value',
$path . '/Internal/Translation/Value',
]; ];
// As we scan for services, compile a list of which classes implement which // As we scan for services, compile a list of which classes implement which
// interfaces so that we can set up aliases for interfaces that are only // interfaces so that we can set up aliases for interfaces that are only
...@@ -108,6 +109,9 @@ final class PackageManagerServiceProvider extends ServiceProviderBase { ...@@ -108,6 +109,9 @@ final class PackageManagerServiceProvider extends ServiceProviderBase {
'class_resolver' => 'Drupal\Core\DependencyInjection\ClassResolverInterface', 'class_resolver' => 'Drupal\Core\DependencyInjection\ClassResolverInterface',
'request_stack' => 'Symfony\Component\HttpFoundation\RequestStack', 'request_stack' => 'Symfony\Component\HttpFoundation\RequestStack',
'theme_handler' => 'Drupal\Core\Extension\ThemeHandlerInterface', 'theme_handler' => 'Drupal\Core\Extension\ThemeHandlerInterface',
'cron' => 'Drupal\Core\CronInterface',
'logger.factory' => 'Drupal\Core\Logger\LoggerChannelFactoryInterface',
'string_translation' => 'Drupal\Core\StringTranslation\TranslationInterface',
]; ];
foreach ($aliases as $service_id => $alias) { foreach ($aliases as $service_id => $alias) {
if (!$container->hasAlias($alias)) { if (!$container->hasAlias($alias)) {
...@@ -115,11 +119,6 @@ final class PackageManagerServiceProvider extends ServiceProviderBase { ...@@ -115,11 +119,6 @@ final class PackageManagerServiceProvider extends ServiceProviderBase {
} }
} }
// END: DELETE FROM CORE MERGE REQUEST // END: DELETE FROM CORE MERGE REQUEST
// Decorate certain Composer Stager preconditions.
$container->register(NoSymlinksPointToADirectory::class)
->setPublic(FALSE)
->setAutowired(TRUE)
->setDecoratedService(NoSymlinksPointToADirectoryInterface::class);
} }
} }
...@@ -4,11 +4,16 @@ declare(strict_types = 1); ...@@ -4,11 +4,16 @@ declare(strict_types = 1);
namespace Drupal\package_manager; namespace Drupal\package_manager;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Extension\ModuleUninstallValidatorInterface; use Drupal\Core\Extension\ModuleUninstallValidatorInterface;
use Drupal\Core\Queue\QueueFactory;
use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\StringTranslation\StringTranslationTrait;
use PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface; use Drupal\Core\TempStore\SharedTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerAwareInterface; use PhpTuf\ComposerStager\API\Core\BeginnerInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait; use PhpTuf\ComposerStager\API\Core\CommitterInterface;
use PhpTuf\ComposerStager\API\Core\StagerInterface;
use PhpTuf\ComposerStager\API\Path\Factory\PathFactoryInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/** /**
* Prevents any module from being uninstalled if update is in process. * Prevents any module from being uninstalled if update is in process.
...@@ -18,26 +23,62 @@ use Symfony\Component\DependencyInjection\ContainerAwareTrait; ...@@ -18,26 +23,62 @@ use Symfony\Component\DependencyInjection\ContainerAwareTrait;
* at any time without warning. External code should not interact with this * at any time without warning. External code should not interact with this
* class. * class.
*/ */
final class PackageManagerUninstallValidator implements ModuleUninstallValidatorInterface, ContainerAwareInterface { final class PackageManagerUninstallValidator implements ModuleUninstallValidatorInterface {
use ContainerAwareTrait;
use StringTranslationTrait; use StringTranslationTrait;
/**
* Constructs a new PackageManagerUninstallValidator object.
*
* @param \Drupal\package_manager\PathLocator $pathLocator
* The path locator service.
* @param \PhpTuf\ComposerStager\API\Core\BeginnerInterface $beginner
* The beginner service.
* @param \PhpTuf\ComposerStager\API\Core\StagerInterface $stager
* The stager service.
* @param \PhpTuf\ComposerStager\API\Core\CommitterInterface $committer
* The committer service.
* @param \Drupal\Core\Queue\QueueFactory $queueFactory
* The queue factory service.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
* The event dispatcher service.
* @param \Drupal\Core\TempStore\SharedTempStoreFactory $sharedTempStoreFactory
* The shared temp store factory service.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
* @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(
private readonly PathLocator $pathLocator,
private readonly BeginnerInterface $beginner,
private readonly StagerInterface $stager,
private readonly CommitterInterface $committer,
private readonly QueueFactory $queueFactory,
private readonly EventDispatcherInterface $eventDispatcher,
private readonly SharedTempStoreFactory $sharedTempStoreFactory,
private readonly TimeInterface $time,
private readonly PathFactoryInterface $pathFactory,
private readonly FailureMarker $failureMarker
) {}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function validate($module) { public function validate($module) {
$stage = new class( $stage = new class(
$this->container->get('package_manager.path_locator'), $this->pathLocator,
$this->container->get('package_manager.beginner'), $this->beginner,
$this->container->get('package_manager.stager'), $this->stager,
$this->container->get('package_manager.committer'), $this->committer,
$this->container->get('file_system'), $this->queueFactory,
$this->container->get('event_dispatcher'), $this->eventDispatcher,
$this->container->get('tempstore.shared'), $this->sharedTempStoreFactory,
$this->container->get('datetime.time'), $this->time,
$this->container->get(PathFactoryInterface::class), $this->pathFactory,
$this->container->get('package_manager.failure_marker')) extends StageBase {}; $this->failureMarker) extends StageBase {};
if ($stage->isAvailable() || !$stage->isApplying()) { if ($stage->isAvailable() || !$stage->isApplying()) {
return []; return [];
} }
......
...@@ -4,6 +4,7 @@ declare(strict_types = 1); ...@@ -4,6 +4,7 @@ declare(strict_types = 1);
namespace Drupal\package_manager; namespace Drupal\package_manager;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface; use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface; use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
...@@ -48,18 +49,24 @@ final class PackageManagerUpdateProcessor extends UpdateProcessor { ...@@ -48,18 +49,24 @@ final class PackageManagerUpdateProcessor extends UpdateProcessor {
* The key/value factory. * The key/value factory.
* @param \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface $key_value_expirable_factory * @param \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface $key_value_expirable_factory
* The expirable key/value 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) { public function __construct(
$this->updateFetcher = $update_fetcher; ConfigFactoryInterface $config_factory,
$this->updateSettings = $config_factory->get('update.settings'); 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->fetchQueue = $queue_factory->get('package_manager.update_fetch_tasks');
$this->tempStore = $key_value_expirable_factory->get('package_manager.update'); $this->tempStore = $key_value_expirable_factory->get('package_manager.update');
$this->fetchTaskStore = $key_value_factory->get('package_manager.update_fetch_task'); $this->fetchTaskStore = $key_value_factory->get('package_manager.update_fetch_task');
$this->availableReleasesTempStore = $key_value_expirable_factory->get('package_manager.update_available_releases'); $this->availableReleasesTempStore = $key_value_expirable_factory->get('package_manager.update_available_releases');
$this->stateStore = $state_store;
$this->privateKey = $private_key;
$this->fetchTasks = [];
$this->failed = [];
} }
/** /**
......
...@@ -4,7 +4,12 @@ declare(strict_types = 1); ...@@ -4,7 +4,12 @@ declare(strict_types = 1);
namespace Drupal\package_manager\PathExcluder; namespace Drupal\package_manager\PathExcluder;
use Drupal\Core\File\Exception\FileException;
use Drupal\Core\File\FileSystemInterface;
use Drupal\package_manager\Event\CollectPathsToExcludeEvent; 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; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/** /**
...@@ -22,8 +27,16 @@ class SiteConfigurationExcluder implements EventSubscriberInterface { ...@@ -22,8 +27,16 @@ class SiteConfigurationExcluder implements EventSubscriberInterface {
* *
* @param string $sitePath * @param string $sitePath
* The current site path, relative to the Drupal root. * The current site path, relative to the Drupal root.
* @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) {} public function __construct(
protected string $sitePath,
private readonly PathLocator $pathLocator,
private readonly FileSystemInterface $fileSystem
) {}
/** /**
* Excludes site configuration files from stage operations. * Excludes site configuration files from stage operations.
...@@ -32,8 +45,11 @@ class SiteConfigurationExcluder implements EventSubscriberInterface { ...@@ -32,8 +45,11 @@ class SiteConfigurationExcluder implements EventSubscriberInterface {
* The event object. * The event object.
*/ */
public function excludeSiteConfiguration(CollectPathsToExcludeEvent $event): void { public function excludeSiteConfiguration(CollectPathsToExcludeEvent $event): void {
// Site configuration files are always excluded relative to the web root. // These two files are never relevant to existing sites.
$paths = []; $paths = [
'sites/default/default.settings.php',
'sites/default/default.services.yml',
];
// Exclude 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. // By default, Drupal core will always try to write-protect these files.
...@@ -46,15 +62,92 @@ class SiteConfigurationExcluder implements EventSubscriberInterface { ...@@ -46,15 +62,92 @@ class SiteConfigurationExcluder implements EventSubscriberInterface {
$paths[] = $this->sitePath . '/' . $settings_file; $paths[] = $this->sitePath . '/' . $settings_file;
$paths[] = 'sites/default/' . $settings_file; $paths[] = 'sites/default/' . $settings_file;
} }
// Site configuration files are always excluded relative to the web root.
$event->addPathsRelativeToWebRoot($paths); $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']);
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public static function getSubscribedEvents(): array { public static function getSubscribedEvents(): array {
return [ return [
CollectPathsToExcludeEvent::class => 'excludeSiteConfiguration', CollectPathsToExcludeEvent::class => 'excludeSiteConfiguration',
PostCreateEvent::class => 'makeDefaultSiteDirectoryWritable',
PreApplyEvent::class => 'syncDefaultSiteDirectoryPermissions',
]; ];
} }
......
...@@ -59,7 +59,9 @@ final class SiteFilesExcluder implements EventSubscriberInterface { ...@@ -59,7 +59,9 @@ final class SiteFilesExcluder implements EventSubscriberInterface {
$path = $wrapper->getDirectoryPath(); $path = $wrapper->getDirectoryPath();
if ($this->fileSystem->isAbsolutePath($path)) { if ($this->fileSystem->isAbsolutePath($path)) {
$event->addPathsRelativeToProjectRoot([realpath($path)]); if ($path = realpath($path)) {
$event->addPathsRelativeToProjectRoot([$path]);
}
} }
else { else {
$event->addPathsRelativeToWebRoot([$path]); $event->addPathsRelativeToWebRoot([$path]);
......
...@@ -6,7 +6,7 @@ namespace Drupal\package_manager\PathExcluder; ...@@ -6,7 +6,7 @@ namespace Drupal\package_manager\PathExcluder;
use Drupal\Core\Database\Connection; use Drupal\Core\Database\Connection;
use Drupal\package_manager\Event\CollectPathsToExcludeEvent; use Drupal\package_manager\Event\CollectPathsToExcludeEvent;
use Drupal\package_manager\PathLocator; use PhpTuf\ComposerStager\API\Path\Factory\PathFactoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/** /**
...@@ -22,16 +22,14 @@ class SqliteDatabaseExcluder implements EventSubscriberInterface { ...@@ -22,16 +22,14 @@ class SqliteDatabaseExcluder implements EventSubscriberInterface {
/** /**
* Constructs a SqliteDatabaseExcluder object. * Constructs a SqliteDatabaseExcluder object.
* *
* @param \Drupal\package_manager\PathLocator $pathLocator * @param \PhpTuf\ComposerStager\API\Path\Factory\PathFactoryInterface $pathFactory
* The path locator service. * The path factory service.
* @param \Drupal\Core\Database\Connection $database * @param \Drupal\Core\Database\Connection $database
* The database connection. * The database connection.
*/ */
public function __construct( public function __construct(
private readonly PathLocator $pathLocator, private readonly PathFactoryInterface $pathFactory,
// TRICKY: this cannot be private nor readonly for testing purposes. private readonly Connection $database,
// @see \Drupal\Tests\package_manager\Kernel\PathExcluder\SqliteDatabaseExcluderTest::mockDatabase()
protected Connection $database
) {} ) {}
/** /**
...@@ -50,19 +48,27 @@ class SqliteDatabaseExcluder implements EventSubscriberInterface { ...@@ -50,19 +48,27 @@ class SqliteDatabaseExcluder implements EventSubscriberInterface {
* The event object. * The event object.
*/ */
public function excludeDatabaseFiles(CollectPathsToExcludeEvent $event): void { public function excludeDatabaseFiles(CollectPathsToExcludeEvent $event): void {
// If the database is SQLite, it might be located in the active directory // If the database is SQLite, it might be located in the project directory
// and we should exclude it. Always treat it as relative to the project root. // and we should exclude it.
if ($this->database->driver() === 'sqlite') { if ($this->database->driver() === 'sqlite') {
$options = $this->database->getConnectionOptions(); $db_path = $this->database->getConnectionOptions()['database'];
// Nothing to exclude if the database lives outside the project root. // Exclude the database file and auxiliary files created by SQLite.
if (str_starts_with($options['database'], '/') && !str_starts_with($options['database'], $this->pathLocator->getProjectRoot())) { $paths = [$db_path, "$db_path-shm", "$db_path-wal"];
return;
// 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);
} }
$event->addPathsRelativeToProjectRoot([
$options['database'],
$options['database'] . '-shm',
$options['database'] . '-wal',
]);
} }
} }
......
...@@ -5,9 +5,15 @@ declare(strict_types = 1); ...@@ -5,9 +5,15 @@ declare(strict_types = 1);
namespace Drupal\package_manager\PathExcluder; namespace Drupal\package_manager\PathExcluder;
use Drupal\Component\Serialization\Json; use Drupal\Component\Serialization\Json;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\package_manager\ComposerInspector; use Drupal\package_manager\ComposerInspector;
use Drupal\package_manager\Event\CollectPathsToExcludeEvent; use Drupal\package_manager\Event\CollectPathsToExcludeEvent;
use Drupal\package_manager\Event\StatusCheckEvent;
use Drupal\package_manager\PathLocator; use Drupal\package_manager\PathLocator;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/** /**
...@@ -23,12 +29,22 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; ...@@ -23,12 +29,22 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
* *
* If web root and project root are the same, nothing is excluded. * 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 * @internal
* This is an internal part of Package Manager and may be changed or removed * 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 * at any time without warning. External code should not interact with this
* class. * class.
*/ */
final class UnknownPathExcluder implements EventSubscriberInterface { final class UnknownPathExcluder implements EventSubscriberInterface, LoggerAwareInterface {
use LoggerAwareTrait;
use StringTranslationTrait;
/** /**
* Constructs a UnknownPathExcluder object. * Constructs a UnknownPathExcluder object.
...@@ -37,11 +53,16 @@ final class UnknownPathExcluder implements EventSubscriberInterface { ...@@ -37,11 +53,16 @@ final class UnknownPathExcluder implements EventSubscriberInterface {
* The Composer inspector service. * The Composer inspector service.
* @param \Drupal\package_manager\PathLocator $pathLocator * @param \Drupal\package_manager\PathLocator $pathLocator
* The path locator service. * The path locator service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* The config factory service.
*/ */
public function __construct( public function __construct(
private readonly ComposerInspector $composerInspector, private readonly ComposerInspector $composerInspector,
private readonly PathLocator $pathLocator, private readonly PathLocator $pathLocator,
) {} private readonly ConfigFactoryInterface $configFactory,
) {
$this->setLogger(new NullLogger());
}
/** /**
* {@inheritdoc} * {@inheritdoc}
...@@ -49,53 +70,110 @@ final class UnknownPathExcluder implements EventSubscriberInterface { ...@@ -49,53 +70,110 @@ final class UnknownPathExcluder implements EventSubscriberInterface {
public static function getSubscribedEvents(): array { public static function getSubscribedEvents(): array {
return [ return [
CollectPathsToExcludeEvent::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\CollectPathsToExcludeEvent $event * @return string[]
* The event object. * The paths that should be excluded from stage operations, relative to the
* project root.
* *
* @throws \Exception * @throws \Exception
* See \Drupal\package_manager\ComposerInspector::validate(). * See \Drupal\package_manager\ComposerInspector::validate().
*/ */
public function excludeUnknownPaths(CollectPathsToExcludeEvent $event): void { private function getExcludedPaths(): array {
$project_root = $this->pathLocator->getProjectRoot(); // If this excluder is disabled, or the project root and web root are the
$web_root = $project_root . DIRECTORY_SEPARATOR . $this->pathLocator->getWebRoot(); // same, we are not excluding any paths.
if (realpath($web_root) === $project_root) { $is_disabled = $this->configFactory->get('package_manager.settings')
return; ->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 // To determine the files to include, the installed packages must be known,
// be known, and that requires Composer commands to be able to run. This // and that requires Composer commands to be able to run. This intentionally
// intentionally does not catch exceptions: failed Composer validation in // does not catch exceptions: failed Composer validation in the project root
// the project root implies that this excluder cannot function correctly. // implies that this excluder cannot function correctly.
// Note: the call to ComposerInspector::getInstalledPackagesList() would // Note: the call to ComposerInspector::getConfig() would
// also have triggered this, but explicitness is preferred here. // also have triggered this, but explicitness is preferred here.
// @see \Drupal\package_manager\StatusCheckTrait::runStatusCheck() // @see \Drupal\package_manager\StatusCheckTrait::runStatusCheck()
$project_root = $this->pathLocator->getProjectRoot();
$this->composerInspector->validate($project_root); $this->composerInspector->validate($project_root);
$vendor_dir = $this->pathLocator->getVendorDirectory(); // The vendor directory and web root are always included in staging
$scaffold_files_paths = $this->getScaffoldFiles(); // operations, along with `composer.json`, `composer.lock`, and any scaffold
// Search for all files (including hidden ones) in project root. // files provided by Drupal core.
$paths_in_project_root = glob("$project_root/{,.}*", GLOB_BRACE); $always_include = [
$paths = []; $this->composerInspector->getConfig('vendor-dir', $project_root),
$known_paths = array_merge([$vendor_dir, $web_root, "$project_root/composer.json", "$project_root/composer.lock"], $scaffold_files_paths); $web_root,
foreach ($paths_in_project_root as $path_in_project_root) { 'composer.json',
if (!in_array($path_in_project_root, $known_paths, TRUE)) { 'composer.lock',
$paths[] = $path_in_project_root; ];
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);
} }
$event->addPathsRelativeToProjectRoot($paths);
} }
/** /**
* Gets the path of scaffold files, for example 'index.php' and 'robots.txt'. * Gets the path of scaffold files, for example 'index.php' and 'robots.txt'.
* *
* @return array * @return string[]
* The array of scaffold file paths. * 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. * @todo Intelligently load scaffold files in https://drupal.org/i/3343802.
*/ */
...@@ -103,10 +181,9 @@ final class UnknownPathExcluder implements EventSubscriberInterface { ...@@ -103,10 +181,9 @@ final class UnknownPathExcluder implements EventSubscriberInterface {
$project_root = $this->pathLocator->getProjectRoot(); $project_root = $this->pathLocator->getProjectRoot();
$packages = $this->composerInspector->getInstalledPackagesList($project_root); $packages = $this->composerInspector->getInstalledPackagesList($project_root);
$extra = Json::decode($this->composerInspector->getConfig('extra', $packages['drupal/core']->path . '/composer.json')); $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']); $scaffold_files = $extra['drupal-scaffold']['file-mapping'] ?? [];
} return str_replace('[project-root]', '', array_keys($scaffold_files));
return [];
} }
} }
<?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; ...@@ -6,9 +6,8 @@ namespace Drupal\package_manager;
use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\File\FileSystemInterface; use Drupal\Core\File\FileSystemInterface;
use PhpTuf\ComposerStager\Infrastructure\Factory\Process\ProcessFactoryInterface; use PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface;
use PhpTuf\ComposerStager\Infrastructure\Factory\Process\ProcessFactory as StagerProcessFactory; use PhpTuf\ComposerStager\API\Process\Service\ProcessInterface;
use Symfony\Component\Process\Process;
// cspell:ignore BINDIR // cspell:ignore BINDIR
...@@ -22,13 +21,6 @@ use Symfony\Component\Process\Process; ...@@ -22,13 +21,6 @@ use Symfony\Component\Process\Process;
*/ */
final class ProcessFactory implements ProcessFactoryInterface { final class ProcessFactory implements ProcessFactoryInterface {
/**
* The decorated process factory.
*
* @var \PhpTuf\ComposerStager\Infrastructure\Factory\Process\ProcessFactoryInterface
*/
private $decorated;
/** /**
* Constructs a ProcessFactory object. * Constructs a ProcessFactory object.
* *
...@@ -36,13 +28,14 @@ final class ProcessFactory implements ProcessFactoryInterface { ...@@ -36,13 +28,14 @@ final class ProcessFactory implements ProcessFactoryInterface {
* The file system service. * The file system service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* The config factory service. * The config factory service.
* @param \PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface $decorated
* The decorated process factory service.
*/ */
public function __construct( public function __construct(
private readonly FileSystemInterface $fileSystem, private readonly FileSystemInterface $fileSystem,
private readonly ConfigFactoryInterface $configFactory private readonly ConfigFactoryInterface $configFactory,
) { private readonly ProcessFactoryInterface $decorated,
$this->decorated = new StagerProcessFactory(); ) {}
}
/** /**
* Returns the value of an environment variable. * Returns the value of an environment variable.
...@@ -63,11 +56,11 @@ final class ProcessFactory implements ProcessFactoryInterface { ...@@ -63,11 +56,11 @@ final class ProcessFactory implements ProcessFactoryInterface {
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function create(array $command): Process { public function create(array $command): ProcessInterface {
$process = $this->decorated->create($command); $process = $this->decorated->create($command);
$env = $process->getEnv(); $env = $process->getEnv();
if ($this->isComposerCommand($command)) { if ($command && $this->isComposerCommand($command)) {
$env['COMPOSER_HOME'] = $this->getComposerHomePath(); $env['COMPOSER_HOME'] = $this->getComposerHomePath();
} }
// Ensure that the current PHP installation is the first place that will be // Ensure that the current PHP installation is the first place that will be
...@@ -87,7 +80,7 @@ final class ProcessFactory implements ProcessFactoryInterface { ...@@ -87,7 +80,7 @@ final class ProcessFactory implements ProcessFactoryInterface {
* @see php_sapi_name() * @see php_sapi_name()
* @see https://www.php.net/manual/en/reserved.constants.php * @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') { if (PHP_SAPI === 'cli' || PHP_SAPI === 'cli-server') {
return dirname(PHP_BINARY); return dirname(PHP_BINARY);
} }
......
...@@ -4,7 +4,8 @@ declare(strict_types = 1); ...@@ -4,7 +4,8 @@ declare(strict_types = 1);
namespace Drupal\package_manager; namespace Drupal\package_manager;
use PhpTuf\ComposerStager\Domain\Service\ProcessOutputCallback\ProcessOutputCallbackInterface; use PhpTuf\ComposerStager\API\Process\Service\OutputCallbackInterface;
use PhpTuf\ComposerStager\API\Process\Value\OutputTypeEnum;
use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait; use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger; use Psr\Log\NullLogger;
...@@ -19,7 +20,7 @@ use Psr\Log\NullLogger; ...@@ -19,7 +20,7 @@ use Psr\Log\NullLogger;
* at any time without warning. External code should not interact with this * at any time without warning. External code should not interact with this
* class. * class.
*/ */
final class ProcessOutputCallback implements ProcessOutputCallbackInterface, LoggerAwareInterface { final class ProcessOutputCallback implements OutputCallbackInterface, LoggerAwareInterface {
use LoggerAwareTrait; use LoggerAwareTrait;
...@@ -47,16 +48,14 @@ final class ProcessOutputCallback implements ProcessOutputCallbackInterface, Log ...@@ -47,16 +48,14 @@ final class ProcessOutputCallback implements ProcessOutputCallbackInterface, Log
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function __invoke(string $type, string $buffer): void { public function __invoke(OutputTypeEnum $type, string $buffer): void {
if ($type === self::OUT) {
if ($type === OutputTypeEnum::OUT) {
$this->outBuffer .= $buffer; $this->outBuffer .= $buffer;
} }
elseif ($type === self::ERR) { elseif ($type === OutputTypeEnum::ERR) {
$this->errorBuffer .= $buffer; $this->errorBuffer .= $buffer;
} }
else {
throw new \InvalidArgumentException("Unsupported output type: '$type'");
}
} }
/** /**
......
...@@ -192,7 +192,7 @@ final class ProjectInfo { ...@@ -192,7 +192,7 @@ final class ProjectInfo {
// update processor service. // update processor service.
if (!isset($available_projects[$this->name])) { if (!isset($available_projects[$this->name])) {
/** @var \Drupal\package_manager\PackageManagerUpdateProcessor $update_processor */ /** @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)) { if ($project_data = $update_processor->getProjectData($this->name)) {
$available_projects[$this->name] = $project_data; $available_projects[$this->name] = $project_data;
} }
......
...@@ -6,21 +6,19 @@ namespace Drupal\package_manager; ...@@ -6,21 +6,19 @@ namespace Drupal\package_manager;
use Composer\Semver\VersionParser; use Composer\Semver\VersionParser;
use Drupal\Component\Datetime\TimeInterface; use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Utility\Crypt; use Drupal\Component\Utility\Random;
use Drupal\Core\File\Exception\FileException; use Drupal\Core\Queue\QueueFactory;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TempStore\SharedTempStore; use Drupal\Core\TempStore\SharedTempStore;
use Drupal\Core\TempStore\SharedTempStoreFactory; use Drupal\Core\TempStore\SharedTempStoreFactory;
use Drupal\Core\Utility\Error;
use Drupal\package_manager\Event\CollectPathsToExcludeEvent; use Drupal\package_manager\Event\CollectPathsToExcludeEvent;
use Drupal\package_manager\Event\PostApplyEvent; use Drupal\package_manager\Event\PostApplyEvent;
use Drupal\package_manager\Event\PostCreateEvent; use Drupal\package_manager\Event\PostCreateEvent;
use Drupal\package_manager\Event\PostDestroyEvent;
use Drupal\package_manager\Event\PostRequireEvent; use Drupal\package_manager\Event\PostRequireEvent;
use Drupal\package_manager\Event\PreApplyEvent; use Drupal\package_manager\Event\PreApplyEvent;
use Drupal\package_manager\Event\PreCreateEvent; use Drupal\package_manager\Event\PreCreateEvent;
use Drupal\package_manager\Event\PreDestroyEvent;
use Drupal\package_manager\Event\PreOperationStageEvent; use Drupal\package_manager\Event\PreOperationStageEvent;
use Drupal\package_manager\Event\PreRequireEvent; use Drupal\package_manager\Event\PreRequireEvent;
use Drupal\package_manager\Event\StageEvent; use Drupal\package_manager\Event\StageEvent;
...@@ -28,13 +26,13 @@ use Drupal\package_manager\Exception\ApplyFailedException; ...@@ -28,13 +26,13 @@ use Drupal\package_manager\Exception\ApplyFailedException;
use Drupal\package_manager\Exception\StageEventException; use Drupal\package_manager\Exception\StageEventException;
use Drupal\package_manager\Exception\StageException; use Drupal\package_manager\Exception\StageException;
use Drupal\package_manager\Exception\StageOwnershipException; use Drupal\package_manager\Exception\StageOwnershipException;
use PhpTuf\ComposerStager\Domain\Core\Beginner\BeginnerInterface; use PhpTuf\ComposerStager\API\Core\BeginnerInterface;
use PhpTuf\ComposerStager\Domain\Core\Committer\CommitterInterface; use PhpTuf\ComposerStager\API\Core\CommitterInterface;
use PhpTuf\ComposerStager\Domain\Core\Stager\StagerInterface; use PhpTuf\ComposerStager\API\Core\StagerInterface;
use PhpTuf\ComposerStager\Domain\Exception\InvalidArgumentException; use PhpTuf\ComposerStager\API\Exception\InvalidArgumentException;
use PhpTuf\ComposerStager\Domain\Exception\PreconditionException; use PhpTuf\ComposerStager\API\Exception\PreconditionException;
use PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface; use PhpTuf\ComposerStager\API\Path\Factory\PathFactoryInterface;
use PhpTuf\ComposerStager\Infrastructure\Value\PathList\PathList; use PhpTuf\ComposerStager\API\Path\Value\PathListInterface;
use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait; use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger; use Psr\Log\NullLogger;
...@@ -161,26 +159,37 @@ abstract class StageBase implements LoggerAwareInterface { ...@@ -161,26 +159,37 @@ abstract class StageBase implements LoggerAwareInterface {
*/ */
protected SharedTempStore $tempStore; 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. * Constructs a new Stage object.
* *
* @param \Drupal\package_manager\PathLocator $pathLocator * @param \Drupal\package_manager\PathLocator $pathLocator
* The path locator service. * The path locator service.
* @param \PhpTuf\ComposerStager\Domain\Core\Beginner\BeginnerInterface $beginner * @param \PhpTuf\ComposerStager\API\Core\BeginnerInterface $beginner
* The beginner service. * The beginner service.
* @param \PhpTuf\ComposerStager\Domain\Core\Stager\StagerInterface $stager * @param \PhpTuf\ComposerStager\API\Core\StagerInterface $stager
* The stager service. * The stager service.
* @param \PhpTuf\ComposerStager\Domain\Core\Committer\CommitterInterface $committer * @param \PhpTuf\ComposerStager\API\Core\CommitterInterface $committer
* The committer service. * The committer service.
* @param \Drupal\Core\File\FileSystemInterface $fileSystem * @param \Drupal\Core\Queue\QueueFactory $queueFactory
* The file system service. * The queue factory.
* @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $eventDispatcher * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $eventDispatcher
* The event dispatcher service. * The event dispatcher service.
* @param \Drupal\Core\TempStore\SharedTempStoreFactory $tempStoreFactory * @param \Drupal\Core\TempStore\SharedTempStoreFactory $tempStoreFactory
* The shared tempstore factory. * The shared tempstore factory.
* @param \Drupal\Component\Datetime\TimeInterface $time * @param \Drupal\Component\Datetime\TimeInterface $time
* The time service. * The time service.
* @param \PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface $pathFactory * @param \PhpTuf\ComposerStager\API\Path\Factory\PathFactoryInterface $pathFactory
* The path factory service. * The path factory service.
* @param \Drupal\package_manager\FailureMarker $failureMarker * @param \Drupal\package_manager\FailureMarker $failureMarker
* The failure marker service. * The failure marker service.
...@@ -190,7 +199,7 @@ abstract class StageBase implements LoggerAwareInterface { ...@@ -190,7 +199,7 @@ abstract class StageBase implements LoggerAwareInterface {
protected readonly BeginnerInterface $beginner, protected readonly BeginnerInterface $beginner,
protected readonly StagerInterface $stager, protected readonly StagerInterface $stager,
protected readonly CommitterInterface $committer, protected readonly CommitterInterface $committer,
protected readonly FileSystemInterface $fileSystem, protected readonly QueueFactory $queueFactory,
protected EventDispatcherInterface $eventDispatcher, protected EventDispatcherInterface $eventDispatcher,
protected readonly SharedTempStoreFactory $tempStoreFactory, protected readonly SharedTempStoreFactory $tempStoreFactory,
protected readonly TimeInterface $time, protected readonly TimeInterface $time,
...@@ -201,6 +210,31 @@ abstract class StageBase implements LoggerAwareInterface { ...@@ -201,6 +210,31 @@ abstract class StageBase implements LoggerAwareInterface {
$this->setLogger(new NullLogger()); $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. * Determines if the stage directory can be created.
* *
...@@ -223,7 +257,7 @@ abstract class StageBase implements LoggerAwareInterface { ...@@ -223,7 +257,7 @@ abstract class StageBase implements LoggerAwareInterface {
* @return mixed * @return mixed
* The metadata value, or NULL if it is not set. * The metadata value, or NULL if it is not set.
*/ */
protected function getMetadata(string $key) { public function getMetadata(string $key) {
$this->checkOwnership(); $this->checkOwnership();
$metadata = $this->tempStore->get(static::TEMPSTORE_METADATA_KEY) ?: []; $metadata = $this->tempStore->get(static::TEMPSTORE_METADATA_KEY) ?: [];
...@@ -237,11 +271,13 @@ abstract class StageBase implements LoggerAwareInterface { ...@@ -237,11 +271,13 @@ abstract class StageBase implements LoggerAwareInterface {
* claimed by its owner, or created during the current request. * claimed by its owner, or created during the current request.
* *
* @param string $key * @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 * @param mixed $data
* The metadata to store. * The metadata to store.
*/ */
protected function setMetadata(string $key, $data): void { public function setMetadata(string $key, $data): void {
$this->checkOwnership(); $this->checkOwnership();
$metadata = $this->tempStore->get(static::TEMPSTORE_METADATA_KEY); $metadata = $this->tempStore->get(static::TEMPSTORE_METADATA_KEY);
...@@ -252,7 +288,7 @@ abstract class StageBase implements LoggerAwareInterface { ...@@ -252,7 +288,7 @@ abstract class StageBase implements LoggerAwareInterface {
/** /**
* Collects paths that Composer Stager should exclude. * Collects paths that Composer Stager should exclude.
* *
* @return string[] * @return \PhpTuf\ComposerStager\API\Path\Value\PathListInterface
* A list of paths that Composer Stager should exclude when creating the * A list of paths that Composer Stager should exclude when creating the
* stage directory and applying staged changes to the active directory. * stage directory and applying staged changes to the active directory.
* *
...@@ -262,15 +298,14 @@ abstract class StageBase implements LoggerAwareInterface { ...@@ -262,15 +298,14 @@ abstract class StageBase implements LoggerAwareInterface {
* @see ::create() * @see ::create()
* @see ::apply() * @see ::apply()
*/ */
protected function getPathsToExclude(): array { protected function getPathsToExclude(): PathListInterface {
$event = new CollectPathsToExcludeEvent($this, $this->pathLocator, $this->pathFactory); $event = new CollectPathsToExcludeEvent($this, $this->pathLocator, $this->pathFactory);
try { try {
$this->eventDispatcher->dispatch($event); return $this->eventDispatcher->dispatch($event);
} }
catch (\Throwable $e) { catch (\Throwable $e) {
$this->rethrowAsStageException($e); $this->rethrowAsStageException($e);
} }
return $event->getAll();
} }
/** /**
...@@ -309,26 +344,40 @@ abstract class StageBase implements LoggerAwareInterface { ...@@ -309,26 +344,40 @@ abstract class StageBase implements LoggerAwareInterface {
// to create a stage directory at around the same time. If an error occurs // 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. // while the event is being processed, the stage is marked as available.
// @see ::dispatch() // @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 // 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 // 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, // 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 // which would result in the lock having the wrong owner and the stage not
// being claimable by whoever is actually creating it. // being claimable by whoever is actually creating it.
$this->tempStore = $this->tempStoreFactory->get('package_manager_stage'); $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); $this->claim($id);
$active_dir = $this->pathFactory->create($this->pathLocator->getProjectRoot()); $active_dir = $this->pathFactory->create($this->pathLocator->getProjectRoot());
$stage_dir = $this->pathFactory->create($this->getStageDirectory()); $stage_dir = $this->pathFactory->create($this->getStageDirectory());
$event = new PreCreateEvent($this, $this->getPathsToExclude()); $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 // If an error occurs and we won't be able to create the stage, mark it as
// available. // available.
$this->dispatch($event, [$this, 'markAsAvailable']); $this->dispatch($event, [$this, 'markAsAvailable']);
try { 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) { catch (\Throwable $error) {
$this->destroy(); $this->destroy();
...@@ -407,7 +456,7 @@ abstract class StageBase implements LoggerAwareInterface { ...@@ -407,7 +456,7 @@ abstract class StageBase implements LoggerAwareInterface {
// If constraints were changed, update those packages. // If constraints were changed, update those packages.
if ($runtime || $dev) { 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); $do_stage($command);
} }
$this->dispatch(new PostRequireEvent($this, $runtime, $dev)); $this->dispatch(new PostRequireEvent($this, $runtime, $dev));
...@@ -440,27 +489,24 @@ abstract class StageBase implements LoggerAwareInterface { ...@@ -440,27 +489,24 @@ abstract class StageBase implements LoggerAwareInterface {
$active_dir = $this->pathFactory->create($this->pathLocator->getProjectRoot()); $active_dir = $this->pathFactory->create($this->pathLocator->getProjectRoot());
$stage_dir = $this->pathFactory->create($this->getStageDirectory()); $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() // 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 // doesn't think we're in the middle of applying the staged changes to the
// active directory. // active directory.
$event = new PreApplyEvent($this, $this->getPathsToExclude());
$this->tempStore->set(self::TEMPSTORE_APPLY_TIME_KEY, $this->time->getRequestTime()); $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. // Create a marker file so that we can tell later on if the commit failed.
$this->failureMarker->write($this, $this->getFailureMarkerMessage()); $this->failureMarker->write($this, $this->getFailureMarkerMessage());
// Exclude the failure file from the commit operation.
$paths_to_exclude = new PathList($event->getExcludedPaths());
$paths_to_exclude->add([
str_replace($this->pathLocator->getProjectRoot() . DIRECTORY_SEPARATOR, '', $this->failureMarker->getPath()),
]);
try { try {
$this->committer->commit($stage_dir, $active_dir, $paths_to_exclude, NULL, $timeout); $this->committer->commit($stage_dir, $active_dir, $excluded_paths, NULL, $timeout);
} }
catch (InvalidArgumentException | PreconditionException $e) { catch (InvalidArgumentException | PreconditionException $e) {
// The commit operation has not started yet, so we can clear the failure // 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->failureMarker->clear();
$this->rethrowAsStageException($e); $this->rethrowAsStageException($e);
} }
...@@ -469,7 +515,7 @@ abstract class StageBase implements LoggerAwareInterface { ...@@ -469,7 +515,7 @@ abstract class StageBase implements LoggerAwareInterface {
// is in an indeterminate state. Release the flag which says we're still // is in an indeterminate state. Release the flag which says we're still
// applying, because in this situation, the site owner should probably // applying, because in this situation, the site owner should probably
// restore everything from a backup. // restore everything from a backup.
$this->setNotApplying()(); $this->setNotApplying();
// Update the marker file with the information from the throwable. // Update the marker file with the information from the throwable.
$this->failureMarker->write($this, $this->getFailureMarkerMessage(), $throwable); $this->failureMarker->write($this, $this->getFailureMarkerMessage(), $throwable);
throw new ApplyFailedException($this, $this->failureMarker->getMessage(), $throwable->getCode(), $throwable); throw new ApplyFailedException($this, $this->failureMarker->getMessage(), $throwable->getCode(), $throwable);
...@@ -480,15 +526,9 @@ abstract class StageBase implements LoggerAwareInterface { ...@@ -480,15 +526,9 @@ abstract class StageBase implements LoggerAwareInterface {
/** /**
* Returns a closure that marks this stage as no longer being applied. * 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 { private function setNotApplying(): void {
return function (): void { $this->tempStore->delete(self::TEMPSTORE_APPLY_TIME_KEY);
$this->tempStore->delete(self::TEMPSTORE_APPLY_TIME_KEY);
};
} }
/** /**
...@@ -513,7 +553,7 @@ abstract class StageBase implements LoggerAwareInterface { ...@@ -513,7 +553,7 @@ abstract class StageBase implements LoggerAwareInterface {
// unlikely to call newly added code during the current request. // unlikely to call newly added code during the current request.
$this->eventDispatcher = \Drupal::service('event_dispatcher'); $this->eventDispatcher = \Drupal::service('event_dispatcher');
$release_apply = $this->setNotApplying(); $release_apply = $this->setNotApplying(...);
$this->dispatch(new PostApplyEvent($this), $release_apply); $this->dispatch(new PostApplyEvent($this), $release_apply);
$release_apply(); $release_apply();
} }
...@@ -539,26 +579,16 @@ abstract class StageBase implements LoggerAwareInterface { ...@@ -539,26 +579,16 @@ abstract class StageBase implements LoggerAwareInterface {
throw new StageException($this, 'Cannot destroy the stage directory while it is being applied to the active directory.'); throw new StageException($this, 'Cannot destroy the stage directory while it is being applied to the active directory.');
} }
$this->dispatch(new PreDestroyEvent($this)); // If the stage directory exists, queue it to be automatically cleaned up
$staging_root = $this->getStagingRoot(); // later by a queue (which may or may not happen during cron).
// If the stage root directory exists, delete it and everything in it. // @see \Drupal\package_manager\Plugin\QueueWorker\Cleaner
if (file_exists($staging_root)) { if ($this->stageDirectoryExists()) {
try { $this->queueFactory->get('package_manager_cleanup')
$this->fileSystem->deleteRecursive($staging_root, function (string $path): void { ->createItem($this->getStageDirectory());
$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.
}
} }
$this->storeDestroyInfo($force, $message); $this->storeDestroyInfo($force, $message);
$this->markAsAvailable(); $this->markAsAvailable();
$this->dispatch(new PostDestroyEvent($this));
} }
/** /**
...@@ -598,6 +628,8 @@ abstract class StageBase implements LoggerAwareInterface { ...@@ -598,6 +628,8 @@ abstract class StageBase implements LoggerAwareInterface {
} }
if (isset($error)) { if (isset($error)) {
// Ensure the error is logged for post-mortem diagnostics.
Error::logException($this->logger, $error);
if ($on_error) { if ($on_error) {
$on_error(); $on_error();
} }
...@@ -651,7 +683,7 @@ abstract class StageBase implements LoggerAwareInterface { ...@@ -651,7 +683,7 @@ abstract class StageBase implements LoggerAwareInterface {
)->render()); )->render());
} }
if ($stored_lock === [$unique_id, static::class]) { if ($stored_lock === [$unique_id, static::class, $this->getType()]) {
$this->lock = $stored_lock; $this->lock = $stored_lock;
return $this; return $this;
} }
......
...@@ -6,7 +6,7 @@ namespace Drupal\package_manager; ...@@ -6,7 +6,7 @@ namespace Drupal\package_manager;
use Drupal\package_manager\Event\CollectPathsToExcludeEvent; use Drupal\package_manager\Event\CollectPathsToExcludeEvent;
use Drupal\package_manager\Event\StatusCheckEvent; use Drupal\package_manager\Event\StatusCheckEvent;
use PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface; use PhpTuf\ComposerStager\API\Path\Factory\PathFactoryInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/** /**
...@@ -28,7 +28,7 @@ trait StatusCheckTrait { ...@@ -28,7 +28,7 @@ trait StatusCheckTrait {
* (optional) The event dispatcher service. * (optional) The event dispatcher service.
* @param \Drupal\package_manager\PathLocator $path_locator * @param \Drupal\package_manager\PathLocator $path_locator
* (optional) The path locator service. * (optional) The path locator service.
* @param \PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface $path_factory * @param \PhpTuf\ComposerStager\API\Path\Factory\PathFactoryInterface $path_factory
* (optional) The path factory service. * (optional) The path factory service.
* *
* @return \Drupal\package_manager\ValidationResult[] * @return \Drupal\package_manager\ValidationResult[]
...@@ -42,22 +42,12 @@ trait StatusCheckTrait { ...@@ -42,22 +42,12 @@ trait StatusCheckTrait {
try { try {
$paths_to_exclude_event = new CollectPathsToExcludeEvent($stage, $path_locator, $path_factory); $paths_to_exclude_event = new CollectPathsToExcludeEvent($stage, $path_locator, $path_factory);
$event_dispatcher->dispatch($paths_to_exclude_event); $event_dispatcher->dispatch($paths_to_exclude_event);
$event = new StatusCheckEvent($stage, $paths_to_exclude_event->getAll());
} }
catch (\Throwable $throwable) { catch (\Throwable $throwable) {
// We can dispatch the status check event without the paths to exclude, $paths_to_exclude_event = $throwable;
// but it must be set explicitly to NULL, to allow those status checks to
// run that do not need the paths to exclude.
$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 paths to exclude.'));
} }
$event = new StatusCheckEvent($stage, $paths_to_exclude_event);
$event_dispatcher->dispatch($event); return $event_dispatcher->dispatch($event)->getResults();
return $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,6 +7,7 @@ namespace Drupal\package_manager; ...@@ -7,6 +7,7 @@ namespace Drupal\package_manager;
use Drupal\Component\Assertion\Inspector; use Drupal\Component\Assertion\Inspector;
use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\system\SystemManager; use Drupal\system\SystemManager;
use PhpTuf\ComposerStager\API\Exception\ExceptionInterface;
/** /**
* A value object to contain the results of a validation. * A value object to contain the results of a validation.
...@@ -72,7 +73,10 @@ final class ValidationResult { ...@@ -72,7 +73,10 @@ final class ValidationResult {
* @return static * @return static
*/ */
public static function createErrorFromThrowable(\Throwable $throwable, ?TranslatableMarkup $summary = NULL): static { public static function createErrorFromThrowable(\Throwable $throwable, ?TranslatableMarkup $summary = NULL): static {
return new static(SystemManager::REQUIREMENT_ERROR, [$throwable->getMessage()], $summary, FALSE); // 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);
} }
/** /**
......
...@@ -4,6 +4,7 @@ declare(strict_types = 1); ...@@ -4,6 +4,7 @@ declare(strict_types = 1);
namespace Drupal\package_manager\Validator; namespace Drupal\package_manager\Validator;
use Composer\Semver\Semver;
use Drupal\Component\Serialization\Json; use Drupal\Component\Serialization\Json;
use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\StringTranslation\StringTranslationTrait;
...@@ -155,15 +156,26 @@ final class ComposerPatchesValidator implements EventSubscriberInterface { ...@@ -155,15 +156,26 @@ final class ComposerPatchesValidator implements EventSubscriberInterface {
*/ */
private function computePatcherStatus(string $working_dir): array { private function computePatcherStatus(string $working_dir): array {
$list = $this->composerInspector->getInstalledPackagesList($working_dir); $list = $this->composerInspector->getInstalledPackagesList($working_dir);
$is_installed = isset($list[static::PLUGIN_NAME]); $installed_version = $list[static::PLUGIN_NAME]?->version;
$info = $this->composerInspector->getRootPackageInfo($working_dir); $info = $this->composerInspector->getRootPackageInfo($working_dir);
$is_root_requirement = array_key_exists(static::PLUGIN_NAME, $info['requires'] ?? []) || array_key_exists(static::PLUGIN_NAME, $info['devRequires'] ?? []); $is_root_requirement = array_key_exists(static::PLUGIN_NAME, $info['requires'] ?? []) || array_key_exists(static::PLUGIN_NAME, $info['devRequires'] ?? []);
$extra = Json::decode($this->composerInspector->getConfig('extra', $working_dir)); // The 2.x version of the plugin always exits with an error if a patch can't
$exit_on_failure = $extra['composer-exit-on-patch-failure'] ?? FALSE; // be applied.
if ($installed_version && Semver::satisfies($installed_version, '^2')) {
$exit_on_failure = TRUE;
}
else {
$extra = Json::decode($this->composerInspector->getConfig('extra', $working_dir));
$exit_on_failure = $extra['composer-exit-on-patch-failure'] ?? FALSE;
}
return [$is_installed, $is_root_requirement, $exit_on_failure]; return [
is_string($installed_version),
$is_root_requirement,
$exit_on_failure,
];
} }
/** /**
......