Skip to content

Commit 2e0da54

Browse files
committed
ReferenceFilter: simplify how filters are combined
Instead of the special `IncludeExcludeFilter`, treat filters just as sets, with the usual set operations (union, intersection, and inversion) and build them up that way. Add a `Combiner` that knows how to combine two `ReferenceFilter`s, and use it to implement "include" vs. "exclude". Build special behavior into the combiners if the left argument is `nil`, to get the old semantics that the "default" for an unmentioned reference depends on whether the first directive is "include" vs. "exclude".
1 parent 0b0adf1 commit 2e0da54

4 files changed

Lines changed: 144 additions & 98 deletions

File tree

git-sizer.go

Lines changed: 46 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -80,25 +80,28 @@ var ReleaseVersion string
8080
var BuildVersion string
8181

8282
type filterValue struct {
83-
// The filter to which values will be appended:
84-
filter *git.IncludeExcludeFilter
85-
86-
// The polarity of this option (i.e., does it cause the things
87-
// that it references to be included or excluded?):
88-
polarity git.Polarity
89-
90-
// If this is set, then it is used as the pattern. If not, then
91-
// the user should supply the pattern.
83+
// filter is the filter that will be modified if this option
84+
// is used.
85+
filter *git.ReferenceFilter
86+
87+
// combiner specifies how the filter generated by this option
88+
// is combined with the existing filter; i.e., does it cause
89+
// the matching references to be included or excluded?
90+
combiner git.Combiner
91+
92+
// pattern, if it is set, is the pattern (prefix or regexp) to
93+
// be matched. If it is not set, then the user must supply the
94+
// pattern.
9295
pattern string
9396

94-
// Should `pattern` be interpreted as a regexp (as opposed to a
95-
// prefix)?
97+
// regexp specifies whether `pattern` should be interpreted as
98+
// a regexp (as opposed to a prefix).
9699
regexp bool
97100
}
98101

