Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Add remote config support #343

Merged
merged 13 commits into from
Nov 3, 2022
91 changes: 83 additions & 8 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
- [`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)
- [`files`](#files-global)
- [`parallel`](#parallel)
Expand Down Expand Up @@ -57,23 +61,20 @@ colors: false

### `extends`

You can extend your config with another one YAML file.
You can extend your config with another one YAML file. Its content will be merged. Extends for `lefthook.yml`, `lefthook-local.yml`, and [`remote`](#remote) configs are handled separately, so you can have different extends in these files.

**Example**

```yml
# lefthook.yml

extends:
- $HOME/work/lefthook-extend.yml
- $HOME/work/lefthook-extend-2.yml
- /home/user/work/lefthook-extend.yml
- /home/user/work/lefthook-extend-2.yml
- lefthook-extends/file.yml
- ../extend.yml
```

**Notes**

Files for extend should *not* be named "lefthook.yml". All file names should be unique.


### `min_version`

If you want to specify a minimum version for lefthook binary (e.g. if you need some features older versions don't have) you can set this option.
Expand Down Expand Up @@ -138,6 +139,80 @@ Change a directory for *local* script files (not stored in VCS).

This option is useful if you have a `lefthook-local.yml` config file and want to reference different scripts there.

## `remote`

> :test_tube: This feature is in **Beta** version

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`.

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 `remote` will be merged to confiuration in `lefthook.yml`, so the priority will be the following:

- `lefthook.yml`
- `remote`
- `lefthook-local.yml`

This can be changed in the future. For convenience, please use `remote` configuration without any hooks configuration in `lefthook.yml`.

### `git_url`

A URL to Git repository. It will be accessed with priveleges of the machine lefthook runs on.

**Example**

```yml
# lefthook.yml

remote:
git_url: git@github.com:evilmartians/lefthook
```

Or

```yml
# lefthook.yml

remote:
git_url: https://github.com/evilmartians/lefthook
```

### `ref`

An optional *branch* or *tag* name.

**Example**

```yml
# lefthook.yml

remote:
git_url: git@github.com:evilmartians/lefthook
ref: v1.0.0
```


### `config`

**Default:** `lefthook.yml`

An optional config path from remote's root.

**Example**

```yml
# lefthook.yml

remote:
git_url: git@github.com:evilmartians/remote
ref: v1.0.0
config: examples/ruby-linter.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
13 changes: 13 additions & 0 deletions examples/remote/ping.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Test `remote` config of lefthook.
#
# # lefthook.yml
#
# remote:
# git_url: git@github.com:evilmartians/lefthook
# config: examples/remote/ping.yml
#
# $ lefthook run pre-commit

pre-commit:
commands:
run: echo pong
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ require (
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-isatty v0.0.14
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/spf13/cast v1.5.0 // indirect
Expand Down
1 change: 1 addition & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
type Config struct {
Colors bool `mapstructure:"colors"`
Extends []string `mapstructure:"extends"`
Remote Remote `mapstructure:"remote"`
MinVersion string `mapstructure:"min_version"`
SkipOutput []string `mapstructure:"skip_output"`
SourceDir string `mapstructure:"source_dir"`
Expand Down
91 changes: 73 additions & 18 deletions internal/config/load.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package config

import (
"fmt"
"path/filepath"
"regexp"
"strings"

"github.com/spf13/afero"
"github.com/spf13/viper"

"github.com/evilmartians/lefthook/internal/git"
"github.com/evilmartians/lefthook/internal/log"
)

const (
DefaultConfigName = "lefthook.yml"
DefaultSourceDir = ".lefthook"
DefaultSourceDirLocal = ".lefthook-local"
DefaultColorsEnabled = true
Expand All @@ -18,13 +23,13 @@ const (
var hookKeyRegexp = regexp.MustCompile(`^(?P<hookName>[^.]+)\.(scripts|commands)`)

// Loads configs from the given directory with extensions.
func Load(fs afero.Fs, path string) (*Config, error) {
global, err := read(fs, path, "lefthook")
func Load(fs afero.Fs, repo *git.Repository) (*Config, error) {
global, err := read(fs, repo.RootPath, "lefthook")
if err != nil {
return nil, err
}

extends, err := mergeAllExtends(fs, path)
extends, err := mergeAll(fs, repo)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -61,43 +66,93 @@ func read(fs afero.Fs, path string, name string) (*viper.Viper, error) {
return v, nil
}

// Merges extends from .lefthook and .lefthook-local.
func mergeAllExtends(fs afero.Fs, path string) (*viper.Viper, error) {
extends, err := read(fs, path, "lefthook")
// mergeAll merges remotes and extends from .lefthook and .lefthook-local.
func mergeAll(fs afero.Fs, repo *git.Repository) (*viper.Viper, error) {
extends, err := read(fs, repo.RootPath, "lefthook")
if err != nil {
return nil, err
}

if err := extend(fs, extends); err != nil {
if err := extend(extends, repo.RootPath); err != nil {
return nil, err
}

extends.SetConfigName("lefthook-local")
if err := extends.MergeInConfig(); err != nil {
if err := mergeRemote(fs, repo, extends); err != nil {
return nil, err
}

if err := merge("lefthook-local", "", extends); err != nil {
if _, notFoundErr := err.(viper.ConfigFileNotFoundError); !notFoundErr {
return nil, err
}
}

if err := extend(fs, extends); err != nil {
if err := extend(extends, repo.RootPath); err != nil {
return nil, err
}

return extends, nil
}

func extend(fs afero.Fs, v *viper.Viper) error {
for _, path := range v.GetStringSlice("extends") {
name := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
// 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)
if err != nil {
return err
}

another, err := read(fs, filepath.Dir(path), name)
if err != nil {
return err
if !remote.Configured() {
return nil
}

remotePath := repo.RemoteFolder(remote.GitURL)
configFile := DefaultConfigName
if len(remote.Config) > 0 {
configFile = remote.Config
}
configPath := filepath.Join(remotePath, configFile)

log.Debugf("Merging remote config: %s", configPath)

_, err = fs.Stat(configPath)
if err != nil {
return nil
}

if err := merge("remote", configPath, v); err != nil {
return err
}

if err := extend(v, filepath.Dir(configPath)); err != nil {
return err
}

return nil
}

// extend merges all files listed in 'extends' option into the config.
func extend(v *viper.Viper, root string) error {
for i, path := range v.GetStringSlice("extends") {
if !filepath.IsAbs(path) {
path = filepath.Join(root, path)
}
if err = v.MergeConfigMap(another.AllSettings()); err != nil {
if err := merge(fmt.Sprintf("extend_%d", i), path, v); err != nil {
return err
}
}
return nil
}

// merge merges the configuration using viper builtin MergeInConfig.
func merge(name, path string, v *viper.Viper) error {
v.SetConfigName(name)
if len(path) > 0 {
v.SetConfigFile(path)
}
if err := v.MergeInConfig(); err != nil {
return err
}

return nil
}
Expand All @@ -112,7 +167,7 @@ func unmarshalConfigs(base, extra *viper.Viper, c *Config) error {
}

// For extra non-git hooks.
// This behavior will be deprecated in next versions.
// This behavior may be deprecated in next versions.
for _, maybeHook := range base.AllKeys() {
if !hookKeyRegexp.MatchString(maybeHook) {
continue
Expand Down
Loading