diff --git a/README.md b/README.md index ececfa8..a5bafb5 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ if a file: - [x] content matches regex - [ ] content matches template - [x] content contains a value -- [ ] is gitignored +- [x] is gitignored - [ ] is gitcrypted - [ ] has specific permissions diff --git a/internal/arch/file/should/be_gitignored.go b/internal/arch/file/should/be_gitignored.go new file mode 100644 index 0000000..abd452f --- /dev/null +++ b/internal/arch/file/should/be_gitignored.go @@ -0,0 +1,52 @@ +package should + +import ( + "fmt" + "goarkitect/internal/arch/rule" + "os/exec" + "path/filepath" +) + +func BeGitignored(opts ...Option) *gitIgnoredExpression { + expr := &gitIgnoredExpression{} + + for _, opt := range opts { + opt.apply(&expr.options) + } + + return expr +} + +type gitIgnoredExpression struct { + baseExpression +} + +func (e gitIgnoredExpression) Evaluate(rb rule.Builder) []rule.Violation { + return e.evaluate(rb, e.doEvaluate, e.getViolation) +} + +func (e gitIgnoredExpression) doEvaluate(rb rule.Builder, filePath string) bool { + cmd := exec.Command("git", "check-ignore", "-q", filePath) + if err := cmd.Run(); err != nil { + switch err.(type) { + case *exec.ExitError: + return err.(*exec.ExitError).ExitCode() != 0 + default: + panic(err) + } + } + + return false +} + +func (e gitIgnoredExpression) getViolation(filePath string) rule.Violation { + format := "file '%s' is not gitignored" + + if e.options.negated { + format = "file '%s' is gitignored" + } + + return rule.NewViolation( + fmt.Sprintf(format, filepath.Base(filePath)), + ) +} diff --git a/internal/arch/file/should/be_gitignored_test.go b/internal/arch/file/should/be_gitignored_test.go new file mode 100644 index 0000000..0a302cf --- /dev/null +++ b/internal/arch/file/should/be_gitignored_test.go @@ -0,0 +1,69 @@ +package should_test + +import ( + "goarkitect/internal/arch/file" + "goarkitect/internal/arch/file/should" + "goarkitect/internal/arch/rule" + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +func Test_BeGitignored(t *testing.T) { + basePath, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + testCases := []struct { + desc string + ruleBuilder *file.RuleBuilder + options []should.Option + want []rule.Violation + }{ + { + desc: "file 'ignored.txt' should be gitignored", + ruleBuilder: file.One(filepath.Join(basePath, "test/ignored.txt")), + want: nil, + }, + { + desc: "file 'not_ignored.txt' should be gitignored", + ruleBuilder: file.One(filepath.Join(basePath, "test/not_ignored.txt")), + want: []rule.Violation{ + rule.NewViolation("file 'not_ignored.txt' is not gitignored"), + }, + }, + { + desc: "negated: file 'ignored.txt' should not be gitignored", + ruleBuilder: file.One(filepath.Join(basePath, "test/ignored.txt")), + options: []should.Option{ + should.Negated{}, + }, + want: []rule.Violation{ + rule.NewViolation("file 'ignored.txt' is gitignored"), + }, + }, + { + desc: "negated: file 'not_ignored.txt' should not be gitignored", + ruleBuilder: file.One(filepath.Join(basePath, "test/not_ignored.txt")), + options: []should.Option{ + should.Negated{}, + }, + want: nil, + }, + } + + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + hcm := should.BeGitignored(tC.options...) + got := hcm.Evaluate(tC.ruleBuilder) + + if !cmp.Equal(got, tC.want, cmp.AllowUnexported(rule.Violation{}), cmpopts.EquateEmpty()) { + t.Errorf("want = %+v, got = %+v", tC.want, got) + } + }) + } +} diff --git a/internal/arch/file/should/test/.gitignore b/internal/arch/file/should/test/.gitignore new file mode 100644 index 0000000..f89d64d --- /dev/null +++ b/internal/arch/file/should/test/.gitignore @@ -0,0 +1 @@ +ignored.txt