Skip to content

Commit fcc1f1e

Browse files
authored
Merge pull request #5 from github/filter-labels
refactor(label-filters): improve the label filtering
2 parents cdacfec + ec6ec44 commit fcc1f1e

File tree

5 files changed

+155
-105
lines changed

5 files changed

+155
-105
lines changed

internal/cmd/inputs.go

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

9+
var errLabelsConflict = 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 {
14+
return err
4215
}
4316

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

4922
// Warn if no filtering options are provided at all
5023
if branchPrefix == "" && branchSuffix == "" && branchRegex == "" &&
51-
ignoreLabel == "" && len(ignoreLabels) == 0 && selectLabel == "" && len(selectLabels) == 0 &&
24+
len(ignoreLabels) == 0 && len(selectLabels) == 0 &&
5225
!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.")
26+
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.")
27+
}
28+
29+
return nil
30+
}
31+
32+
func ValidateLabels(selectLabels []string, ignoreLabels []string) error {
33+
// Check for conflicts between ignoreLabels and selectLabels
34+
for _, ignoreL := range ignoreLabels {
35+
if i := slices.Index(selectLabels, ignoreL); i != -1 {
36+
return fmt.Errorf("%w: %q %q", errLabelsConflict, selectLabels[i], ignoreLabels)
37+
}
5438
}
5539

5640
return nil

internal/cmd/inputs_test.go

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,53 @@
11
package cmd
22

33
import (
4-
"bytes"
5-
"log/slog"
4+
"errors"
65
"os"
7-
"strings"
86
"testing"
97
)
108

9+
func TestValidateLabels(t *testing.T) {
10+
t.Parallel()
11+
12+
tests := []struct {
13+
selectLabels []string
14+
ignoreLabels []string
15+
want error
16+
}{
17+
{
18+
want: nil,
19+
},
20+
{
21+
selectLabels: []string{"a"},
22+
ignoreLabels: []string{"b"},
23+
want: nil,
24+
},
25+
{
26+
selectLabels: []string{"a"},
27+
ignoreLabels: []string{"a", "b"},
28+
want: errLabelsConflict,
29+
},
30+
{
31+
selectLabels: []string{"a", "b"},
32+
ignoreLabels: []string{"b"},
33+
want: errLabelsConflict,
34+
},
35+
}
36+
37+
for _, test := range tests {
38+
t.Run("", func(t *testing.T) {
39+
t.Parallel()
40+
41+
got := ValidateLabels(test.selectLabels, test.ignoreLabels)
42+
43+
if !errors.Is(got, test.want) {
44+
t.Fatalf("want %s, but go %s", test.want, got)
45+
}
46+
})
47+
}
48+
}
49+
50+
/*
1151
// mockLogger creates a test logger that writes to a bytes.Buffer
1252
func setupMockLogger() (*bytes.Buffer, func()) {
1353
var buf bytes.Buffer
@@ -279,6 +319,7 @@ func TestValidateInputs_ValidInputs(t *testing.T) {
279319
})
280320
}
281321
}
322+
*/
282323

283324
// TestMain sets up and tears down the testing environment
284325
func TestMain(m *testing.M) {

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)