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

feat(remotes): add remotes and configs #609

Merged
merged 3 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 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
```

More complicated example.

```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).
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
3 changes: 2 additions & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:"-"`
}
Expand Down
83 changes: 58 additions & 25 deletions internal/config/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"})
Expand All @@ -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
}

Expand All @@ -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
}
Comment on lines +181 to +183
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we will extend the same things multiple times, and if files from different remotes have extends it will break here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I've tested this concern and I coundn't reproduce an error, because extends get replaced after merging each remote. That's fine. I will polish the PR a little bit and prepare it for the release.

}

// 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
Loading
Loading