From 7e774af1d8a59a796f66d24a922cfd94f7c24264 Mon Sep 17 00:00:00 2001 From: Nikita COEUR Date: Fri, 12 Jan 2024 22:39:21 +0100 Subject: [PATCH 1/3] 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 From 79defa83bbe92202b79c0ca267faf877065ea061 Mon Sep 17 00:00:00 2001 From: Valentin Kiselev Date: Wed, 17 Jan 2024 17:27:49 +0300 Subject: [PATCH 2/3] docs: chore in the docs --- docs/configuration.md | 134 ++++++++++++++++++++++-------------------- 1 file changed, 71 insertions(+), 63 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 24c42bf1..a72fac0e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -2,63 +2,66 @@ Lefthook [supports](#config-file) YAML, JSON, and TOML configuration. In this document `lefthook.yml` is used for simplicity. -- [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) +- [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`](#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) +- [`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) + - [`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) + - [`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) + - [`use_stdin`](#use_stdin) + - [`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) ---- @@ -286,9 +289,9 @@ $ 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` // DEPRECATED show remotes instead +## `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** @@ -346,7 +353,9 @@ 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` // DEPRECATED use configs like specified in `remotes` +### `config` + +> :warning: DEPRECATED use [`remotes`](#remotes) setting **Default:** `lefthook.yml` @@ -363,7 +372,7 @@ remote: config: examples/ruby-linter.yml ``` -## `remotes` (Replace `remote`) +## `remotes` > :test_tube: This feature is in **Beta** version @@ -442,9 +451,8 @@ remotes: - examples/test.yml ``` -### Remotes full example : +More complicated example. -A more complete example here : ```yml # lefthook.yml From 1bf3f7983503582e8c6d5a10f56f702ded4b043c Mon Sep 17 00:00:00 2001 From: Valentin Kiselev Date: Thu, 18 Jan 2024 12:06:46 +0300 Subject: [PATCH 3/3] fix: some small improvements and deprecation logs --- docs/configuration.md | 36 +++++++++++++-------------- internal/config/config.go | 6 +++-- internal/config/load.go | 47 +++++++++++++++++++++++++----------- internal/config/load_test.go | 30 ++++++++++++++--------- internal/config/remote.go | 7 +++--- internal/git/remote.go | 44 +++++++++++++++------------------ internal/lefthook/install.go | 5 ---- internal/lefthook/run.go | 5 ---- testdata/remote.txt | 11 ++++++--- testdata/remotes.txt | 4 ++- 10 files changed, 106 insertions(+), 89 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index a72fac0e..4333f46b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -376,21 +376,21 @@ 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 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) related to the config file (not absolute paths). +You can use [`extends`](#extends) but the paths must be relative to the remote repository root. -If you provide [`scripts`](#scripts) in a remote file, the [scripts](#source_dir) folder must be in the **root of the repository**. +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** -Configuration in `remotes` will be merged to configuration in `lefthook.yml`, so the priority will be the following: +The configuration from `remotes` will be merged to the local config using the following priority: -- `lefthook.yml` -- `remotes` -- `lefthook-local.yml` +1. Local main config (`lefthook.yml`) +1. Remote configs (`remotes`) +1. Local overrides (`lefthook-local.yml`) -This can be changed in the future. For convenience, please use `remotes` configuration without any hooks configuration in `lefthook.yml`. +This priority may be changed in the future. For convenience, if you use `remotes`, please don't configure any hooks. ### `git_url` @@ -428,13 +428,13 @@ remotes: ref: v1.0.0 ``` -> **Note** +> :warning: **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. +> 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` +**Default:** `[lefthook.yml]` An optional array of config paths from remote's root. @@ -444,32 +444,32 @@ An optional array of config paths from remote's root. # lefthook.yml remotes: - - git_url: git@github.com:evilmartians/remote + - git_url: git@github.com:evilmartians/lefthook ref: v1.0.0 configs: - examples/ruby-linter.yml - examples/test.yml ``` -More complicated example. +Example with multiple remotes merging multiple configurations. ```yml # lefthook.yml remotes: - - git_url: git@github.com:evilmartians/remote + - 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:example/repository + - git_url: https://github.com/org2/lefthook-configs configs: - lefthooks/pre_commit.yml - lefthooks/post_merge.yml - - git_url : https://github.com:example2/repository2 - ref: specific_branch + - git_url: https://github.com/org3/lefthook-configs + ref: feature/new configs: - - example/pre-push.yml + - configs/pre-push.yml ``` diff --git a/internal/config/config.go b/internal/config/config.go index 9c31a3f1..19c72e5f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -24,8 +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 in favor of Remotes - Remotes []*Remote `mapstructure:"remotes,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 0c593759..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 (remotes) -// 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 { @@ -124,27 +127,24 @@ func mergeAll(fs afero.Fs, repo *git.Repository) (*viper.Viper, error) { return extends, nil } -// mergeRemotes merges remotes config to the current one. +// 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 - var remote *Remote // Use for backward compatibility err := v.UnmarshalKey("remotes", &remotes) if err != nil { return err } - // Use for backward compatibility + // Deprecated err = v.UnmarshalKey("remote", &remote) if err != nil { return err } - // Use for backward compatibility - // If "remote" key exists, append it to "remotes" + // Backward compatibility if remote != nil { - // Not logged because it's breaking tests - // log.Warn("DEPRECATED: \"remote\" key is deprecated, use \"remotes\" instead") remotes = append(remotes, remote) } @@ -153,10 +153,8 @@ func mergeRemotes(fs afero.Fs, repo *git.Repository, v *viper.Viper) error { continue } - // Use for backward compatibility with "remote(s).config" instead of "remote(s).configs" + // Use for backward compatibility with "remote(s).config" 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) } @@ -169,7 +167,7 @@ func mergeRemotes(fs afero.Fs, repo *git.Repository, v *viper.Viper) error { configFile := config configPath := filepath.Join(remotePath, configFile) - log.Debugf("Merging remote config: %s", configPath) + log.Debugf("Merging remote config: %s: %s", remote.GitURL, configPath) _, err = fs.Stat(configPath) if err != nil { @@ -267,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 4935d361..76f0aa43 100644 --- a/internal/config/load_test.go +++ b/internal/config/load_test.go @@ -433,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": { @@ -488,10 +490,12 @@ pre-commit: 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": { @@ -573,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{ @@ -830,9 +836,9 @@ pre-commit: 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.0.0", + Configs: []string{"examples/custom.yml"}, }, { GitURL: "https://github.com/evilmartians/lefthook", diff --git a/internal/config/remote.go b/internal/config/remote.go index b45e933a..ebdfca57 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"` // Deprecated in favor of Configs + 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"` + // 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"` } diff --git a/internal/git/remote.go b/internal/git/remote.go index 475140bf..ae72f734 100644 --- a/internal/git/remote.go +++ b/internal/git/remote.go @@ -17,17 +17,9 @@ const ( // RemoteFolder returns the path to the folder where the remote // repository is located. 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(), - directoryName, + remoteDirectoryName(url, ref), ) } @@ -40,25 +32,15 @@ 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 } - directoryName := filepath.Base( - strings.TrimSuffix(url, filepath.Ext(url)), - ) - - if ref != "" { - directoryName = directoryName + "-" + ref - } - - remotePath := filepath.Join( - remotesPath, - directoryName, - ) + directoryName := remoteDirectoryName(url, ref) + remotePath := filepath.Join(remotesPath, directoryName) _, err = r.Fs.Stat(remotePath) if err == nil { @@ -96,10 +78,10 @@ func (r *Repository) updateRemote(path, ref string) error { return nil } -func (r *Repository) cloneRemote(cwd, directoryName, url, ref string) error { - log.Debugf("Cloning remote config repository: %v/%v", cwd, directoryName) +func (r *Repository) cloneRemote(dest, directoryName, url, ref string) error { + log.Debugf("Cloning remote config repository: %v/%v", dest, directoryName) - cmdClone := []string{"git", "-C", cwd, "clone", "--quiet", "--depth", "1"} + cmdClone := []string{"git", "-C", dest, "clone", "--quiet", "--depth", "1"} if len(ref) > 0 { cmdClone = append(cmdClone, "--branch", ref) } @@ -112,3 +94,15 @@ func (r *Repository) cloneRemote(cwd, directoryName, 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 98dbbeec..1724a97e 100644 --- a/internal/lefthook/install.go +++ b/internal/lefthook/install.go @@ -50,11 +50,6 @@ func (l *Lefthook) Install(force bool) error { 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 { diff --git a/internal/lefthook/run.go b/internal/lefthook/run.go index be51c640..6b7849e4 100644 --- a/internal/lefthook/run.go +++ b/internal/lefthook/run.go @@ -131,11 +131,6 @@ Run 'lefthook install' manually.`, filepath.Join(l.repo.RootPath, cfg.SourceDirLocal), } - // 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 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 index 0870bee7..7858b6e3 100644 --- a/testdata/remotes.txt +++ b/testdata/remotes.txt @@ -15,6 +15,7 @@ remotes: - 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: @@ -45,7 +46,8 @@ pre-push: remotes: - git_url: https://github.com/evilmartians/lefthook ref: v1.4.0 - config: examples/with_scripts/lefthook.yml + configs: + - examples/with_scripts/lefthook.yml - git_url: https://github.com/evilmartians/lefthook configs: - examples/verbose/lefthook.yml