From 7e774af1d8a59a796f66d24a922cfd94f7c24264 Mon Sep 17 00:00:00 2001 From: Nikita COEUR Date: Fri, 12 Jan 2024 22:39:21 +0100 Subject: [PATCH] feat(remotes): add remotes and configs --- docs/configuration.md | 220 +++++++++++++++++++++++++---------- examples/remote/ping.yml | 9 +- internal/config/config.go | 3 +- internal/config/load.go | 83 +++++++++---- internal/config/load_test.go | 163 +++++++++++++++++++++++++- internal/config/remote.go | 7 +- internal/git/remote.go | 36 ++++-- internal/lefthook/install.go | 23 ++-- internal/lefthook/run.go | 25 ++-- testdata/remotes.txt | 52 +++++++++ 10 files changed, 499 insertions(+), 122 deletions(-) create mode 100644 testdata/remotes.txt diff --git a/docs/configuration.md b/docs/configuration.md index bb46d8e7..24c42bf1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -2,63 +2,63 @@ Lefthook [supports](#config-file) YAML, JSON, and TOML configuration. In this document `lefthook.yml` is used for simplicity. -- [Config file](#config-file) -- [Top level options](#top-level-options) - - [`assert_lefthook_installed`](#assert_lefthook_installed) - - [`colors`](#colors) - - [`yellow`](#colors) - - [`green`](#colors) - - [`cyan`](#colors) - - [`gray`](#colors) - - [`red`](#colors) - - [`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) - - [`git_url`](#git_url) - - [`ref`](#ref) - - [`config`](#config) -- [Hook](#git-hook) - - [`skip`](#skip) - - [`only`](#only) - - [`files`](#files-global) - - [`parallel`](#parallel) - - [`piped`](#piped) - - [`follow`](#follow) - - [`exclude_tags`](#exclude_tags) - - [`commands`](#commands) - - [`scripts`](#scripts) -- [Command](#command) - - [`run`](#run) - - [`skip`](#skip) - - [`only`](#only) - - [`tags`](#tags) - - [`glob`](#glob) - - [`files`](#files) - - [`env`](#env) - - [`root`](#root) - - [`exclude`](#exclude) - - [`fail_text`](#fail_text) - - [`stage_fixed`](#stage_fixed) - - [`interactive`](#interactive) - - [`use_stdin`](#use_stdin) - - [`priority`](#priority) -- [Script](#script) - - [`runner`](#runner) - - [`skip`](#skip) - - [`only`](#only) - - [`tags`](#tags) - - [`env`](#env) - - [`fail_text`](#fail_text) - - [`stage_fixed`](#stage_fixed) - - [`interactive`](#interactive) - - [`use_stdin`](#use_stdin) -- [Examples](#examples) -- [More info](#more-info) +- [Configure lefthook](#configure-lefthook) + - [Config file](#config-file) + - [Top level options](#top-level-options) + - [`assert_lefthook_installed`](#assert_lefthook_installed) + - [`colors`](#colors) + - [`no_tty`](#no_tty) + - [`extends`](#extends) + - [`min_version`](#min_version) + - [`skip_output`](#skip_output) + - [`source_dir`](#source_dir) + - [`source_dir_local`](#source_dir_local) + - [`rc`](#rc) + - [`remote` // DEPRECATED show remotes instead](#remote--deprecated-show-remotes-instead) + - [`git_url`](#git_url) + - [`ref`](#ref) + - [`config` // DEPRECATED use configs like specified in `remotes`](#config--deprecated-use-configs-like-specified-in-remotes) + - [`remotes` (Replace `remote`)](#remotes-replace-remote) + - [`git_url`](#git_url-1) + - [`ref`](#ref-1) + - [`configs`](#configs) + - [Remotes full example :](#remotes-full-example-) + - [Git hook](#git-hook) + - [`files` (global)](#files-global) + - [`parallel`](#parallel) + - [`piped`](#piped) + - [`follow`](#follow) + - [`exclude_tags`](#exclude_tags) + - [`commands`](#commands) + - [`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) + - [Git arguments](#git-arguments) + - [Rubocop](#rubocop) + - [Quotes](#quotes) + - [`skip`](#skip) + - [`only`](#only) + - [`tags`](#tags) + - [`glob`](#glob) + - [`files`](#files) + - [`env`](#env) + - [Extending PATH](#extending-path) + - [`root`](#root) + - [`exclude`](#exclude) + - [`fail_text`](#fail_text) + - [`stage_fixed`](#stage_fixed) + - [`interactive`](#interactive) + - [`priority`](#priority) + - [Script](#script) + - [`use_stdin`](#use_stdin) + - [`runner`](#runner) + - [Examples](#examples) + - [More info](#more-info) ---- @@ -286,7 +286,7 @@ $ lefthook install -f Now any program that runs your hooks will have a tweaked PATH environment variable and will be able to get `nvm` :wink: -## `remote` +## `remote` // DEPRECATED show remotes instead > :test_tube: This feature is in **Beta** version @@ -346,7 +346,7 @@ remote: > > :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. -### `config` +### `config` // DEPRECATED use configs like specified in `remotes` **Default:** `lefthook.yml` @@ -363,6 +363,108 @@ remote: config: examples/ruby-linter.yml ``` +## `remotes` (Replace `remote`) + +> :test_tube: This feature is in **Beta** version + +You can provide multiple remotes 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) related to the config file (not absolute paths). + +If you provide [`scripts`](#scripts) in a remote file, the [scripts](#source_dir) folder must be in the **root of the repository**. + +**Note** + +Configuration in `remotes` will be merged to configuration in `lefthook.yml`, so the priority will be the following: + +- `lefthook.yml` +- `remotes` +- `lefthook-local.yml` + +This can be changed in the future. For convenience, please use `remotes` configuration without any hooks configuration in `lefthook.yml`. + +### `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 +``` + +> **Note** +> +> :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. + +### `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/remote + ref: v1.0.0 + configs: + - examples/ruby-linter.yml + - examples/test.yml +``` + +### Remotes full example : + +A more complete example here : +```yml +# lefthook.yml + +remotes: + - git_url: git@github.com:evilmartians/remote + ref: v1.0.0 + configs: + - examples/ruby-linter.yml + - examples/test.yml + - git_url : https://github.com:example/repository + configs: + - lefthooks/pre_commit.yml + - lefthooks/post_merge.yml + - git_url : https://github.com:example2/repository2 + ref: specific_branch + configs: + - example/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..9c31a3f1 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -24,7 +24,8 @@ 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"` + Remote *Remote `mapstructure:"remote,omitempty"` // Deprecated in favor of Remotes + Remotes []*Remote `mapstructure:"remotes,omitempty"` Hooks map[string]*Hook `mapstructure:"-"` } diff --git a/internal/config/load.go b/internal/config/load.go index 1e5b6b93..0c593759 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -94,7 +94,7 @@ 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) +// mergeAll merges (.lefthook or lefthook) and (extended config) and (remotes) // and (.lefthook-local or .lefthook-local) configs. func mergeAll(fs afero.Fs, repo *git.Repository) (*viper.Viper, error) { extends, err := readOne(fs, repo.RootPath, []string{"lefthook", ".lefthook"}) @@ -106,7 +106,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 +124,75 @@ 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 remotes config to the current one. +func mergeRemotes(fs afero.Fs, repo *git.Repository, v *viper.Viper) error { + var remotes []*Remote + var remote *Remote // Use for backward compatibility + + err := v.UnmarshalKey("remotes", &remotes) if err != nil { return err } - if !remote.Configured() { - return nil + // Use for backward compatibility + 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 + // Use for backward compatibility + // If "remote" key exists, append it to "remotes" + if remote != nil { + // Not logged because it's breaking tests + // log.Warn("DEPRECATED: \"remote\" key is deprecated, use \"remotes\" instead") + 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" instead of "remote(s).configs" + if remote.Config != "" { + // Not logged because it's breaking tests + // log.Warn("DEPRECATED: \"config\" key is deprecated, use \"configs\" instead for remotes") + 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", 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. diff --git a/internal/config/load_test.go b/internal/config/load_test.go index fe9e1d6f..4935d361 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 { @@ -482,7 +483,7 @@ 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, @@ -763,4 +764,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", + Config: "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..b45e933a 100644 --- a/internal/config/remote.go +++ b/internal/config/remote.go @@ -1,9 +1,10 @@ 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"` + 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 in favor of Configs + 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..475140bf 100644 --- a/internal/git/remote.go +++ b/internal/git/remote.go @@ -16,12 +16,18 @@ 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 { + directoryName := filepath.Base( + strings.TrimSuffix(url, filepath.Ext(url)), + ) + + if ref != "" { + directoryName = directoryName + "-" + ref + } + return filepath.Join( r.RemotesFolder(), - filepath.Base( - strings.TrimSuffix(url, filepath.Ext(url)), - ), + directoryName, ) } @@ -41,11 +47,17 @@ func (r *Repository) SyncRemote(url, ref string) error { return err } + directoryName := filepath.Base( + strings.TrimSuffix(url, filepath.Ext(url)), + ) + + if ref != "" { + directoryName = directoryName + "-" + ref + } + remotePath := filepath.Join( remotesPath, - filepath.Base( - strings.TrimSuffix(url, filepath.Ext(url)), - ), + directoryName, ) _, err = r.Fs.Stat(remotePath) @@ -53,7 +65,7 @@ func (r *Repository) SyncRemote(url, ref string) error { 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 +96,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(cwd, directoryName, url, ref string) error { + log.Debugf("Cloning remote config repository: %v/%v", cwd, directoryName) - cmdClone := []string{"git", "-C", path, "clone", "--quiet", "--depth", "1"} + cmdClone := []string{"git", "-C", cwd, "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 { diff --git a/internal/lefthook/install.go b/internal/lefthook/install.go index 05627c39..98dbbeec 100644 --- a/internal/lefthook/install.go +++ b/internal/lefthook/install.go @@ -50,14 +50,21 @@ 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 backward compatibility with single remote config + if cfg.Remote != nil { + cfg.Remotes = append(cfg.Remotes, cfg.Remote) + } + + 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 da16fcb6..be51c640 100644 --- a/internal/lefthook/run.go +++ b/internal/lefthook/run.go @@ -131,15 +131,22 @@ 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 backward compatibility with single remote config + if cfg.Remote != nil { + cfg.Remotes = append(cfg.Remotes, cfg.Remote) + } + + 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/remotes.txt b/testdata/remotes.txt new file mode 100644 index 00000000..0870bee7 --- /dev/null +++ b/testdata/remotes.txt @@ -0,0 +1,52 @@ +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 -- +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 + config: examples/with_scripts/lefthook.yml + - git_url: https://github.com/evilmartians/lefthook + configs: + - examples/verbose/lefthook.yml + - examples/remote/ping.yml