Skip to content

Commit af8d726

Browse files
committed
refactor(label-filters): improve the label filtering
This removes --label and --ignore-label in favor of only using --labels and --ignore-labels. Having a singular and plural version caused confusion and created additional code checks that weren't needed. It also create some new tests to check for validity of the new logic. Note that it changes the prLabels from undefined []struct{} to []string. Eventually it might go back to []PR once it's properly defined. But until then we can use strings only.
1 parent cdacfec commit af8d726

File tree

4 files changed

+110
-102
lines changed

4 files changed

+110
-102
lines changed

internal/cmd/inputs.go

Lines changed: 17 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,42 +3,14 @@ package cmd
33
import (
44
"errors"
55
"fmt"
6+
"slices"
67
)
78

9+
var errIgnoreLabelsConflict = errors.New("--ignore-labels contains a value which conflicts with --labels")
10+
811
// validateInputs checks if the provided inputs are valid
912
func ValidateInputs(args []string) error {
10-
// Check that ignore-label and select-label are not the same
11-
if ignoreLabel != "" && selectLabel != "" && ignoreLabel == selectLabel {
12-
return errors.New("--ignore-label(s) and --label(s) cannot have the same value")
13-
}
14-
15-
// Check for conflicts between ignoreLabels and selectLabel
16-
if selectLabel != "" && len(ignoreLabels) > 0 {
17-
for _, ignoreL := range ignoreLabels {
18-
if ignoreL == selectLabel {
19-
return fmt.Errorf("--ignore-labels contains %q which conflicts with --label %q", ignoreL, selectLabel)
20-
}
21-
}
22-
}
23-
24-
// Check for conflicts between ignoreLabel and selectLabels
25-
if ignoreLabel != "" && len(selectLabels) > 0 {
26-
for _, selectL := range selectLabels {
27-
if selectL == ignoreLabel {
28-
return fmt.Errorf("--label(s) contains %q which conflicts with --ignore-label %q", selectL, ignoreLabel)
29-
}
30-
}
31-
}
32-
33-
// Check for conflicts between ignoreLabels and selectLabels
34-
if len(ignoreLabels) > 0 && len(selectLabels) > 0 {
35-
for _, ignoreL := range ignoreLabels {
36-
for _, selectL := range selectLabels {
37-
if ignoreL == selectL {
38-
return fmt.Errorf("--ignore-labels contains %q which conflicts with --labels containing the same value", ignoreL)
39-
}
40-
}
41-
}
13+
if err := ValidateLabels(selectLabels, ignoreLabels); err != nil {
4214
}
4315

4416
// If no args and no file, we can't proceed
@@ -48,9 +20,20 @@ func ValidateInputs(args []string) error {
4820

4921
// Warn if no filtering options are provided at all
5022
if branchPrefix == "" && branchSuffix == "" && branchRegex == "" &&
51-
ignoreLabel == "" && len(ignoreLabels) == 0 && selectLabel == "" && len(selectLabels) == 0 &&
23+
len(ignoreLabels) == 0 && len(selectLabels) == 0 &&
5224
!requireCI && !mustBeApproved {
53-
Logger.Warn("No filtering options specified. This will attempt to combine ALL open pull requests. Use --label, --labels, --ignore-label, --ignore-labels, --branch-prefix, --branch-suffix, --branch-regex, --dependabot, etc to filter.")
25+
Logger.Warn("No filtering options specified. This will attempt to combine ALL open pull requests. Use --labels, --ignore-labels, --branch-prefix, --branch-suffix, --branch-regex, --dependabot, etc to filter.")
26+
}
27+
28+
return nil
29+
}
30+
31+
func ValidateLabels(selectLabels []string, ignoreLabels []string) error {
32+
// Check for conflicts between ignoreLabels and selectLabels
33+
for _, ignoreL := range ignoreLabels {
34+
if i := slices.Index(selectLabels, ignoreL); i != -1 {
35+
return fmt.Errorf("%w: %q %q", errIgnoreLabelsConflict, ignoreLabels[i], selectLabels)
36+
}
5437
}
5538

5639
return nil

internal/cmd/match_criteria.go

Lines changed: 12 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,22 @@ import (
44
"context"
55
"fmt"
66
"regexp"
7+
"slices"
78
"strings"
89

910
"github.com/cli/go-gh/v2/pkg/api"
1011
graphql "github.com/cli/shurcooL-graphql"
1112
)
1213

1314
// checks if a PR matches all filtering criteria
14-
func PrMatchesCriteria(branch string, prLabels []struct{ Name string }) bool {
15+
func PrMatchesCriteria(branch string, prLabels []string) bool {
1516
// Check branch criteria if any are specified
1617
if !branchMatchesCriteria(branch) {
1718
return false
1819
}
1920

2021
// Check label criteria if any are specified
21-
if !labelsMatchCriteria(prLabels) {
22+
if !labelsMatch(prLabels, ignoreLabels, selectLabels) {
2223
return false
2324
}
2425

@@ -58,64 +59,25 @@ func branchMatchesCriteria(branch string) bool {
5859
return true
5960
}
6061

61-
// labelsMatchCriteria checks if PR labels match the label filtering criteria
62-
func labelsMatchCriteria(prLabels []struct{ Name string }) bool {
63-
// If no label filters are specified, all PRs pass this check
64-
if ignoreLabel == "" && len(ignoreLabels) == 0 &&
65-
selectLabel == "" && len(selectLabels) == 0 {
66-
return true
67-
}
68-
69-
// Check for ignore label (singular)
70-
if ignoreLabel != "" {
71-
for _, label := range prLabels {
72-
if label.Name == ignoreLabel {
73-
return false
74-
}
75-
}
76-
}
77-
78-
// Check for ignore labels (plural)
79-
if len(ignoreLabels) > 0 {
80-
for _, ignoreL := range ignoreLabels {
81-
for _, prLabel := range prLabels {
82-
if prLabel.Name == ignoreL {
83-
return false
84-
}
85-
}
62+
func labelsMatch(prLabels, ignoreLabels, selectLabels []string) bool {
63+
for _, l := range ignoreLabels {
64+
if i := slices.Index(prLabels, l); i != -1 {
65+
return false
8666
}
8767
}
8868

89-
// Check for select label (singular)
90-
if selectLabel != "" {
69+
for _, l := range selectLabels {
9170
found := false
92-
for _, label := range prLabels {
93-
if label.Name == selectLabel {
94-
found = true
95-
break
96-
}
71+
if i := slices.Index(prLabels, l); i != -1 {
72+
found = true
73+
break
9774
}
75+
9876
if !found {
9977
return false
10078
}
10179
}
10280

103-
// Check for select labels (plural)
104-
if len(selectLabels) > 0 {
105-
for _, requiredLabel := range selectLabels {
106-
found := false
107-
for _, prLabel := range prLabels {
108-
if prLabel.Name == requiredLabel {
109-
found = true
110-
break
111-
}
112-
}
113-
if !found {
114-
return false
115-
}
116-
}
117-
}
118-
11981
return true
12082
}
12183

internal/cmd/match_criteria_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package cmd
2+
3+
import "testing"
4+
5+
func TestLabelsMatch(t *testing.T) {
6+
t.Parallel()
7+
8+
tests := []struct {
9+
prLabels []string
10+
ignoreLabels []string
11+
selectLabels []string
12+
want bool
13+
}{
14+
{
15+
want: true,
16+
},
17+
18+
{
19+
prLabels: []string{"a", "b"},
20+
ignoreLabels: []string{"b"},
21+
want: false,
22+
},
23+
{
24+
prLabels: []string{"a", "b"},
25+
ignoreLabels: []string{"b", "c"},
26+
want: false,
27+
},
28+
29+
{
30+
prLabels: []string{"a"},
31+
ignoreLabels: []string{"b"},
32+
selectLabels: []string{"c"},
33+
want: false,
34+
},
35+
{
36+
prLabels: []string{"a", "c"},
37+
ignoreLabels: []string{"b"},
38+
selectLabels: []string{"c"},
39+
want: true,
40+
},
41+
{
42+
prLabels: []string{"a"},
43+
ignoreLabels: []string{"b"},
44+
selectLabels: []string{"a", "c"},
45+
want: true,
46+
},
47+
}
48+
49+
for _, test := range tests {
50+
t.Run("", func(t *testing.T) {
51+
t.Parallel()
52+
53+
got := labelsMatch(test.prLabels, test.ignoreLabels, test.selectLabels)
54+
if got != test.want {
55+
t.Errorf("want %v, got %v", test.want, got)
56+
}
57+
})
58+
}
59+
}

internal/cmd/root.go

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,20 @@ import (
1414
)
1515

1616
var (
17-
branchPrefix string
18-
branchSuffix string
19-
branchRegex string
20-
selectLabel string
21-
selectLabels []string
22-
addLabels []string
23-
addAssignees []string
17+
branchPrefix string
18+
branchSuffix string
19+
branchRegex string
20+
21+
selectLabels []string
22+
ignoreLabels []string
23+
24+
addLabels []string
25+
addAssignees []string
26+
2427
requireCI bool
2528
mustBeApproved bool
2629
autoclose bool
2730
updateBranch bool
28-
ignoreLabel string
29-
ignoreLabels []string
3031
reposFile string
3132
minimum int
3233
defaultOwner string
@@ -46,7 +47,7 @@ func NewRootCmd() *cobra.Command {
4647
# Note: You should use some form of filtering to avoid combining all open PRs in a repository.
4748
# For example, you can filter by branch name, labels, or other criteria.
4849
# Forms of filtering include:
49-
# --label, --labels, --ignore-label, --ignore-labels, --branch-prefix, --branch-suffix, --branch-regex, --dependabot, etc.
50+
# --labels, --ignore-labels, --branch-prefix, --branch-suffix, --branch-regex, --dependabot, etc.
5051
5152
# Basic usage with a single repository to combine all pull requests into one
5253
gh combine owner/repo
@@ -75,11 +76,11 @@ func NewRootCmd() *cobra.Command {
7576
gh combine owner/repo --branch-regex "dependabot/.*"
7677
7778
# Filter PRs by labels
78-
gh combine owner/repo --label dependencies # PRs must have this single label
79+
gh combine owner/repo --labels dependencies # PRs must have this single label
7980
gh combine owner/repo --labels security,dependencies # PRs must have ALL these labels
8081
8182
# Exclude PRs by labels
82-
gh combine owner/repo --ignore-label wip # Ignore PRs with this label
83+
gh combine owner/repo --ignore-labels wip # Ignore PRs with this label
8384
gh combine owner/repo --ignore-labels wip,draft # Ignore PRs with ANY of these labels
8485
8586
# Set requirements for PRs to be combined
@@ -105,12 +106,7 @@ func NewRootCmd() *cobra.Command {
105106
rootCmd.Flags().StringVar(&branchSuffix, "branch-suffix", "", "Branch suffix to filter PRs")
106107
rootCmd.Flags().StringVar(&branchRegex, "branch-regex", "", "Regex pattern to filter PRs by branch name")
107108

108-
// Label selection flags - singular and plural forms
109-
rootCmd.Flags().StringVar(&selectLabel, "label", "", "Only include PRs with this specific label")
110109
rootCmd.Flags().StringSliceVar(&selectLabels, "labels", nil, "Only include PRs with ALL these labels (comma-separated)")
111-
112-
// Label ignoring flags - singular and plural forms
113-
rootCmd.Flags().StringVar(&ignoreLabel, "ignore-label", "", "Ignore PRs with this specific label")
114110
rootCmd.Flags().StringSliceVar(&ignoreLabels, "ignore-labels", nil, "Ignore PRs with ANY of these labels (comma-separated)")
115111

116112
// Labels to add to the combined PR
@@ -278,8 +274,16 @@ func processRepository(ctx context.Context, client *api.RESTClient, graphQlClien
278274
for _, pull := range pulls {
279275
branch := pull.Head.Ref
280276

277+
// Temporary workaround because passing structures is useless in this
278+
// context.
279+
// Eventually the []Labels should have better support.
280+
labels := []string{}
281+
for _, label := range pull.Labels {
282+
labels = append(labels, label.Name)
283+
}
284+
281285
// Check if PR matches all filtering criteria
282-
if !PrMatchesCriteria(branch, pull.Labels) {
286+
if !PrMatchesCriteria(branch, labels) {
283287
continue
284288
}
285289

0 commit comments

Comments
 (0)