@@ -81,25 +81,16 @@ func newFeaturesEnableCmd() *cobra.Command {
8181 return fmt .Errorf ("unknown feature gate: %q" , gateName )
8282 }
8383
84+ if len (excludes ) > 0 && len (gitlabGroups ) == 0 && len (githubOrgs ) == 0 {
85+ return fmt .Errorf ("--exclude requires --gitlab-group or --github-org" )
86+ }
87+
8488 fs := state.FeatureState {Enabled : true }
8589
8690 if len (projects ) > 0 || len (gitlabGroups ) > 0 || len (githubOrgs ) > 0 {
87- override := & state.ScopeOverride {Projects : projects }
88- for _ , g := range gitlabGroups {
89- host , group , ok := strings .Cut (g , "/" )
90- if ! ok {
91- return fmt .Errorf ("invalid --gitlab-group format %q, expected host/group" , g )
92- }
93- entry := state.ScopeOverrideGroup {Host : host , Group : group , ExcludeProjects : excludes }
94- override .GitlabGroups = append (override .GitlabGroups , entry )
95- }
96- for _ , o := range githubOrgs {
97- host , org , ok := strings .Cut (o , "/" )
98- if ! ok {
99- return fmt .Errorf ("invalid --github-org format %q, expected host/org" , o )
100- }
101- entry := state.ScopeOverrideOrg {Host : host , Org : org , ExcludeProjects : excludes }
102- override .GithubOrgs = append (override .GithubOrgs , entry )
91+ override , err := buildOverrideFromFlags (projects , gitlabGroups , githubOrgs , excludes )
92+ if err != nil {
93+ return err
10394 }
10495 fs .OverrideScope = override
10596 }
@@ -122,27 +113,128 @@ func newFeaturesEnableCmd() *cobra.Command {
122113}
123114
124115func newFeaturesDisableCmd () * cobra.Command {
125- return & cobra.Command {
116+ var (
117+ projects []string
118+ gitlabGroups []string
119+ githubOrgs []string
120+ excludes []string
121+ )
122+
123+ cmd := & cobra.Command {
126124 Use : "disable <gate>" ,
127- Short : "Disable a feature gate" ,
125+ Short : "Disable a feature gate, or (with scope flags) disable it only for specific projects " ,
128126 Args : cobra .ExactArgs (1 ),
129127 RunE : func (cmd * cobra.Command , args []string ) error {
130128 gateName := args [0 ]
131129
132- if _ , ok := globalSt .Features [gateName ]; ! ok {
130+ if len (excludes ) > 0 && len (gitlabGroups ) == 0 && len (githubOrgs ) == 0 {
131+ return fmt .Errorf ("--exclude requires --gitlab-group or --github-org" )
132+ }
133+
134+ fs , ok := globalSt .Features [gateName ]
135+ scoped := len (projects ) > 0 || len (gitlabGroups ) > 0 || len (githubOrgs ) > 0
136+
137+ // Unscoped disable only consults state, so it can also clean up a stale
138+ // entry for a gate that has since been removed from the config.
139+ if ! scoped {
140+ if ! ok {
141+ fmt .Printf ("Feature gate %q was not enabled\n " , gateName )
142+ return nil
143+ }
144+ delete (globalSt .Features , gateName )
145+ if err := state .Save (globalCwd , globalSt ); err != nil {
146+ return fmt .Errorf ("failed to save state: %w" , err )
147+ }
148+ fmt .Printf ("Feature gate %q disabled\n " , gateName )
149+ return nil
150+ }
151+
152+ // Scoped disable needs the gate's configured scope to reconstruct the override.
153+ gate , cfgOK := globalCfg .FeatureGates [gateName ]
154+ if ! cfgOK {
155+ return fmt .Errorf ("unknown feature gate: %q" , gateName )
156+ }
157+ if ! ok || ! fs .Enabled {
133158 fmt .Printf ("Feature gate %q was not enabled\n " , gateName )
134159 return nil
135160 }
136161
137- delete (globalSt .Features , gateName )
162+ // Remove the matching projects from the gate's current scope, leaving it
163+ // enabled for the rest.
164+ removal , err := buildOverrideFromFlags (projects , gitlabGroups , githubOrgs , excludes )
165+ if err != nil {
166+ return err
167+ }
168+
169+ scopeProjects := features .ProjectsInGateScope (globalCfg , gate )
170+ checked := features .OverrideToCheckedState (fs .OverrideScope , scopeProjects )
171+
172+ removed := 0
173+ for name , sp := range scopeProjects {
174+ if checked [name ] && features .ProjectMatchesOverrideScope (name , sp .Host , sp .Path , removal ) {
175+ checked [name ] = false
176+ removed ++
177+ }
178+ }
179+
180+ if removed == 0 {
181+ fmt .Printf ("Feature gate %q was not enabled for the given project(s)\n " , gateName )
182+ return nil
183+ }
184+
185+ anyLeft := false
186+ for _ , v := range checked {
187+ if v {
188+ anyLeft = true
189+ break
190+ }
191+ }
192+
193+ if anyLeft {
194+ fs .OverrideScope = features .CheckedStateToOverride (checked , scopeProjects )
195+ globalSt .Features [gateName ] = fs
196+ } else {
197+ delete (globalSt .Features , gateName )
198+ }
199+
138200 if err := state .Save (globalCwd , globalSt ); err != nil {
139201 return fmt .Errorf ("failed to save state: %w" , err )
140202 }
141203
142- fmt .Printf ("Feature gate %q disabled\n " , gateName )
204+ if anyLeft {
205+ fmt .Printf ("Feature gate %q disabled for the given project(s)\n " , gateName )
206+ } else {
207+ fmt .Printf ("Feature gate %q disabled\n " , gateName )
208+ }
143209 return nil
144210 },
145211 }
212+
213+ cmd .Flags ().StringArrayVar (& projects , "project" , nil , "disable only for specific project(s)" )
214+ cmd .Flags ().StringArrayVar (& gitlabGroups , "gitlab-group" , nil , "disable only for gitlab group (host/group)" )
215+ cmd .Flags ().StringArrayVar (& githubOrgs , "github-org" , nil , "disable only for github org (host/org)" )
216+ cmd .Flags ().StringArrayVar (& excludes , "exclude" , nil , "exclude project from all groups/orgs" )
217+ return cmd
218+ }
219+
220+ // buildOverrideFromFlags turns the CLI scope flags into a ScopeOverride.
221+ func buildOverrideFromFlags (projects , gitlabGroups , githubOrgs , excludes []string ) (* state.ScopeOverride , error ) {
222+ override := & state.ScopeOverride {Projects : projects }
223+ for _ , g := range gitlabGroups {
224+ host , group , ok := strings .Cut (g , "/" )
225+ if ! ok {
226+ return nil , fmt .Errorf ("invalid --gitlab-group format %q, expected host/group" , g )
227+ }
228+ override .GitlabGroups = append (override .GitlabGroups , state.ScopeOverrideGroup {Host : host , Group : group , ExcludeProjects : excludes })
229+ }
230+ for _ , o := range githubOrgs {
231+ host , org , ok := strings .Cut (o , "/" )
232+ if ! ok {
233+ return nil , fmt .Errorf ("invalid --github-org format %q, expected host/org" , o )
234+ }
235+ override .GithubOrgs = append (override .GithubOrgs , state.ScopeOverrideOrg {Host : host , Org : org , ExcludeProjects : excludes })
236+ }
237+ return override , nil
146238}
147239
148240func buildOverrideScopeDescription (o * state.ScopeOverride ) string {
0 commit comments