diff --git a/docs/configuration.md b/docs/configuration.md index 70d3793c..22b549a5 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -13,10 +13,10 @@ Lefthook [supports](#config-file) YAML, JSON, and TOML configuration. In this do - [`source_dir`](#source_dir) - [`source_dir_local`](#source_dir_local) - [`rc`](#rc) -- [`remote`](#remote--deprecated-show-remotes-instead) :warning: DEPRECATED use [`remotes`](#remotes) +- [`remote`](#remote--deprecated-show-remotes-instead) :warning: **DEPRECATED** use [`remotes`](#remotes) - [`git_url`](#git_url) - [`ref`](#ref) - - [`config`](#config--deprecated-use-configs-like-specified-in-remotes) + - [`config`](#config) - [`remotes`](#remotes) - [`git_url`](#git_url-1) - [`ref`](#ref-1) @@ -41,6 +41,7 @@ Lefthook [supports](#config-file) YAML, JSON, and TOML configuration. In this do - [`tags`](#tags) - [`glob`](#glob) - [`files`](#files) + - [`file_types`](#file_types) - [`env`](#env) - [`root`](#root) - [`exclude`](#exclude) @@ -202,7 +203,8 @@ LEFTHOOK_OUTPUT="meta,success,summary" lefthook run pre-commit ### `skip_output` -> **Deprecated:** This feature is deprecated and might be removed in future versions. Please, use `[output]` instead for managing verbosity. +> [!IMPORTANT] +> **DEPRECATED** This feature is deprecated and might be removed in future versions. Please, use `[output]` instead for managing verbosity. You can manage the verbosity using the `skip_output` config. You can set whether lefthook should print some parts of its output. @@ -330,7 +332,8 @@ Now any program that runs your hooks will have a tweaked PATH environment variab ## `remote` -> :warning: DEPRECATED use [`remotes`](#remotes) setting +> [!WARNING] +> **DEPRECATED** Use [`remotes`](#remotes) setting You can provide a remote config if you want to share your lefthook configuration across many projects. Lefthook will automatically download and merge the configuration into your local `lefthook.yml`. @@ -350,7 +353,8 @@ This can be changed in the future. For convenience, please use `remote` configur ### `git_url` -> :warning: DEPRECATED use [`remotes`](#remotes) setting +> [!WARNING] +> **DEPRECATED** Use [`remotes`](#remotes) setting A URL to Git repository. It will be accessed with privileges of the machine lefthook runs on. @@ -374,7 +378,8 @@ remote: ### `ref` -> :warning: DEPRECATED use [`remotes`](#remotes) setting +> [!WARNING] +> **DEPRECATED** Use [`remotes`](#remotes) setting An optional *branch* or *tag* name. @@ -388,13 +393,14 @@ remote: ref: v1.0.0 ``` -> **Note** +> [!CAUTION] > -> :warning: If you initially had `ref` option, ran `lefthook install`, and then removed it, lefthook won't decide which branch/tag to use as a ref. So, if you added it once, please, use it always to avoid issues in local setups. +> If you initially had `ref` option, ran `lefthook install`, and then removed it, lefthook won't decide which branch/tag to use as a ref. So, if you added it once, please, use it always to avoid issues in local setups. ### `config` -> :warning: DEPRECATED use [`remotes`](#remotes) setting +> [!WARNING] +> **DEPRECATED**. Use [`remotes`](#remotes) setting **Default:** `lefthook.yml` @@ -413,6 +419,7 @@ remote: ## `remotes` +> [!IMPORTANT] > :test_tube: This feature is in **Beta** version You can provide multiple remote configs if you want to share yours lefthook configurations across many projects. Lefthook will automatically download and merge configurations into your local `lefthook.yml`. @@ -467,7 +474,7 @@ remotes: ref: v1.0.0 ``` -> :warning: **Note** +> [!NOTE] > > If you initially had `ref` option, ran `lefthook install`, and then removed it, lefthook won't decide which branch/tag to use as a ref. So, if you added it once, please, use it always to avoid issues in local setups. @@ -665,7 +672,8 @@ pre-commit: Scripts to be executed for the hook. Each script has a name (filename in scripts dir) and associated run [options](#script). -**:warning: Important**: script must exist under `//` folder. See [`source_dir`](#source_dir). +> [!IMPORTANT] +> Script must exist under `//` folder. See [`source_dir`](#source_dir). **Example** @@ -938,33 +946,33 @@ pre-commit: run: yarn test ``` -**Notes** - -Always skipping is useful when you have a `lefthook-local.yml` config and you don't want to run some commands locally. So you just overwrite the `skip` option for them to be `true`. - -```yml -# lefthook.yml - -pre-commit: - commands: - lint: - run: yarn lint -``` - -```yml -# lefthook-local.yml - -pre-commit: - commands: - lint: - skip: true -``` +> [!TIP] +> +> Always skipping is useful when you have a `lefthook-local.yml` config and you don't want to run some commands locally. So you just overwrite the `skip` option for them to be `true`. +> +> ```yml +> # lefthook.yml +> +> pre-commit: +> commands: +> lint: +> run: yarn lint +> ``` +> +> ```yml +> # lefthook-local.yml +> +> pre-commit: +> commands: +> lint: +> skip: true +> ``` ### `only` You can force a command, script, or the whole hook to execute only in certain conditions. This option acts like the opposite of [`skip`](#skip). It accepts the same values but skips execution only if the condition is not satisfied. -> **Note** +> [!NOTE] > > `skip` option takes precedence over `only` option, so if you have conflicting conditions the execution will be skipped. @@ -1092,6 +1100,66 @@ pre-push: run: bundle exec rubocop --force-exclusion --parallel {files} ``` +### `file_types` + +Filter files in a [`run`](#run) templates by their type. Supported types: + +|File type| Exlanation| +|---------|-----------| +|`text` | Any file that contains text. Symlinks are not followed. | +|`binary` | Any file that contains non-text bytes. Symlinks are not followed. | +|`executable` | Any file that has executable bits set. Symlinks are not followed. | +|`not executable` | Any file without executable bits in file mode. Symlinks included. | +|`symlink` | A symlink file. | +|`not symlink` | Any non-symlink file. | + +> [!IMPORTANT] +> When passed multiple file types all constraints will be applied to the resulting list of files. + +**Examples** + +Apply some different linters on text and binary files. + +```yml +# lefthook.yml + +pre-commit: + commands: + lint-code: + run: yarn lint {staged_files} + file_types: text + check-hex-codes: + run: yarn check-hex {staged_files} + file_types: binary +``` + +Skip symlinks. + +```yml +# lefthook.yml + +pre-commit: + commands: + lint: + run: yarn lint --fix {staged_files} + file_types: + - not symlink +``` + +Lint executable scripts. + +```yml +# lefthook.yml + +pre-commit: + commands: + lint: + run: yarn lint --fix {staged_files} + file_types: + - executable + - text +``` + ### `env` You can specify some ENV variables for the command or script. diff --git a/go.mod b/go.mod index 0a17b5b0..1e844c23 100644 --- a/go.mod +++ b/go.mod @@ -48,7 +48,7 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect golang.org/x/sys v0.15.0 // indirect - golang.org/x/term v0.5.0 // indirect + golang.org/x/term v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index 5b7c1685..07f88fa2 100644 --- a/go.sum +++ b/go.sum @@ -110,8 +110,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= diff --git a/internal/config/command.go b/internal/config/command.go index 9caf44a0..f86d6d81 100644 --- a/internal/config/command.go +++ b/internal/config/command.go @@ -13,19 +13,21 @@ import ( var errFilesIncompatible = errors.New("One of your runners contains incompatible file types") type Command struct { - Run string `json:"run" mapstructure:"run" toml:"run" yaml:"run"` + Run string `json:"run" mapstructure:"run" toml:"run" yaml:"run"` + Files string `json:"files,omitempty" mapstructure:"files" toml:"files,omitempty" yaml:",omitempty"` - Skip interface{} `json:"skip,omitempty" mapstructure:"skip" toml:"skip,omitempty,inline" yaml:",omitempty"` - Only interface{} `json:"only,omitempty" mapstructure:"only" toml:"only,omitempty,inline" yaml:",omitempty"` - Tags []string `json:"tags,omitempty" mapstructure:"tags" toml:"tags,omitempty" yaml:",omitempty"` - Glob string `json:"glob,omitempty" mapstructure:"glob" toml:"glob,omitempty" yaml:",omitempty"` - Files string `json:"files,omitempty" mapstructure:"files" toml:"files,omitempty" yaml:",omitempty"` - Env map[string]string `json:"env,omitempty" mapstructure:"env" toml:"env,omitempty" yaml:",omitempty"` + Skip interface{} `json:"skip,omitempty" mapstructure:"skip" toml:"skip,omitempty,inline" yaml:",omitempty"` + Only interface{} `json:"only,omitempty" mapstructure:"only" toml:"only,omitempty,inline" yaml:",omitempty"` + Tags []string `json:"tags,omitempty" mapstructure:"tags" toml:"tags,omitempty" yaml:",omitempty"` + Env map[string]string `json:"env,omitempty" mapstructure:"env" toml:"env,omitempty" yaml:",omitempty"` - Root string `json:"root,omitempty" mapstructure:"root" toml:"root,omitempty" yaml:",omitempty"` - Exclude string `json:"exclude,omitempty" mapstructure:"exclude" toml:"exclude,omitempty" yaml:",omitempty"` - Priority int `json:"priority,omitempty" mapstructure:"priority" toml:"priority,omitempty" yaml:",omitempty"` + FileTypes []string `json:"file_types,omitempty" mapstructure:"file_types" toml:"file_types,omitempty" yaml:"file_types,omitempty"` + Glob string `json:"glob,omitempty" mapstructure:"glob" toml:"glob,omitempty" yaml:",omitempty"` + Root string `json:"root,omitempty" mapstructure:"root" toml:"root,omitempty" yaml:",omitempty"` + Exclude string `json:"exclude,omitempty" mapstructure:"exclude" toml:"exclude,omitempty" yaml:",omitempty"` + + Priority int `json:"priority,omitempty" mapstructure:"priority" toml:"priority,omitempty" yaml:",omitempty"` FailText string `json:"fail_text,omitempty" mapstructure:"fail_text" toml:"fail_text,omitempty" yaml:"fail_text,omitempty"` Interactive bool `json:"interactive,omitempty" mapstructure:"interactive" toml:"interactive,omitempty" yaml:",omitempty"` UseStdin bool `json:"use_stdin,omitempty" mapstructure:"use_stdin" toml:"use_stdin,omitempty" yaml:",omitempty"` diff --git a/internal/lefthook/runner/filter/filters.go b/internal/lefthook/runner/filter/filters.go deleted file mode 100644 index 00af16fc..00000000 --- a/internal/lefthook/runner/filter/filters.go +++ /dev/null @@ -1,71 +0,0 @@ -package filter - -import ( - "regexp" - "strings" - - "github.com/gobwas/glob" - - "github.com/evilmartians/lefthook/internal/config" - "github.com/evilmartians/lefthook/internal/log" -) - -func Apply(command *config.Command, files []string) []string { - if len(files) == 0 { - return nil - } - - log.Debug("[lefthook] files before filters:\n", files) - - files = byGlob(files, command.Glob) - files = byExclude(files, command.Exclude) - files = byRoot(files, command.Root) - - log.Debug("[lefthook] files after filters:\n", files) - - return files -} - -func byGlob(vs []string, matcher string) []string { - if matcher == "" { - return vs - } - - g := glob.MustCompile(strings.ToLower(matcher)) - - vsf := make([]string, 0) - for _, v := range vs { - if res := g.Match(strings.ToLower(v)); res { - vsf = append(vsf, v) - } - } - return vsf -} - -func byExclude(vs []string, matcher string) []string { - if matcher == "" { - return vs - } - - vsf := make([]string, 0) - for _, v := range vs { - if res, _ := regexp.MatchString(matcher, v); !res { - vsf = append(vsf, v) - } - } - return vsf -} - -func byRoot(vs []string, matcher string) []string { - if matcher == "" { - return vs - } - - vsf := make([]string, 0) - for _, v := range vs { - if strings.HasPrefix(v, matcher) { - vsf = append(vsf, strings.Replace(v, matcher, "./", 1)) - } - } - return vsf -} diff --git a/internal/lefthook/runner/filters/detect_text.go b/internal/lefthook/runner/filters/detect_text.go new file mode 100644 index 00000000..193c5cea --- /dev/null +++ b/internal/lefthook/runner/filters/detect_text.go @@ -0,0 +1,47 @@ +package filters + +import ( + "bytes" +) + +// See: https://github.com/gabriel-vasile/mimetype/blob/6e3aeb1/internal/charset/charset.go + +var boms = [][]byte{ + {0xEF, 0xBB, 0xBF}, // utf-8 + {0x00, 0x00, 0xFE, 0xFF}, // utf-32be + {0xFF, 0xFE, 0x00, 0x00}, // utf-32le + {0xFE, 0xFF}, // utf-16be + {0xFF, 0xFE}, // utf-16le +} + +// hasBOM returns true if the charset declared in the BOM of content. +func hasBOM(content []byte) bool { + for _, bom := range boms { + if bytes.HasPrefix(content, bom) { + return true + } + } + return false +} + +// detectText checks if a sequence contains of a plain text bytes. +// +// This function does not parse BOM-less UTF16 and UTF32 files. Not really +// sure it should. Linux file utility also requires a BOM for UTF16 and UTF32. +func detectText(bytes []byte) bool { + if hasBOM(bytes) { + return true + } + + // Binary data bytes as defined here: https://mimesniff.spec.whatwg.org/#binary-data-byte + for _, b := range bytes { + if b <= 0x08 || + b == 0x0B || + 0x0E <= b && b <= 0x1A || + 0x1C <= b && b <= 0x1F { + return false + } + } + + return true +} diff --git a/internal/lefthook/runner/filters/detect_text_test.go b/internal/lefthook/runner/filters/detect_text_test.go new file mode 100644 index 00000000..4a31b281 --- /dev/null +++ b/internal/lefthook/runner/filters/detect_text_test.go @@ -0,0 +1,56 @@ +package filters + +import ( + "fmt" + "testing" +) + +func TestDetectText(t *testing.T) { + for i, tt := range [...]struct { + bytes []byte + result bool + }{ + { + bytes: []byte{}, + result: true, + }, + { + bytes: []byte{0xEF, 0xBB, 0xBF}, // utf-8 BOM + result: true, + }, + { + bytes: []byte{0x00, 0x00, 0xFE, 0xFF}, // utf-32be BOM + result: true, + }, + { + bytes: []byte{0xFF, 0xFE, 0x00, 0x00}, // utf-32le BOM + result: true, + }, + { + bytes: []byte{0xFE, 0xFF}, // utf-16be BOM + result: true, + }, + { + bytes: []byte{0xFF, 0xFE}, // utf-16le BOM + result: true, + }, + { + bytes: []byte{0xFA, 0xCF, 0xFE, 0xED, 0x00, 0x0C}, + result: false, + }, + { + bytes: []byte{0x70, 0x5B, 0x65, 0x72, 0x63, 0x2D}, // .lefthook.toml + result: true, + }, + { + bytes: []byte{0x5B, 0x21, 0x75, 0x42, 0x6C, 0x69, 0x20, 0x64, 0x74, 0x53, 0x74, 0x61, 0x73, 0x75, 0x28, 0x5D}, // README.md + result: true, + }, + } { + t.Run(fmt.Sprintf("#%d:", i), func(t *testing.T) { + if detectText(tt.bytes) != tt.result { + t.Error("results don't match; expected", tt.result) + } + }) + } +} diff --git a/internal/lefthook/runner/filters/filters.go b/internal/lefthook/runner/filters/filters.go new file mode 100644 index 00000000..b82cfe71 --- /dev/null +++ b/internal/lefthook/runner/filters/filters.go @@ -0,0 +1,192 @@ +package filters + +import ( + "errors" + "io" + "os" + "regexp" + "strings" + + "github.com/gobwas/glob" + "github.com/spf13/afero" + + "github.com/evilmartians/lefthook/internal/config" + "github.com/evilmartians/lefthook/internal/log" +) + +type typeMask int + +const ( + typeExecutable typeMask = 1 << iota + typeNotExecutable + typeSymlink + typeNotSymlink + typeText + typeBinary + + detectTypes = typeText | typeBinary + detectBufSize = 1024 + executableMask = 0o111 +) + +func Apply(fs afero.Fs, command *config.Command, files []string) []string { + if len(files) == 0 { + return nil + } + + log.Debug("[lefthook] files before filters:\n", files) + + files = byGlob(files, command.Glob) + files = byExclude(files, command.Exclude) + files = byRoot(files, command.Root) + files = byType(fs, files, command.FileTypes) + + log.Debug("[lefthook] files after filters:\n", files) + + return files +} + +func byGlob(vs []string, matcher string) []string { + if matcher == "" { + return vs + } + + g := glob.MustCompile(strings.ToLower(matcher)) + + vsf := make([]string, 0) + for _, v := range vs { + if res := g.Match(strings.ToLower(v)); res { + vsf = append(vsf, v) + } + } + return vsf +} + +func byExclude(vs []string, matcher string) []string { + if matcher == "" { + return vs + } + + vsf := make([]string, 0) + for _, v := range vs { + if res, _ := regexp.MatchString(matcher, v); !res { + vsf = append(vsf, v) + } + } + return vsf +} + +func byRoot(vs []string, matcher string) []string { + if matcher == "" { + return vs + } + + vsf := make([]string, 0) + for _, v := range vs { + if strings.HasPrefix(v, matcher) { + vsf = append(vsf, strings.Replace(v, matcher, "./", 1)) + } + } + return vsf +} + +func byType(fs afero.Fs, vs []string, types []string) []string { + if len(types) == 0 { + return vs + } + + mask := fillTypeMask(types) + + vsf := make([]string, 0) + for _, v := range vs { + var err error + var fileInfo os.FileInfo + lfs, ok := fs.(afero.Lstater) + if ok { + fileInfo, _, err = lfs.LstatIfPossible(v) + } else { + fileInfo, err = fs.Stat(v) + } + if err != nil { + log.Errorf("Couldn't check file type of %s: %s", v, err) + continue + } + + isSymlink := fileInfo.Mode()&os.ModeSymlink != 0 + isExecutable := fileInfo.Mode().Perm()&executableMask != 0 + if mask&typeSymlink != 0 && !isSymlink { + continue + } + if mask&typeNotSymlink != 0 && isSymlink { + continue + } + if mask&typeExecutable != 0 && (!isExecutable || isSymlink) { + continue + } + if mask&typeNotExecutable != 0 && (isExecutable && !isSymlink) { + continue + } + + if mask&detectTypes != 0 { + if !fileInfo.Mode().IsRegular() { + continue + } + + text := checkIsText(fs, v) + binary := !text + + if mask&typeText != 0 && binary { + continue + } + if mask&typeBinary != 0 && text { + continue + } + } + + vsf = append(vsf, v) + } + + return vsf +} + +func fillTypeMask(types []string) typeMask { + var mask typeMask + + for _, t := range types { + switch t { + case "executable": + mask |= typeExecutable + case "symlink": + mask |= typeSymlink + case "not executable": + mask |= typeNotExecutable + case "not symlink": + mask |= typeNotSymlink + case "binary": + mask |= typeBinary + case "text": + mask |= typeText + default: + log.Warn("Unknown filter type: ", t) + } + } + + return mask +} + +func checkIsText(fs afero.Fs, filepath string) bool { + file, err := fs.Open(filepath) + if err != nil { + log.Error("Couldn't open file for content detecting: ", err) + return false + } + + var buf []byte = make([]byte, detectBufSize) + n, err := io.ReadFull(file, buf) + if err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF) { + log.Error("Couldn't read file for content detecting: ", err) + return false + } + + return detectText(buf[:n]) +} diff --git a/internal/lefthook/runner/filter/filters_test.go b/internal/lefthook/runner/filters/filters_test.go similarity index 99% rename from internal/lefthook/runner/filter/filters_test.go rename to internal/lefthook/runner/filters/filters_test.go index 17ce2c8c..6995c813 100644 --- a/internal/lefthook/runner/filter/filters_test.go +++ b/internal/lefthook/runner/filters/filters_test.go @@ -1,4 +1,4 @@ -package filter +package filters import ( "fmt" diff --git a/internal/lefthook/runner/prepare_command.go b/internal/lefthook/runner/prepare_command.go index 288b07bb..e4e11378 100644 --- a/internal/lefthook/runner/prepare_command.go +++ b/internal/lefthook/runner/prepare_command.go @@ -8,7 +8,7 @@ import ( "gopkg.in/alessio/shellescape.v1" "github.com/evilmartians/lefthook/internal/config" - "github.com/evilmartians/lefthook/internal/lefthook/runner/filter" + "github.com/evilmartians/lefthook/internal/lefthook/runner/filters" "github.com/evilmartians/lefthook/internal/log" "github.com/evilmartians/lefthook/internal/system" ) @@ -107,7 +107,7 @@ func (r *Runner) buildRun(command *config.Command) (*run, error) { return nil, fmt.Errorf("error replacing %s: %w", filesType, err) } - files = filter.Apply(command, files) + files = filters.Apply(r.Repo.Fs, command, files) if !r.Force && len(files) == 0 { return nil, &skipError{"no files for inspection"} } @@ -124,7 +124,7 @@ func (r *Runner) buildRun(command *config.Command) (*run, error) { return nil, fmt.Errorf("error calling replace command for %s: %w", config.SubFiles, err) } - files = filter.Apply(command, files) + files = filters.Apply(r.Repo.Fs, command, files) if len(files) == 0 { return nil, &skipError{"no files for inspection"} @@ -142,7 +142,7 @@ func (r *Runner) buildRun(command *config.Command) (*run, error) { } if config.HookUsesStagedFiles(r.HookName) { - ok, err := canSkipCommand(command, templates[config.SubStagedFiles], r.Repo.StagedFiles) + ok, err := r.canSkipCommand(command, templates[config.SubStagedFiles], r.Repo.StagedFiles) if err != nil { return nil, err } @@ -152,7 +152,7 @@ func (r *Runner) buildRun(command *config.Command) (*run, error) { } if config.HookUsesPushFiles(r.HookName) { - ok, err := canSkipCommand(command, templates[config.SubPushFiles], r.Repo.PushFiles) + ok, err := r.canSkipCommand(command, templates[config.SubPushFiles], r.Repo.PushFiles) if err != nil { return nil, err } @@ -164,7 +164,7 @@ func (r *Runner) buildRun(command *config.Command) (*run, error) { return result, nil } -func canSkipCommand(command *config.Command, template *template, filesFn func() ([]string, error)) (bool, error) { +func (r *Runner) canSkipCommand(command *config.Command, template *template, filesFn func() ([]string, error)) (bool, error) { if template != nil { return len(template.files) == 0, nil } @@ -173,7 +173,7 @@ func canSkipCommand(command *config.Command, template *template, filesFn func() if err != nil { return false, fmt.Errorf("error getting files: %w", err) } - if len(filter.Apply(command, files)) == 0 { + if len(filters.Apply(r.Repo.Fs, command, files)) == 0 { return true, nil } diff --git a/internal/lefthook/runner/runner.go b/internal/lefthook/runner/runner.go index 80534882..80c1467c 100644 --- a/internal/lefthook/runner/runner.go +++ b/internal/lefthook/runner/runner.go @@ -23,7 +23,7 @@ import ( "github.com/evilmartians/lefthook/internal/config" "github.com/evilmartians/lefthook/internal/git" "github.com/evilmartians/lefthook/internal/lefthook/runner/exec" - "github.com/evilmartians/lefthook/internal/lefthook/runner/filter" + "github.com/evilmartians/lefthook/internal/lefthook/runner/filters" "github.com/evilmartians/lefthook/internal/log" ) @@ -465,7 +465,7 @@ func (r *Runner) runCommand(ctx context.Context, name string, command *config.Co return result } - files = filter.Apply(command, files) + files = filters.Apply(r.Repo.Fs, command, files) } if len(command.Root) > 0 { diff --git a/testdata/filter_by_file_type.txt b/testdata/filter_by_file_type.txt new file mode 100644 index 00000000..e0d61a16 --- /dev/null +++ b/testdata/filter_by_file_type.txt @@ -0,0 +1,57 @@ +[windows] skip + +exec git init +exec git config user.email "you@example.com" +exec git config user.name "Your Name" +exec lefthook install +chmod 777 executable +symlink symlink -> results +exec git add -A +exec git commit -m 'test' +exec lefthook run filters +stdout '.*all ❯\s+executable lefthook.yml results symlink\s+┃.*' +stdout '.*filter_text ❯\s+executable lefthook.yml results\s+┃.*' +stdout '.*filter_executable ❯\s+executable\s+┃.*' +stdout '.*filter_symlink ❯\s+symlink\s+┃.*' +stdout '.*filter_not_symlink ❯\s+executable lefthook.yml results\s+┃.*' +stdout '.*filter_not_executable ❯\s+lefthook.yml results symlink\s*' + +-- lefthook.yml -- +output: + - execution + - skips +filters: + piped: true + commands: + all: + run: echo {all_files} + priority: 1 + filter_text: + run: echo {all_files} + file_types: text + priority: 2 + filter_executable: + run: echo {all_files} + file_types: executable + priority: 3 + filter_symlink: + run: echo {all_files} + file_types: symlink + priority: 4 + filter_not_symlink: + run: echo {all_files} + file_types: not symlink + priority: 5 + filter_not_executable: + run: echo {all_files} + priority: 6 + file_types: + - not executable + +-- results -- +some text + +-- executable -- +#!/bin/sh + +echo 'Executable' diff --git a/testdata/remotes.txt b/testdata/remotes.txt index beffc20f..9836ba77 100644 --- a/testdata/remotes.txt +++ b/testdata/remotes.txt @@ -27,8 +27,8 @@ pre-commit: run: echo pong ruby-lint: run: bundle exec rubocop --force-exclusion --parallel '{files}' - glob: '*.rb' files: git diff-tree -r --name-only --diff-filter=CDMR HEAD origin/master + glob: '*.rb' ruby-test: run: bundle exec rspec skip: @@ -43,8 +43,8 @@ pre-push: commands: spelling: run: npx yaspeller {files} - glob: '*.md' files: git diff --name-only HEAD @{push} + glob: '*.md' remotes: - git_url: https://github.com/evilmartians/lefthook ref: v1.4.0