99102
func (v *filterValue) Set(s string) error {
100103
var filter git.ReferenceFilter
101-
polarity := v.polarity
104+
combiner := v.combiner
102105

103106
var pattern string
104107
if v.pattern != "" {
@@ -112,7 +115,7 @@ func (v *filterValue) Set(s string) error {
112115
return err
113116
}
114117
if !b {
115-
polarity = polarity.Inverted()
118+
combiner = combiner.Inverted()
116119
}
117120
} else {
118121
// The user must supply the pattern.
@@ -129,12 +132,7 @@ func (v *filterValue) Set(s string) error {
129132
filter = git.PrefixFilter(pattern)
130133
}
131134

132-
switch polarity {
133-
case git.Include:
134-
v.filter.Include(filter)
135-
case git.Exclude:
136-
v.filter.Exclude(filter)
137-
}
135+
*v.filter = combiner.Combine(*v.filter, filter)
138136

139137
return nil
140138
}
@@ -158,7 +156,7 @@ func (v *filterValue) Type() string {
158156
}
159157

160158
type filterGroupValue struct {
161-
filter *git.IncludeExcludeFilter
159+
filter *git.ReferenceFilter
162160
repo *git.Repository
163161
}
164162

@@ -181,7 +179,9 @@ func (v *filterGroupValue) Set(name string) error {
181179
for _, entry := range config.Entries {
182180
switch entry.Key {
183181
case "include":
184-
v.filter.Include(git.PrefixFilter(entry.Value))
182+
*v.filter = git.Include.Combine(
183+
*v.filter, git.PrefixFilter(entry.Value),
184+
)
185185
case "includeregexp":
186186
filter, err := git.RegexpFilter(entry.Value)
187187
if err != nil {
@@ -190,9 +190,11 @@ func (v *filterGroupValue) Set(name string) error {
190190
name, entry.Key, err,
191191
)
192192
}
193-
v.filter.Include(filter)
193+
*v.filter = git.Include.Combine(*v.filter, filter)
194194
case "exclude":
195-
v.filter.Exclude(git.PrefixFilter(entry.Value))
195+
*v.filter = git.Exclude.Combine(
196+
*v.filter, git.PrefixFilter(entry.Value),
197+
)
196198
case "excluderegexp":
197199
filter, err := git.RegexpFilter(entry.Value)
198200
if err != nil {
@@ -201,7 +203,7 @@ func (v *filterGroupValue) Set(name string) error {
201203
name, entry.Key, err,
202204
)
203205
}
204-
v.filter.Exclude(filter)
206+
*v.filter = git.Exclude.Combine(*v.filter, filter)
205207
default:
206208
// Ignore unrecognized keys.
207209
}
@@ -237,7 +239,7 @@ func mainImplementation(args []string) error {
237239
var threshold sizes.Threshold = 1
238240
var progress bool
239241
var version bool
240-
var filter git.IncludeExcludeFilter
242+
var filter git.ReferenceFilter
241243
var showRefs bool
242244

243245
// Try to open the repository, but it's not an error yet if this
@@ -467,23 +469,16 @@ func mainImplementation(args []string) error {
467469
progress = v
468470
}
469471

470-
var refFilter git.ReferenceFilter = filter.Filter
472+
if filter == nil {
473+
filter = git.AllReferencesFilter
474+
}
471475

472476
if showRefs {
473-
oldRefFilter := refFilter
474477
fmt.Fprintf(os.Stderr, "References (included references marked with '+'):\n")
475-
refFilter = func(refname string) bool {
476-
b := oldRefFilter(refname)
477-
if b {
478-
fmt.Fprintf(os.Stderr, "+ %s\n", refname)
479-
} else {
480-
fmt.Fprintf(os.Stderr, " %s\n", refname)
481-
}
482-
return b
483-
}
478+
filter = showRefFilter{filter}
484479
}
485480

486-
historySize, err := sizes.ScanRepositoryUsingGraph(repo, refFilter, nameStyle, progress)
481+
historySize, err := sizes.ScanRepositoryUsingGraph(repo, filter, nameStyle, progress)
487482
if err != nil {
488483
return fmt.Errorf("error scanning repository: %s", err)
489484
}
@@ -509,3 +504,17 @@ func mainImplementation(args []string) error {
509504

510505
return nil
511506
}
507+
508+
type showRefFilter struct {
509+
f git.ReferenceFilter
510+
}
511+
512+
func (f showRefFilter) Filter(refname string) bool {
513+
b := f.f.Filter(refname)
514+
if b {
515+
fmt.Fprintf(os.Stderr, "+ %s\n", refname)
516+
} else {
517+
fmt.Fprintf(os.Stderr, " %s\n", refname)
518+
}
519+
return b
520+
}

git/ref_filter.go

Lines changed: 82 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -5,68 +5,84 @@ import (
55
"strings"
66
)
77

8-
type ReferenceFilter func(refname string) bool
8+
type ReferenceFilter interface {
9+
Filter(refname string) bool
10+
}
911

10-
func AllReferencesFilter(_ string) bool {
11-
return true
12+
// Combiner combines two `ReferenceFilter`s into one compound one.
13+
// `f1` is allowed to be `nil`.
14+
type Combiner interface {
15+
Combine(f1, f2 ReferenceFilter) ReferenceFilter
16+
Inverted() Combiner
1217
}
1318

14-
type Polarity uint8
19+
type inverse struct {
20+
f ReferenceFilter
21+
}
1522

16-
const (
17-
Include Polarity = iota
18-
Exclude
19-
)
23+
func (f inverse) Filter(refname string) bool {
24+
return !f.f.Filter(refname)
25+
}
2026

21-
func (p Polarity) Inverted() Polarity {
22-
switch p {
23-
case Include:
24-
return Exclude
25-
case Exclude:
26-
return Include
27-
default:
28-
// This shouldn't happen:
29-
return Exclude
30-
}
27+
type intersection struct {
28+
f1, f2 ReferenceFilter
29+
}
30+
31+
func (f intersection) Filter(refname string) bool {
32+
return f.f1.Filter(refname) && f.f2.Filter(refname)
3133
}
3234

33-
// polarizedFilter is a filter that might match, in which case it
34-
// includes or excludes the reference (according to its polarity). If
35-
// it doesn't match, then it doesn't say anything about the reference.
36-
type polarizedFilter struct {
37-
polarity Polarity
38-
filter ReferenceFilter
35+
// Include is a Combiner that includes the references matched by `f2`.
36+
// If `f1` is `nil`, it is treated as including nothing.
37+
type include struct{}
38+
39+
func (_ include) Combine(f1, f2 ReferenceFilter) ReferenceFilter {
40+
if f1 == nil {
41+
return f2
42+
}
43+
return union{f1, f2}
3944
}
4045

41-
// IncludeExcludeFilter is a filter based on a bunch of
42-
// `polarizedFilter`s. The last one that matches a reference wins. If
43-
// none match, then the result is based on the polarity of the first
44-
// polarizedFilter: if it is `Include`, then return `false`; if it is
45-
// `Exclude`, then return `true`.
46-
type IncludeExcludeFilter struct {
47-
filters []polarizedFilter
46+
func (_ include) Inverted() Combiner {
47+
return Exclude
4848
}
4949

50-
func (ief *IncludeExcludeFilter) Include(f ReferenceFilter) {
51-
ief.filters = append(ief.filters, polarizedFilter{Include, f})
50+
var Include include
51+
52+
type union struct {
53+
f1, f2 ReferenceFilter
5254
}
5355

54-
func (ief *IncludeExcludeFilter) Exclude(f ReferenceFilter) {
55-
ief.filters = append(ief.filters, polarizedFilter{Exclude, f})
56+
func (f union) Filter(refname string) bool {
57+
return f.f1.Filter(refname) || f.f2.Filter(refname)
5658
}
5759

58-
func (ief *IncludeExcludeFilter) Filter(refname string) bool {
59-
for i := len(ief.filters); i > 0; i-- {
60-
f := ief.filters[i-1]
61-
if !f.filter(refname) {
62-
continue
63-
}
64-
return f.polarity == Include
60+
// Exclude is a Combiner that excludes the references matched by `f2`.
61+
// If `f1` is `nil`, it is treated as including everything.
62+
type exclude struct{}
63+
64+
func (_ exclude) Combine(f1, f2 ReferenceFilter) ReferenceFilter {
65+
if f1 == nil {
66+
return inverse{f2}
6567
}
68+
return intersection{f1, inverse{f2}}
69+
70+
}
6671

67-
return len(ief.filters) == 0 || ief.filters[0].polarity == Exclude
72+
func (_ exclude) Inverted() Combiner {
73+
return include{}
6874
}
6975

76+
var Exclude exclude
77+
78+
type allReferencesFilter struct{}
79+
80+
func (_ allReferencesFilter) Filter(_ string) bool {
81+
return true
82+
}
83+
84+
var AllReferencesFilter allReferencesFilter
85+
7086
// PrefixFilter returns a `ReferenceFilter` that matches references
7187
// whose names start with the specified `prefix`, which must match at
7288
// a component boundary. For example,
@@ -77,16 +93,23 @@ func (ief *IncludeExcludeFilter) Filter(refname string) bool {
7793
// * Prefix "refs/foo/" matches "refs/foo/bar" but not "refs/foo" or
7894
// "refs/foobar".
7995
func PrefixFilter(prefix string) ReferenceFilter {
80-
if strings.HasSuffix(prefix, "/") {
81-
return func(refname string) bool {
82-
return strings.HasPrefix(refname, prefix)
83-
}
96+
if prefix == "" {
97+
return AllReferencesFilter
8498
}
99+
return prefixFilter{prefix}
100+
}
85101

86-
return func(refname string) bool {
87-
return strings.HasPrefix(refname, prefix) &&
88-
(len(refname) == len(prefix) || refname[len(prefix)] == '/')
102+
type prefixFilter struct {
103+
prefix string
104+
}
105+
106+
func (f prefixFilter) Filter(refname string) bool {
107+
if strings.HasSuffix(f.prefix, "/") {
108+
return strings.HasPrefix(refname, f.prefix)
89109
}
110+
111+
return strings.HasPrefix(refname, f.prefix) &&
112+
(len(refname) == len(f.prefix) || refname[len(f.prefix)] == '/')
90113
}
91114

92115
// RegexpFilter returns a `ReferenceFilter` that matches references
@@ -99,7 +122,13 @@ func RegexpFilter(pattern string) (ReferenceFilter, error) {
99122
return nil, err
100123
}
101124

102-
return func(refname string) bool {
103-
return re.MatchString(refname)
104-
}, nil
125+
return regexpFilter{re}, nil
126+
}
127+
128+
type regexpFilter struct {
129+
re *regexp.Regexp
130+
}
131+
132+
func (f regexpFilter) Filter(refname string) bool {
133+
return f.re.MatchString(refname)
105134
}

git/ref_filter_test.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ func TestPrefixFilter(t *testing.T) {
3838
t.Run(
3939
fmt.Sprintf("prefix '%s', refname '%s'", p.prefix, p.refname),
4040
func(t *testing.T) {
41-
assert.Equal(t, p.expected, git.PrefixFilter(p.prefix)(p.refname))
41+
assert.Equal(
42+
t,
43+
p.expected,
44+
git.PrefixFilter(p.prefix).Filter(p.refname),
45+
)
4246
},
4347
)
4448
}
@@ -73,7 +77,11 @@ func TestRegexpFilter(t *testing.T) {
7377
t.Run(
7478
fmt.Sprintf("pattern '%s', refname '%s'", p.pattern, p.refname),
7579
func(t *testing.T) {
76-
assert.Equal(t, p.expected, regexpFilter(t, p.pattern)(p.refname))
80+
assert.Equal(
81+
t,
82+
p.expected,
83+
regexpFilter(t, p.pattern).Filter(p.refname),
84+
)
7785
},
7886
)
7987
}
@@ -82,11 +90,11 @@ func TestRegexpFilter(t *testing.T) {
8290
func TestIncludeExcludeFilter(t *testing.T) {
8391
t.Parallel()
8492

85-
var filter git.IncludeExcludeFilter
86-
filter.Include(git.PrefixFilter("refs/heads"))
87-
filter.Exclude(regexpFilter(t, "refs/heads/.*foo.*"))
88-
filter.Include(git.PrefixFilter("refs/remotes"))
89-
filter.Exclude(git.PrefixFilter("refs/remotes/foo"))
93+
var filter git.ReferenceFilter
94+
filter = git.Include.Combine(filter, git.PrefixFilter("refs/heads"))
95+
filter = git.Exclude.Combine(filter, regexpFilter(t, "refs/heads/.*foo.*"))
96+
filter = git.Include.Combine(filter, git.PrefixFilter("refs/remotes"))
97+
filter = git.Exclude.Combine(filter, git.PrefixFilter("refs/remotes/foo"))
9098

9199
for _, p := range []struct {
92100
refname string

sizes/graph.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ func ScanRepositoryUsingGraph(
6262
if !ok {
6363
break
6464
}
65-
if !filter(ref.Refname) {
65+
if !filter.Filter(ref.Refname) {
6666
continue
6767
}
6868
refs = append(refs, ref)

0 commit comments

Comments
 (0)