Skip to content

Commit

Permalink
feat(remotes): add remotes and configs options (#609)
Browse files Browse the repository at this point in the history
* feat(remotes): add remotes and configs

* docs: chore in the docs

* fix: some small improvements and deprecation logs

---------

Co-authored-by: Valentin Kiselev <mrexox@evilmartians.com>
  • Loading branch information
NikitaCOEUR and mrexox committed Jan 22, 2024
1 parent 3246821 commit f49b54a
Show file tree
Hide file tree
Showing 11 changed files with 496 additions and 94 deletions.
138 changes: 124 additions & 14 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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`.

Expand All @@ -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**
Expand All @@ -330,6 +335,8 @@ remote:

### `ref`

> :warning: DEPRECATED use [`remotes`](#remotes) setting
An optional *branch* or *tag* name.

**Example**
Expand All @@ -348,6 +355,8 @@ remote:
### `config`

> :warning: DEPRECATED use [`remotes`](#remotes) setting
**Default:** `lefthook.yml`

An optional config path from remote's root.
Expand All @@ -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).
Expand Down
9 changes: 5 additions & 4 deletions examples/remote/ping.yml
Original file line number Diff line number Diff line change
@@ -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

Expand Down
5 changes: 4 additions & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:"-"`
}
Expand Down
106 changes: 79 additions & 27 deletions internal/config/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}

Expand All @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit f49b54a

Please sign in to comment.