From 208a95d2361d50f67e2b6f54591146e60e28ca64 Mon Sep 17 00:00:00 2001 From: Cyril Jouve Date: Tue, 29 Aug 2023 23:31:16 +0200 Subject: [PATCH] use .helmignore when identifying changed charts Signed-off-by: Cyril Jouve --- ct/cmd/root.go | 1 + doc/ct_install.md | 1 + doc/ct_lint-and-install.md | 1 + doc/ct_lint.md | 1 + doc/ct_list-changed.md | 1 + pkg/chart/chart.go | 31 ++- pkg/chart/chart_test.go | 47 +++++ pkg/config/config.go | 1 + pkg/config/config_test.go | 1 + pkg/config/test_config.json | 3 +- pkg/config/test_config.yaml | 1 + pkg/ignore/chart_helmignore/.helmignore | 2 + pkg/ignore/chart_no_helmignore/Chart.yaml | 0 pkg/ignore/ignore.go | 81 ++++++++ pkg/ignore/ignore_test.go | 31 +++ pkg/ignore/rules.go | 230 ++++++++++++++++++++++ pkg/ignore/rules_test.go | 155 +++++++++++++++ pkg/ignore/testdata/.helmignore | 3 + pkg/ignore/testdata/.joonix | 0 pkg/ignore/testdata/a.txt | 0 pkg/ignore/testdata/cargo/a.txt | 0 pkg/ignore/testdata/helm.txt | 0 pkg/ignore/testdata/mast/a.txt | 0 pkg/ignore/testdata/rudder.txt | 0 pkg/ignore/testdata/templates/.dotfile | 0 pkg/ignore/testdata/tiller.txt | 0 26 files changed, 585 insertions(+), 6 deletions(-) create mode 100644 pkg/ignore/chart_helmignore/.helmignore create mode 100644 pkg/ignore/chart_no_helmignore/Chart.yaml create mode 100644 pkg/ignore/ignore.go create mode 100644 pkg/ignore/ignore_test.go create mode 100644 pkg/ignore/rules.go create mode 100644 pkg/ignore/rules_test.go create mode 100644 pkg/ignore/testdata/.helmignore create mode 100644 pkg/ignore/testdata/.joonix create mode 100644 pkg/ignore/testdata/a.txt create mode 100644 pkg/ignore/testdata/cargo/a.txt create mode 100644 pkg/ignore/testdata/helm.txt create mode 100644 pkg/ignore/testdata/mast/a.txt create mode 100644 pkg/ignore/testdata/rudder.txt create mode 100644 pkg/ignore/testdata/templates/.dotfile create mode 100644 pkg/ignore/testdata/tiller.txt diff --git a/ct/cmd/root.go b/ct/cmd/root.go index de8223b4..08d5168f 100644 --- a/ct/cmd/root.go +++ b/ct/cmd/root.go @@ -80,6 +80,7 @@ func addCommonFlags(flags *pflag.FlagSet) { flags.Bool("github-groups", false, heredoc.Doc(` Change the delimiters for github to create collapsible groups for command output`)) + flags.Bool("use-helmignore", false, "Use .helmignore when identifying changed charts") } func addCommonLintAndInstallFlags(flags *pflag.FlagSet) { diff --git a/doc/ct_install.md b/doc/ct_install.md index 79517855..a48cde15 100644 --- a/doc/ct_install.md +++ b/doc/ct_install.md @@ -75,6 +75,7 @@ ct install [flags] --target-branch string The name of the target branch used to identify changed charts (default "master") --upgrade Whether to test an in-place upgrade of each chart from its previous revision if the current version should not introduce a breaking change according to the SemVer spec + --use-helmignore Use .helmignore when identifying changed charts ``` ### SEE ALSO diff --git a/doc/ct_lint-and-install.md b/doc/ct_lint-and-install.md index 73d13fe0..86ecd83d 100644 --- a/doc/ct_lint-and-install.md +++ b/doc/ct_lint-and-install.md @@ -70,6 +70,7 @@ ct lint-and-install [flags] --target-branch string The name of the target branch used to identify changed charts (default "master") --upgrade Whether to test an in-place upgrade of each chart from its previous revision if the current version should not introduce a breaking change according to the SemVer spec + --use-helmignore Use .helmignore when identifying changed charts --validate-chart-schema Enable schema validation of 'Chart.yaml' using Yamale (default true) --validate-maintainers Enable validation of maintainer account names in chart.yml. Works for GitHub, GitLab, and Bitbucket (default true) diff --git a/doc/ct_lint.md b/doc/ct_lint.md index f35ee02a..d85d2b19 100644 --- a/doc/ct_lint.md +++ b/doc/ct_lint.md @@ -68,6 +68,7 @@ ct lint [flags] --remote string The name of the Git remote used to identify changed charts (default "origin") --since string The Git reference used to identify changed charts (default "HEAD") --target-branch string The name of the target branch used to identify changed charts (default "master") + --use-helmignore Use .helmignore when identifying changed charts --validate-chart-schema Enable schema validation of 'Chart.yaml' using Yamale (default true) --validate-maintainers Enable validation of maintainer account names in chart.yml. Works for GitHub, GitLab, and Bitbucket (default true) diff --git a/doc/ct_list-changed.md b/doc/ct_list-changed.md index 4c84f770..2cbbdcd1 100644 --- a/doc/ct_list-changed.md +++ b/doc/ct_list-changed.md @@ -28,6 +28,7 @@ ct list-changed [flags] --remote string The name of the Git remote used to identify changed charts (default "origin") --since string The Git reference used to identify changed charts (default "HEAD") --target-branch string The name of the target branch used to identify changed charts (default "master") + --use-helmignore Use .helmignore when identifying changed charts ``` ### SEE ALSO diff --git a/pkg/chart/chart.go b/pkg/chart/chart.go index 352c8298..4ae2a796 100644 --- a/pkg/chart/chart.go +++ b/pkg/chart/chart.go @@ -25,6 +25,7 @@ import ( "github.com/helm/chart-testing/v3/pkg/config" "github.com/helm/chart-testing/v3/pkg/exec" + "github.com/helm/chart-testing/v3/pkg/ignore" "github.com/helm/chart-testing/v3/pkg/tool" "github.com/helm/chart-testing/v3/pkg/util" ) @@ -242,6 +243,7 @@ type Testing struct { directoryLister DirectoryLister utils Utils previousRevisionWorktree string + loadRules func(string) (*ignore.Rules, error) } // TestResults holds results and overall status @@ -271,6 +273,7 @@ func NewTesting(config config.Configuration, extraSetArgs string) (Testing, erro accountValidator: tool.AccountValidator{}, directoryLister: util.DirectoryLister{}, utils: util.Utils{}, + loadRules: ignore.LoadRules, } versionString, err := testing.helm.Version() @@ -744,7 +747,7 @@ func (t *Testing) ComputeChangedChartDirectories() ([]string, error) { return nil, fmt.Errorf("failed creating diff: %w", err) } - var changedChartDirs []string + changedChartFiles := map[string][]string{} for _, file := range allChangedChartFiles { pathElements := strings.SplitN(filepath.ToSlash(file), "/", 3) if len(pathElements) < 2 || util.StringSliceContains(cfg.ExcludedCharts, pathElements[1]) { @@ -761,15 +764,33 @@ func (t *Testing) ComputeChangedChartDirectories() ([]string, error) { continue } } - // Only add it if not already in the list - if !util.StringSliceContains(changedChartDirs, chartDir) { - changedChartDirs = append(changedChartDirs, chartDir) - } + changedChartFiles[chartDir] = append(changedChartFiles[chartDir], strings.TrimPrefix(file, chartDir+"/")) } else { fmt.Fprintf(os.Stderr, "Directory %q is not a valid chart directory. Skipping...\n", dir) } } + changedChartDirs := []string{} + if t.config.UseHelmignore { + for chartDir, changedChartFiles := range changedChartFiles { + rules, err := t.loadRules(chartDir) + if err != nil { + return nil, err + } + filteredChartFiles, err := ignore.FilterFiles(changedChartFiles, rules) + if err != nil { + return nil, err + } + if len(filteredChartFiles) > 0 { + changedChartDirs = append(changedChartDirs, chartDir) + } + } + } else { + for chartDir := range changedChartFiles { + changedChartDirs = append(changedChartDirs, chartDir) + } + } + return changedChartDirs, nil } diff --git a/pkg/chart/chart_test.go b/pkg/chart/chart_test.go index a45ff829..c6add777 100644 --- a/pkg/chart/chart_test.go +++ b/pkg/chart/chart_test.go @@ -20,6 +20,7 @@ import ( "testing" "github.com/helm/chart-testing/v3/pkg/config" + "github.com/helm/chart-testing/v3/pkg/ignore" "github.com/helm/chart-testing/v3/pkg/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -152,6 +153,26 @@ func newTestingMock(cfg config.Configuration) Testing { accountValidator: fakeAccountValidator{}, linter: fakeMockLinter, helm: new(fakeHelm), + loadRules: func(dir string) (*ignore.Rules, error) { + rules := ignore.Empty() + if dir == "test_charts/foo" { + var err error + rules, err = ignore.Parse(strings.NewReader("Chart.yaml\n")) + if err != nil { + return nil, err + } + rules.AddDefaults() + } + if dir == "test_chart_at_multi_level/foo/baz" { + var err error + rules, err = ignore.Parse(strings.NewReader("Chart.yaml\n")) + if err != nil { + return nil, err + } + rules.AddDefaults() + } + return rules, nil + }, } } @@ -165,6 +186,19 @@ func TestComputeChangedChartDirectories(t *testing.T) { assert.Nil(t, err) } +func TestComputeChangedChartDirectoriesWithHelmignore(t *testing.T) { + cfg := config.Configuration{ + ExcludedCharts: []string{"excluded"}, + ChartDirs: []string{"test_charts", "."}, + UseHelmignore: true, + } + ct := newTestingMock(cfg) + actual, err := ct.ComputeChangedChartDirectories() + expected := []string{"test_charts/bar", "test_chart_at_root"} + assert.Nil(t, err) + assert.ElementsMatch(t, expected, actual) +} + func TestComputeChangedChartDirectoriesWithMultiLevelChart(t *testing.T) { cfg := config.Configuration{ ExcludedCharts: []string{"excluded"}, @@ -180,6 +214,19 @@ func TestComputeChangedChartDirectoriesWithMultiLevelChart(t *testing.T) { assert.Nil(t, err) } +func TestComputeChangedChartDirectoriesWithMultiLevelChartWithHelmIgnore(t *testing.T) { + cfg := config.Configuration{ + ExcludedCharts: []string{"excluded"}, + ChartDirs: []string{"test_chart_at_multi_level/foo"}, + UseHelmignore: true, + } + ct := newTestingMock(cfg) + actual, err := ct.ComputeChangedChartDirectories() + expected := []string{"test_chart_at_multi_level/foo/bar"} + assert.Nil(t, err) + assert.ElementsMatch(t, expected, actual) +} + func TestReadAllChartDirectories(t *testing.T) { actual, err := ct.ReadAllChartDirectories() expected := []string{ diff --git a/pkg/config/config.go b/pkg/config/config.go index 114e2596..0181ce13 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -72,6 +72,7 @@ type Configuration struct { KubectlTimeout time.Duration `mapstructure:"kubectl-timeout"` PrintLogs bool `mapstructure:"print-logs"` GithubGroups bool `mapstructure:"github-groups"` + UseHelmignore bool `mapstructure:"use-helmignore"` } func LoadConfiguration(cfgFile string, cmd *cobra.Command, printConfig bool) (*Configuration, error) { diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index bc641315..ea71be2a 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -60,6 +60,7 @@ func loadAndAssertConfigFromFile(t *testing.T, configFile string) { require.Equal(t, true, cfg.ExcludeDeprecated) require.Equal(t, 120*time.Second, cfg.KubectlTimeout) require.Equal(t, true, cfg.SkipCleanUp) + require.Equal(t, true, cfg.UseHelmignore) } func Test_findConfigFile(t *testing.T) { diff --git a/pkg/config/test_config.json b/pkg/config/test_config.json index df0b1009..3ca7488f 100644 --- a/pkg/config/test_config.json +++ b/pkg/config/test_config.json @@ -31,5 +31,6 @@ "release-label": "release", "exclude-deprecated": true, "kubectl-timeout": "120s", - "skip-clean-up": true + "skip-clean-up": true, + "use-helmignore": true } diff --git a/pkg/config/test_config.yaml b/pkg/config/test_config.yaml index 1381b96b..3d61ebcc 100644 --- a/pkg/config/test_config.yaml +++ b/pkg/config/test_config.yaml @@ -27,3 +27,4 @@ release-label: release exclude-deprecated: true kubectl-timeout: 120s skip-clean-up: true +use-helmignore: true diff --git a/pkg/ignore/chart_helmignore/.helmignore b/pkg/ignore/chart_helmignore/.helmignore new file mode 100644 index 00000000..5c37f9f9 --- /dev/null +++ b/pkg/ignore/chart_helmignore/.helmignore @@ -0,0 +1,2 @@ +toto +tutu diff --git a/pkg/ignore/chart_no_helmignore/Chart.yaml b/pkg/ignore/chart_no_helmignore/Chart.yaml new file mode 100644 index 00000000..e69de29b diff --git a/pkg/ignore/ignore.go b/pkg/ignore/ignore.go new file mode 100644 index 00000000..ff60f419 --- /dev/null +++ b/pkg/ignore/ignore.go @@ -0,0 +1,81 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package ignore + +import ( + "io/fs" + "os" + "path/filepath" + "testing/fstest" +) + +func LoadRules(dir string) (*Rules, error) { + rules, err := ParseFile(filepath.Join(dir, HelmIgnore)) + if err != nil && !os.IsNotExist(err) { + return nil, err + } + if rules == nil { + rules = Empty() + } + rules.AddDefaults() + return rules, nil +} + +func FilterFiles(files []string, rules *Rules) ([]string, error) { + fsys := fstest.MapFS{} + for _, file := range files { + fsys[file] = &fstest.MapFile{} + } + + filteredFiles := []string{} + + err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + fi, err := d.Info() + if err != nil { + return err + } + + // Normalize to / since it will also work on Windows + path = filepath.ToSlash(path) + + if fi.IsDir() { + // Directory-based ignore rules should involve skipping the entire + // contents of that directory. + if rules.Ignore(path, fi) { + return filepath.SkipDir + } + return nil + } + + // If a .helmignore file matches, skip this file. + if rules.Ignore(path, fi) { + return nil + } + + filteredFiles = append(filteredFiles, path) + return nil + }) + if err != nil { + return nil, err + } + + return filteredFiles, nil +} diff --git a/pkg/ignore/ignore_test.go b/pkg/ignore/ignore_test.go new file mode 100644 index 00000000..8253bdfe --- /dev/null +++ b/pkg/ignore/ignore_test.go @@ -0,0 +1,31 @@ +package ignore + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLoadRulesNoHelmignore(t *testing.T) { + r, err := LoadRules("chart_no_helmignore") + assert.Nil(t, err) + // default pattern + assert.Len(t, r.patterns, 1) +} + +func TestLoadRulesHelmignore(t *testing.T) { + r, err := LoadRules("chart_helmignore") + assert.Nil(t, err) + assert.Len(t, r.patterns, 3) +} + +func TestFilter(t *testing.T) { + rules, err := Parse(strings.NewReader("/bar/\nREADME.md\n")) + assert.Nil(t, err) + files := []string{"Chart.yaml", "bar/xxx", "template/svc.yaml", "baz/bar/biz.txt", "README.md"} + actual, err := FilterFiles(files, rules) + assert.Nil(t, err) + expected := []string{"Chart.yaml", "baz/bar/biz.txt", "template/svc.yaml"} + assert.ElementsMatch(t, expected, actual) +} diff --git a/pkg/ignore/rules.go b/pkg/ignore/rules.go new file mode 100644 index 00000000..7c346cec --- /dev/null +++ b/pkg/ignore/rules.go @@ -0,0 +1,230 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package ignore + +import ( + "bufio" + "bytes" + "errors" + "io" + "log" + "os" + "path/filepath" + "strings" +) + +// HelmIgnore default name of an ignorefile. +const HelmIgnore = ".helmignore" + +// Rules is a collection of path matching rules. +// +// Parse() and ParseFile() will construct and populate new Rules. +// Empty() will create an immutable empty ruleset. +type Rules struct { + patterns []*pattern +} + +// Empty builds an empty ruleset. +func Empty() *Rules { + return &Rules{patterns: []*pattern{}} +} + +// AddDefaults adds default ignore patterns. +// +// Ignore all dotfiles in "templates/" +func (r *Rules) AddDefaults() { + if err := r.parseRule(`templates/.?*`); err != nil { + panic(err) + } +} + +// ParseFile parses a helmignore file and returns the *Rules. +func ParseFile(file string) (*Rules, error) { + f, err := os.Open(file) + if err != nil { + return nil, err + } + defer f.Close() + return Parse(f) +} + +// Parse parses a rules file +func Parse(file io.Reader) (*Rules, error) { + r := &Rules{patterns: []*pattern{}} + + s := bufio.NewScanner(file) + currentLine := 0 + utf8bom := []byte{0xEF, 0xBB, 0xBF} + for s.Scan() { + scannedBytes := s.Bytes() + // We trim UTF8 BOM + if currentLine == 0 { + scannedBytes = bytes.TrimPrefix(scannedBytes, utf8bom) + } + line := string(scannedBytes) + currentLine++ + + if err := r.parseRule(line); err != nil { + return r, err + } + } + return r, s.Err() +} + +// Ignore evaluates the file at the given path, and returns true if it should be ignored. +// +// Ignore evaluates path against the rules in order. Evaluation stops when a match +// is found. Matching a negative rule will stop evaluation. +func (r *Rules) Ignore(path string, fi os.FileInfo) bool { + // Don't match on empty dirs. + if path == "" { + return false + } + + // Disallow ignoring the current working directory. + // See issue: + // 1776 (New York City) Hamilton: "Pardon me, are you Aaron Burr, sir?" + if path == "." || path == "./" { + return false + } + for _, p := range r.patterns { + if p.match == nil { + log.Printf("ignore: no matcher supplied for %q", p.raw) + return false + } + + // For negative rules, we need to capture and return non-matches, + // and continue for matches. + if p.negate { + if p.mustDir && !fi.IsDir() { + return true + } + if !p.match(path, fi) { + return true + } + continue + } + + // If the rule is looking for directories, and this is not a directory, + // skip it. + if p.mustDir && !fi.IsDir() { + continue + } + if p.match(path, fi) { + return true + } + } + return false +} + +// parseRule parses a rule string and creates a pattern, which is then stored in the Rules object. +func (r *Rules) parseRule(rule string) error { + rule = strings.TrimSpace(rule) + + // Ignore blank lines + if rule == "" { + return nil + } + // Comment + if strings.HasPrefix(rule, "#") { + return nil + } + + // Fail any rules that contain ** + if strings.Contains(rule, "**") { + return errors.New("double-star (**) syntax is not supported") + } + + // Fail any patterns that can't compile. A non-empty string must be + // given to Match() to avoid optimization that skips rule evaluation. + if _, err := filepath.Match(rule, "abc"); err != nil { + return err + } + + p := &pattern{raw: rule} + + // Negation is handled at a higher level, so strip the leading ! from the + // string. + if strings.HasPrefix(rule, "!") { + p.negate = true + rule = rule[1:] + } + + // Directory verification is handled by a higher level, so the trailing / + // is removed from the rule. That way, a directory named "foo" matches, + // even if the supplied string does not contain a literal slash character. + if strings.HasSuffix(rule, "/") { + p.mustDir = true + rule = strings.TrimSuffix(rule, "/") + } + + switch { + case strings.HasPrefix(rule, "/"): + // Require path matches the root path. + p.match = func(n string, fi os.FileInfo) bool { + rule = strings.TrimPrefix(rule, "/") + ok, err := filepath.Match(rule, n) + if err != nil { + log.Printf("Failed to compile %q: %s", rule, err) + return false + } + return ok + } + case strings.Contains(rule, "/"): + // require structural match. + p.match = func(n string, fi os.FileInfo) bool { + ok, err := filepath.Match(rule, n) + if err != nil { + log.Printf("Failed to compile %q: %s", rule, err) + return false + } + return ok + } + default: + p.match = func(n string, fi os.FileInfo) bool { + // When there is no slash in the pattern, we evaluate ONLY the + // filename. + n = filepath.Base(n) + ok, err := filepath.Match(rule, n) + if err != nil { + log.Printf("Failed to compile %q: %s", rule, err) + return false + } + return ok + } + } + + r.patterns = append(r.patterns, p) + return nil +} + +// matcher is a function capable of computing a match. +// +// It returns true if the rule matches. +type matcher func(name string, fi os.FileInfo) bool + +// pattern describes a pattern to be matched in a rule set. +type pattern struct { + // raw is the unparsed string, with nothing stripped. + raw string + // match is the matcher function. + match matcher + // negate indicates that the rule's outcome should be negated. + negate bool + // mustDir indicates that the matched file must be a directory. + mustDir bool +} diff --git a/pkg/ignore/rules_test.go b/pkg/ignore/rules_test.go new file mode 100644 index 00000000..9581cf09 --- /dev/null +++ b/pkg/ignore/rules_test.go @@ -0,0 +1,155 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package ignore + +import ( + "bytes" + "os" + "path/filepath" + "testing" +) + +var testdata = "./testdata" + +func TestParse(t *testing.T) { + rules := `#ignore + + #ignore +foo +bar/* +baz/bar/foo.txt + +one/more +` + r, err := parseString(rules) + if err != nil { + t.Fatalf("Error parsing rules: %s", err) + } + + if len(r.patterns) != 4 { + t.Errorf("Expected 4 rules, got %d", len(r.patterns)) + } + + expects := []string{"foo", "bar/*", "baz/bar/foo.txt", "one/more"} + for i, p := range r.patterns { + if p.raw != expects[i] { + t.Errorf("Expected %q, got %q", expects[i], p.raw) + } + if p.match == nil { + t.Errorf("Expected %s to have a matcher function.", p.raw) + } + } +} + +func TestParseFail(t *testing.T) { + shouldFail := []string{"foo/**/bar", "[z-"} + for _, fail := range shouldFail { + _, err := parseString(fail) + if err == nil { + t.Errorf("Rule %q should have failed", fail) + } + } +} + +func TestParseFile(t *testing.T) { + f := filepath.Join(testdata, HelmIgnore) + if _, err := os.Stat(f); err != nil { + t.Fatalf("Fixture %s missing: %s", f, err) + } + + r, err := ParseFile(f) + if err != nil { + t.Fatalf("Failed to parse rules file: %s", err) + } + + if len(r.patterns) != 3 { + t.Errorf("Expected 3 patterns, got %d", len(r.patterns)) + } +} + +func TestIgnore(t *testing.T) { + // Test table: Given pattern and name, Ignore should return expect. + tests := []struct { + pattern string + name string + expect bool + }{ + // Glob tests + {`helm.txt`, "helm.txt", true}, + {`helm.*`, "helm.txt", true}, + {`helm.*`, "rudder.txt", false}, + {`*.txt`, "tiller.txt", true}, + {`*.txt`, "cargo/a.txt", true}, + {`cargo/*.txt`, "cargo/a.txt", true}, + {`cargo/*.*`, "cargo/a.txt", true}, + {`cargo/*.txt`, "mast/a.txt", false}, + {`ru[c-e]?er.txt`, "rudder.txt", true}, + {`templates/.?*`, "templates/.dotfile", true}, + // "." should never get ignored. https://github.com/helm/helm/issues/1776 + {`.*`, ".", false}, + {`.*`, "./", false}, + {`.*`, ".joonix", true}, + {`.*`, "helm.txt", false}, + {`.*`, "", false}, + + // Directory tests + {`cargo/`, "cargo", true}, + {`cargo/`, "cargo/", true}, + {`cargo/`, "mast/", false}, + {`helm.txt/`, "helm.txt", false}, + + // Negation tests + {`!helm.txt`, "helm.txt", false}, + {`!helm.txt`, "tiller.txt", true}, + {`!*.txt`, "cargo", true}, + {`!cargo/`, "mast/", true}, + + // Absolute path tests + {`/a.txt`, "a.txt", true}, + {`/a.txt`, "cargo/a.txt", false}, + {`/cargo/a.txt`, "cargo/a.txt", true}, + } + + for _, test := range tests { + r, err := parseString(test.pattern) + if err != nil { + t.Fatalf("Failed to parse: %s", err) + } + fi, err := os.Stat(filepath.Join(testdata, test.name)) + if err != nil { + t.Fatalf("Fixture missing: %s", err) + } + + if r.Ignore(test.name, fi) != test.expect { + t.Errorf("Expected %q to be %v for pattern %q", test.name, test.expect, test.pattern) + } + } +} + +func TestAddDefaults(t *testing.T) { + r := Rules{} + r.AddDefaults() + + if len(r.patterns) != 1 { + t.Errorf("Expected 1 default patterns, got %d", len(r.patterns)) + } +} + +func parseString(str string) (*Rules, error) { + b := bytes.NewBuffer([]byte(str)) + return Parse(b) +} diff --git a/pkg/ignore/testdata/.helmignore b/pkg/ignore/testdata/.helmignore new file mode 100644 index 00000000..b2693bae --- /dev/null +++ b/pkg/ignore/testdata/.helmignore @@ -0,0 +1,3 @@ +mast/a.txt +.DS_Store +.git diff --git a/pkg/ignore/testdata/.joonix b/pkg/ignore/testdata/.joonix new file mode 100644 index 00000000..e69de29b diff --git a/pkg/ignore/testdata/a.txt b/pkg/ignore/testdata/a.txt new file mode 100644 index 00000000..e69de29b diff --git a/pkg/ignore/testdata/cargo/a.txt b/pkg/ignore/testdata/cargo/a.txt new file mode 100644 index 00000000..e69de29b diff --git a/pkg/ignore/testdata/helm.txt b/pkg/ignore/testdata/helm.txt new file mode 100644 index 00000000..e69de29b diff --git a/pkg/ignore/testdata/mast/a.txt b/pkg/ignore/testdata/mast/a.txt new file mode 100644 index 00000000..e69de29b diff --git a/pkg/ignore/testdata/rudder.txt b/pkg/ignore/testdata/rudder.txt new file mode 100644 index 00000000..e69de29b diff --git a/pkg/ignore/testdata/templates/.dotfile b/pkg/ignore/testdata/templates/.dotfile new file mode 100644 index 00000000..e69de29b diff --git a/pkg/ignore/testdata/tiller.txt b/pkg/ignore/testdata/tiller.txt new file mode 100644 index 00000000..e69de29b