diff --git a/docs/configuration.md b/docs/configuration.md index bb46d8e7..4333f46b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -6,26 +6,23 @@ Lefthook [supports](#config-file) YAML, JSON, and TOML configuration. In this do - [Top level options](#top-level-options) - [`assert_lefthook_installed`](#assert_lefthook_installed) - [`colors`](#colors) - - [`yellow`](#colors) - - [`green`](#colors) - - [`cyan`](#colors) - - [`gray`](#colors) - - [`red`](#colors) + - [`no_tty`](#no_tty) - [`extends`](#extends) - [`min_version`](#min_version) - - [`no_tty`](#no_tty) - - [`rc`](#rc) - [`skip_output`](#skip_output) - [`source_dir`](#source_dir) - [`source_dir_local`](#source_dir_local) -- [`remote` (Beta :test_tube:)](#remote) + - [`rc`](#rc) +- [`remote`](#remote--deprecated-show-remotes-instead) :warning: DEPRECATED use [`remotes`](#remotes) - [`git_url`](#git_url) - [`ref`](#ref) - - [`config`](#config) -- [Hook](#git-hook) - - [`skip`](#skip) - - [`only`](#only) - - [`files`](#files-global) + - [`config`](#config--deprecated-use-configs-like-specified-in-remotes) +- [`remotes`](#remotes) + - [`git_url`](#git_url-1) + - [`ref`](#ref-1) + - [`configs`](#configs) +- [Git hook](#git-hook) + - [`files` (global)](#files-global) - [`parallel`](#parallel) - [`piped`](#piped) - [`follow`](#follow) @@ -34,6 +31,11 @@ Lefthook [supports](#config-file) YAML, JSON, and TOML configuration. In this do - [`scripts`](#scripts) - [Command](#command) - [`run`](#run) + - [`{files}` template](#files-template) + - [`{staged_files}` template](#staged_files-template) + - [`{push_files}` template](#push_files-template) + - [`{all_files}` template](#all_files-template) + - [`{cmd}` template](#cmd-template) - [`skip`](#skip) - [`only`](#only) - [`tags`](#tags) @@ -48,6 +50,7 @@ Lefthook [supports](#config-file) YAML, JSON, and TOML configuration. In this do - [`use_stdin`](#use_stdin) - [`priority`](#priority) - [Script](#script) + - [`use_stdin`](#use_stdin) - [`runner`](#runner) - [`skip`](#skip) - [`only`](#only) @@ -288,7 +291,7 @@ Now any program that runs your hooks will have a tweaked PATH environment variab ## `remote` -> :test_tube: This feature is in **Beta** version +> :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`. @@ -308,6 +311,8 @@ This can be changed in the future. For convenience, please use `remote` configur ### `git_url` +> :warning: DEPRECATED use [`remotes`](#remotes) setting + A URL to Git repository. It will be accessed with privileges of the machine lefthook runs on. **Example** @@ -330,6 +335,8 @@ remote: ### `ref` +> :warning: DEPRECATED use [`remotes`](#remotes) setting + An optional *branch* or *tag* name. **Example** @@ -348,6 +355,8 @@ remote: ### `config` +> :warning: DEPRECATED use [`remotes`](#remotes) setting + **Default:** `lefthook.yml` An optional config path from remote's root. @@ -363,6 +372,107 @@ remote: config: examples/ruby-linter.yml ``` +## `remotes` + +> :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`. + +You can use [`extends`](#extends) but the paths must be relative to the remote repository root. + +If you provide [`scripts`](#scripts) in a remote config file, the [scripts](#source_dir) folder must also be in the **root of the repository**. + +**Note** + +The configuration from `remotes` will be merged to the local config using the following priority: + +1. Local main config (`lefthook.yml`) +1. Remote configs (`remotes`) +1. Local overrides (`lefthook-local.yml`) + +This priority may be changed in the future. For convenience, if you use `remotes`, please don't configure any hooks. + +### `git_url` + +A URL to Git repository. It will be accessed with privileges of the machine lefthook runs on. + +**Example** + +```yml +# lefthook.yml + +remotes: + - git_url: git@github.com:evilmartians/lefthook +``` + +Or + +```yml +# lefthook.yml + +remotes: + - git_url: https://github.com/evilmartians/lefthook +``` + +### `ref` + +An optional *branch* or *tag* name. + +**Example** + +```yml +# lefthook.yml + +remotes: + - git_url: git@github.com:evilmartians/lefthook + ref: v1.0.0 +``` + +> :warning: **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. + +### `configs` + +**Default:** `[lefthook.yml]` + +An optional array of config paths from remote's root. + +**Example** + +```yml +# lefthook.yml + +remotes: + - git_url: git@github.com:evilmartians/lefthook + ref: v1.0.0 + configs: + - examples/ruby-linter.yml + - examples/test.yml +``` + +Example with multiple remotes merging multiple configurations. + +```yml +# lefthook.yml + +remotes: + - git_url: git@github.com:org/lefthook-configs + ref: v1.0.0 + configs: + - examples/ruby-linter.yml + - examples/test.yml + - git_url: https://github.com/org2/lefthook-configs + configs: + - lefthooks/pre_commit.yml + - lefthooks/post_merge.yml + - git_url: https://github.com/org3/lefthook-configs + ref: feature/new + configs: + - configs/pre-push.yml + +``` + ## Git hook Commands and scripts are defined for git hooks. You can defined a hook for all hooks listed in [this file](../internal/config/available_hooks.go). diff --git a/examples/remote/ping.yml b/examples/remote/ping.yml index 9d369a06..255afea0 100644 --- a/examples/remote/ping.yml +++ b/examples/remote/ping.yml @@ -1,10 +1,11 @@ -# Test `remote` config of lefthook. +# Test `remotes` config of lefthook. # # # lefthook.yml # -# remote: -# git_url: git@github.com:evilmartians/lefthook -# config: examples/remote/ping.yml +# remotes: +# - git_url: git@github.com:evilmartians/lefthook +# configs: +# - examples/remote/ping.yml # # $ lefthook run pre-commit diff --git a/internal/config/config.go b/internal/config/config.go index ea8ffd24..19c72e5f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -24,7 +24,10 @@ type Config struct { NoTTY bool `mapstructure:"no_tty,omitempty"` AssertLefthookInstalled bool `mapstructure:"assert_lefthook_installed,omitempty"` Colors interface{} `mapstructure:"colors,omitempty"` - Remote *Remote `mapstructure:"remote,omitempty"` + + // Deprecated: use Remotes + Remote *Remote `mapstructure:"remote,omitempty"` + Remotes []*Remote `mapstructure:"remotes,omitempty"` Hooks map[string]*Hook `mapstructure:"-"` } diff --git a/internal/config/load.go b/internal/config/load.go index 1e5b6b93..7c1f060d 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -94,8 +94,11 @@ func readOne(fs afero.Fs, path string, names []string) (*viper.Viper, error) { return nil, NotFoundError{fmt.Sprintf("No config files with names %q could not be found in \"%s\"", names, path)} } -// mergeAll merges (.lefthook or lefthook) and (extended config) and (remote) -// and (.lefthook-local or .lefthook-local) configs. +// mergeAll merges configs using the following order. +// - lefthook/.lefthook +// - files from `extends` +// - files from `remotes` +// - lefthook-local/.lefthook-local. func mergeAll(fs afero.Fs, repo *git.Repository) (*viper.Viper, error) { extends, err := readOne(fs, repo.RootPath, []string{"lefthook", ".lefthook"}) if err != nil { @@ -106,7 +109,7 @@ func mergeAll(fs afero.Fs, repo *git.Repository) (*viper.Viper, error) { return nil, err } - if err := mergeRemote(fs, repo, extends); err != nil { + if err := mergeRemotes(fs, repo, extends); err != nil { return nil, err } @@ -124,42 +127,70 @@ func mergeAll(fs afero.Fs, repo *git.Repository) (*viper.Viper, error) { return extends, nil } -// mergeRemote merges remote config to the current one. -func mergeRemote(fs afero.Fs, repo *git.Repository, v *viper.Viper) error { - var remote Remote - err := v.UnmarshalKey("remote", &remote) +// mergeRemotes merges remote configs to the current one. +func mergeRemotes(fs afero.Fs, repo *git.Repository, v *viper.Viper) error { + var remote *Remote // Deprecated + var remotes []*Remote + + err := v.UnmarshalKey("remotes", &remotes) if err != nil { return err } - if !remote.Configured() { - return nil + // Deprecated + err = v.UnmarshalKey("remote", &remote) + if err != nil { + return err } - remotePath := repo.RemoteFolder(remote.GitURL) - configFile := DefaultConfigName - if len(remote.Config) > 0 { - configFile = remote.Config + // Backward compatibility + if remote != nil { + remotes = append(remotes, remote) } - configPath := filepath.Join(remotePath, configFile) - log.Debugf("Merging remote config: %s", configPath) + for _, remote := range remotes { + if !remote.Configured() { + continue + } - _, err = fs.Stat(configPath) - if err != nil { - return nil - } + // Use for backward compatibility with "remote(s).config" + if remote.Config != "" { + remote.Configs = append(remote.Configs, remote.Config) + } - if err := merge("remote", configPath, v); err != nil { - return err - } + if len(remote.Configs) == 0 { + remote.Configs = append(remote.Configs, DefaultConfigName) + } - if err := extend(v, filepath.Dir(configPath)); err != nil { - return err + for _, config := range remote.Configs { + remotePath := repo.RemoteFolder(remote.GitURL, remote.Ref) + configFile := config + configPath := filepath.Join(remotePath, configFile) + + log.Debugf("Merging remote config: %s: %s", remote.GitURL, configPath) + + _, err = fs.Stat(configPath) + if err != nil { + continue + } + + if err = merge("remotes", configPath, v); err != nil { + return err + } + + if err = extend(v, filepath.Dir(configPath)); err != nil { + return err + } + } + + // Reset extends to omit issues when extending with remote extends. + err = v.MergeConfigMap(map[string]interface{}{"extends": nil}) + if err != nil { + return err + } } - // Reset extends to omit issues when extending with remote extends. - return v.MergeConfigMap(map[string]interface{}{"extends": nil}) + return nil } // extend merges all files listed in 'extends' option into the config. @@ -234,7 +265,28 @@ func unmarshalConfigs(base, extra *viper.Viper, c *Config) error { return err } - return base.Unmarshal(c) + if err := base.Unmarshal(c); err != nil { + return err + } + + // Deprecation handling + + if c.Remote != nil { + log.Warn("DEPRECATED: \"remote\" option is deprecated and will be omitted in the next major release, use \"remotes\" option instead") + c.Remotes = append(c.Remotes, c.Remote) + } + c.Remote = nil + + for _, remote := range c.Remotes { + if remote.Config != "" { + log.Warn("DEPRECATED: \"remotes\".\"config\" option is deprecated and will be omitted in the next major release, use \"configs\" option instead") + remote.Configs = append(remote.Configs, remote.Config) + } + + remote.Config = "" + } + + return nil } func addHook(hookName string, base, extra *viper.Viper, c *Config) error { diff --git a/internal/config/load_test.go b/internal/config/load_test.go index fe9e1d6f..76f0aa43 100644 --- a/internal/config/load_test.go +++ b/internal/config/load_test.go @@ -13,6 +13,7 @@ import ( "github.com/evilmartians/lefthook/internal/git" ) +//gocyclo:ignore func TestLoad(t *testing.T) { root, err := filepath.Abs("") if err != nil { @@ -432,8 +433,10 @@ pre-commit: SourceDir: DefaultSourceDir, SourceDirLocal: DefaultSourceDirLocal, Colors: nil, - Remote: &Remote{ - GitURL: "git@github.com:evilmartians/lefthook", + Remotes: []*Remote{ + { + GitURL: "git@github.com:evilmartians/lefthook", + }, }, Hooks: map[string]*Hook{ "pre-commit": { @@ -482,15 +485,17 @@ pre-commit: - merge runner: bash `, - remoteConfigPath: filepath.Join(root, ".git", "info", "lefthook-remotes", "lefthook", "examples", "custom.yml"), + remoteConfigPath: filepath.Join(root, ".git", "info", "lefthook-remotes", "lefthook-v1.0.0", "examples", "custom.yml"), result: &Config{ SourceDir: DefaultSourceDir, SourceDirLocal: DefaultSourceDirLocal, Colors: nil, - Remote: &Remote{ - GitURL: "git@github.com:evilmartians/lefthook", - Ref: "v1.0.0", - Config: "examples/custom.yml", + Remotes: []*Remote{ + { + GitURL: "git@github.com:evilmartians/lefthook", + Ref: "v1.0.0", + Configs: []string{"examples/custom.yml"}, + }, }, Hooks: map[string]*Hook{ "pre-commit": { @@ -572,9 +577,11 @@ pre-push: SourceDir: DefaultSourceDir, SourceDirLocal: DefaultSourceDirLocal, Colors: nil, - Remote: &Remote{ - GitURL: "https://github.com/evilmartians/lefthook", - Config: "examples/config.yml", + Remotes: []*Remote{ + { + GitURL: "https://github.com/evilmartians/lefthook", + Configs: []string{"examples/config.yml"}, + }, }, Extends: []string{"local-extend.yml"}, Hooks: map[string]*Hook{ @@ -763,4 +770,164 @@ run = "echo 1" } }) } + + type remote struct { + RemoteConfigPath string + Content string + } + for i, tt := range [...]struct { + name string + global, local string + remotes []remote + otherFiles map[string]string + result *Config + }{ + { + name: "with remotes, config and configs", + global: ` +pre-commit: + only: + - ref: main + commands: + global: + run: echo 'Global!' + lint: + run: this will be overwritten +remotes: + - git_url: https://github.com/evilmartians/lefthook + ref: v1.0.0 + config: examples/custom.yml + - git_url: https://github.com/evilmartians/lefthook + configs: + - examples/remote/ping.yml + ref: v1.5.5 +`, + remotes: []remote{ + { + RemoteConfigPath: filepath.Join(root, ".git", "info", "lefthook-remotes", "lefthook-v1.0.0", "examples", "custom.yml"), + Content: ` +pre-commit: + commands: + lint: + only: + - merge + - rebase + run: yarn lint + scripts: + "test.sh": + skip: + - merge + runner: bash +`, + }, + { + RemoteConfigPath: filepath.Join(root, ".git", "info", "lefthook-remotes", "lefthook-v1.5.5", "examples", "remote", "ping.yml"), + Content: ` +pre-commit: + commands: + ping: + run: echo pong +`, + }, + }, + result: &Config{ + SourceDir: DefaultSourceDir, + SourceDirLocal: DefaultSourceDirLocal, + Colors: nil, + Remotes: []*Remote{ + { + GitURL: "https://github.com/evilmartians/lefthook", + Ref: "v1.0.0", + Configs: []string{"examples/custom.yml"}, + }, + { + GitURL: "https://github.com/evilmartians/lefthook", + Ref: "v1.5.5", + Configs: []string{ + "examples/remote/ping.yml", + }, + }, + }, + Hooks: map[string]*Hook{ + "pre-commit": { + Only: []interface{}{map[string]interface{}{"ref": "main"}}, + Commands: map[string]*Command{ + "lint": { + Run: "yarn lint", + Only: []interface{}{"merge", "rebase"}, + }, + "ping": { + Run: "echo pong", + }, + "global": { + Run: "echo 'Global!'", + }, + }, + Scripts: map[string]*Script{ + "test.sh": { + Runner: "bash", + Skip: []interface{}{"merge"}, + }, + }, + }, + }, + }, + }, + } { + fs := afero.Afero{Fs: afero.NewMemMapFs()} + repo := &git.Repository{ + Fs: fs, + RootPath: root, + InfoPath: filepath.Join(root, ".git", "info"), + } + + t.Run(fmt.Sprintf("%d: %s", i, tt.name), func(t *testing.T) { + if tt.global != "" { + if err := fs.WriteFile(filepath.Join(root, "lefthook.yml"), []byte(tt.global), 0o644); err != nil { + t.Errorf("unexpected error: %s", err) + } + } + + if tt.local != "" { + if err := fs.WriteFile(filepath.Join(root, "lefthook-local.yml"), []byte(tt.local), 0o644); err != nil { + t.Errorf("unexpected error: %s", err) + } + } + + for _, remote := range tt.remotes { + if err := fs.MkdirAll(filepath.Base(remote.RemoteConfigPath), 0o755); err != nil { + t.Errorf("unexpected error: %s", err) + } + + if err := fs.WriteFile(remote.RemoteConfigPath, []byte(remote.Content), 0o644); err != nil { + t.Errorf("unexpected error: %s", err) + } + } + + for name, content := range tt.otherFiles { + path := filepath.Join( + root, + filepath.Join(strings.Split(name, "/")...), + ) + dir := filepath.Dir(path) + + if err := fs.MkdirAll(dir, 0o775); err != nil { + t.Errorf("unexpected error: %s", err) + } + + if err := fs.WriteFile(path, []byte(content), 0o644); err != nil { + t.Errorf("unexpected error: %s", err) + } + } + + checkConfig, err := Load(fs.Fs, repo) + + if err != nil { + t.Errorf("should parse configs without errors: %s", err) + } else if !cmp.Equal(checkConfig, tt.result, cmpopts.IgnoreUnexported(Hook{})) { + t.Errorf("configs should be equal") + t.Errorf("(-want +got):\n%s", cmp.Diff(tt.result, checkConfig)) + } + }) + } } diff --git a/internal/config/remote.go b/internal/config/remote.go index b8155529..ebdfca57 100644 --- a/internal/config/remote.go +++ b/internal/config/remote.go @@ -3,7 +3,9 @@ package config type Remote struct { GitURL string `mapstructure:"git_url" yaml:"git_url" json:"git_url,omitempty" toml:"git_url"` Ref string `mapstructure:"ref,omitempty" yaml:",omitempty" json:"ref,omitempty" toml:"ref,omitempty"` - Config string `mapstructure:"config,omitempty" yaml:",omitempty" json:"config,omitempty" toml:"config,omitempty"` + // Deprecated + Config string `mapstructure:"config,omitempty" yaml:",omitempty" json:"config,omitempty" toml:"config,omitempty"` + Configs []string `mapstructure:"configs,omitempty" yaml:",omitempty" json:"configs,omitempty" toml:"configs,omitempty"` } func (r *Remote) Configured() bool { diff --git a/internal/git/remote.go b/internal/git/remote.go index be2bd01f..ae72f734 100644 --- a/internal/git/remote.go +++ b/internal/git/remote.go @@ -16,12 +16,10 @@ const ( // RemoteFolder returns the path to the folder where the remote // repository is located. -func (r *Repository) RemoteFolder(url string) string { +func (r *Repository) RemoteFolder(url string, ref string) string { return filepath.Join( r.RemotesFolder(), - filepath.Base( - strings.TrimSuffix(url, filepath.Ext(url)), - ), + remoteDirectoryName(url, ref), ) } @@ -34,26 +32,22 @@ func (r *Repository) RemotesFolder() string { // specified as a remote config repository. If successful, the path to the root // of the repository will be returned. func (r *Repository) SyncRemote(url, ref string) error { - remotesPath := filepath.Join(r.InfoPath, remotesFolder) + remotesPath := r.RemotesFolder() err := r.Fs.MkdirAll(remotesPath, remotesFolderMode) if err != nil && !errors.Is(err, os.ErrExist) { return err } - remotePath := filepath.Join( - remotesPath, - filepath.Base( - strings.TrimSuffix(url, filepath.Ext(url)), - ), - ) + directoryName := remoteDirectoryName(url, ref) + remotePath := filepath.Join(remotesPath, directoryName) _, err = r.Fs.Stat(remotePath) if err == nil { return r.updateRemote(remotePath, ref) } - return r.cloneRemote(remotesPath, url, ref) + return r.cloneRemote(remotesPath, directoryName, url, ref) } func (r *Repository) updateRemote(path, ref string) error { @@ -84,14 +78,14 @@ func (r *Repository) updateRemote(path, ref string) error { return nil } -func (r *Repository) cloneRemote(path, url, ref string) error { - log.Debugf("Cloning remote config repository: %v", path) +func (r *Repository) cloneRemote(dest, directoryName, url, ref string) error { + log.Debugf("Cloning remote config repository: %v/%v", dest, directoryName) - cmdClone := []string{"git", "-C", path, "clone", "--quiet", "--depth", "1"} + cmdClone := []string{"git", "-C", dest, "clone", "--quiet", "--depth", "1"} if len(ref) > 0 { cmdClone = append(cmdClone, "--branch", ref) } - cmdClone = append(cmdClone, url) + cmdClone = append(cmdClone, url, directoryName) _, err := r.Git.Cmd(cmdClone) if err != nil { @@ -100,3 +94,15 @@ func (r *Repository) cloneRemote(path, url, ref string) error { return nil } + +func remoteDirectoryName(url, ref string) string { + name := filepath.Base( + strings.TrimSuffix(url, filepath.Ext(url)), + ) + + if ref != "" { + name = name + "-" + ref + } + + return name +} diff --git a/internal/lefthook/install.go b/internal/lefthook/install.go index 05627c39..1724a97e 100644 --- a/internal/lefthook/install.go +++ b/internal/lefthook/install.go @@ -50,14 +50,16 @@ func (l *Lefthook) Install(force bool) error { return err } - if cfg.Remote.Configured() { - if err := l.repo.SyncRemote(cfg.Remote.GitURL, cfg.Remote.Ref); err != nil { - log.Warnf("Couldn't sync remotes. Will continue without them: %s", err) - } else { - // Reread the config file with synced remotes - cfg, err = l.readOrCreateConfig() - if err != nil { - return err + for _, remote := range cfg.Remotes { + if remote.Configured() { + if err := l.repo.SyncRemote(remote.GitURL, remote.Ref); err != nil { + log.Warnf("Couldn't sync remotes. Will continue without them: %s", err) + } else { + // Reread the config file with synced remotes + cfg, err = l.readOrCreateConfig() + if err != nil { + return err + } } } } diff --git a/internal/lefthook/run.go b/internal/lefthook/run.go index 2b548a7b..42b45801 100644 --- a/internal/lefthook/run.go +++ b/internal/lefthook/run.go @@ -146,15 +146,17 @@ Run 'lefthook install' manually.`, filepath.Join(l.repo.RootPath, cfg.SourceDirLocal), } - if cfg.Remote.Configured() { - // Append only source_dir, because source_dir_local doesn't make sense - sourceDirs = append( - sourceDirs, - filepath.Join( - l.repo.RemoteFolder(cfg.Remote.GitURL), - cfg.SourceDir, - ), - ) + for _, remote := range cfg.Remotes { + if remote.Configured() { + // Append only source_dir, because source_dir_local doesn't make sense + sourceDirs = append( + sourceDirs, + filepath.Join( + l.repo.RemoteFolder(remote.GitURL, remote.Ref), + cfg.SourceDir, + ), + ) + } } ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) diff --git a/testdata/remote.txt b/testdata/remote.txt index de39429e..72435af1 100644 --- a/testdata/remote.txt +++ b/testdata/remote.txt @@ -11,11 +11,14 @@ remote: ref: v1.4.0 -- lefthook-dump.yml -- +DEPRECATED: "remote" option is deprecated and will be omitted in the next major release, use "remotes" option instead +DEPRECATED: "remotes"."config" option is deprecated and will be omitted in the next major release, use "configs" option instead pre-commit: scripts: good_job.js: runner: node -remote: - config: examples/with_scripts/lefthook.yml - git_url: https://github.com/evilmartians/lefthook - ref: v1.4.0 +remotes: + - git_url: https://github.com/evilmartians/lefthook + ref: v1.4.0 + configs: + - examples/with_scripts/lefthook.yml diff --git a/testdata/remotes.txt b/testdata/remotes.txt new file mode 100644 index 00000000..7858b6e3 --- /dev/null +++ b/testdata/remotes.txt @@ -0,0 +1,54 @@ +exec git init +exec lefthook install + +exec lefthook dump +cmp stdout lefthook-dump.yml + +-- lefthook.yml -- +remotes: + - git_url: https://github.com/evilmartians/lefthook + config: examples/with_scripts/lefthook.yml + ref: v1.4.0 + - git_url: https://github.com/evilmartians/lefthook + configs: + - examples/verbose/lefthook.yml + - examples/remote/ping.yml + +-- lefthook-dump.yml -- +DEPRECATED: "remotes"."config" option is deprecated and will be omitted in the next major release, use "configs" option instead +pre-commit: + commands: + js-lint: + run: npx eslint --fix {staged_files} && git add {staged_files} + glob: '*.{js,ts}' + ping: + 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 + ruby-test: + run: bundle exec rspec + skip: + - merge + - rebase + fail_text: Run bundle install + scripts: + good_job.js: + runner: node + parallel: true +pre-push: + commands: + spelling: + run: npx yaspeller {files} + glob: '*.md' + files: git diff --name-only HEAD @{push} +remotes: + - git_url: https://github.com/evilmartians/lefthook + ref: v1.4.0 + configs: + - examples/with_scripts/lefthook.yml + - git_url: https://github.com/evilmartians/lefthook + configs: + - examples/verbose/lefthook.yml + - examples/remote/ping.yml