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: add refetch_frequency parameter to settings #857

Merged
merged 1 commit into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
24 changes: 24 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Config options:
- [`git_url`](#git_url)
- [`ref`](#ref-1)
- [`refetch`](#refetch)
- [`refetch_frequency`](#refetch_frequency)
- [`configs`](#configs)
- [`<pre-commit>`](#hook-name) hook name
- [`files` (global)](#files-global)
Expand Down Expand Up @@ -417,6 +418,29 @@ remotes:
refetch: true
```

### `refetch_frequency`

**Default:** Not set

Specifies how frequently Lefthook should refetch the remote configuration. This can be set to `always`, `never` or a time duration like `24h`, `30m`, etc.

- When set to `always`, Lefthook will always refetch the remote configuration on each run.
- When set to a duration (e.g., `24h`), Lefthook will check the last fetch time and refetch the configuration only if the specified amount of time has passed.
- When set to `never` or not set, Lefthook will not fetch from remote.

**Example**

```yml
# lefthook.yml

remotes:
- git_url: https://github.com/evilmartians/lefthook
refetch_frequency: 24h # Refetches once every 24 hours
```

> [!WARNING]
> If `refetch` is set to `true`, it overrides any setting in `refetch_frequency`.

### `configs`

**Default:** `[lefthook.yml]`
Expand Down
7 changes: 4 additions & 3 deletions internal/config/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ type Remote struct {
GitURL string `json:"git_url,omitempty" mapstructure:"git_url" toml:"git_url" yaml:"git_url"`
Ref string `json:"ref,omitempty" mapstructure:"ref,omitempty" toml:"ref,omitempty" yaml:",omitempty"`
// Deprecated
Config string `json:"config,omitempty" mapstructure:"config,omitempty" toml:"config,omitempty" yaml:",omitempty"`
Configs []string `json:"configs,omitempty" mapstructure:"configs,omitempty" toml:"configs,omitempty" yaml:",omitempty"`
Refetch bool `json:"refetch,omitempty" mapstructure:"refetch,omitempty" toml:"refetch,omitempty" yaml:",omitempty"`
Config string `json:"config,omitempty" mapstructure:"config,omitempty" toml:"config,omitempty" yaml:",omitempty"`
Configs []string `json:"configs,omitempty" mapstructure:"configs,omitempty" toml:"configs,omitempty" yaml:",omitempty"`
Refetch bool `json:"refetch,omitempty" mapstructure:"refetch,omitempty" toml:"refetch,omitempty" yaml:",omitempty"`
RefetchFrequency string `json:"refetch_frequency,omitempty" mapstructure:"refetch_frequency,omitempty" toml:"refetch_frequency,omitempty" yaml:",omitempty"`
}

func (r *Remote) Configured() bool {
Expand Down
35 changes: 34 additions & 1 deletion internal/lefthook/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"regexp"
"strconv"
"strings"
"time"

"github.com/gobwas/glob"
"github.com/spf13/afero"
Expand Down Expand Up @@ -118,7 +119,7 @@ func (l *Lefthook) syncHooks(cfg *config.Config, fetchRemotes bool) (*config.Con

if fetchRemotes {
for _, remote := range cfg.Remotes {
if remote.Configured() && remote.Refetch {
if remote.Configured() && l.shouldRefetch(remote) {
if err = l.repo.SyncRemote(remote.GitURL, remote.Ref, false); err != nil {
log.Warnf("Couldn't sync from %s. Will continue anyway: %s", remote.GitURL, err)
continue
Expand All @@ -141,6 +142,38 @@ func (l *Lefthook) syncHooks(cfg *config.Config, fetchRemotes bool) (*config.Con
return cfg, l.createHooksIfNeeded(cfg, true, false)
}

func (l *Lefthook) shouldRefetch(remote *config.Remote) bool {
if remote.Refetch || remote.RefetchFrequency == "always" {
return true
}
if remote.RefetchFrequency == "" || remote.RefetchFrequency == "never" {
return false
}

timedelta, err := time.ParseDuration(remote.RefetchFrequency)
if err != nil {
log.Warnf("Couldn't parse refetch frequency %s. Will continue anyway: %s", remote.RefetchFrequency, err)
return false
}

var lastFetchTime time.Time
remotePath := l.repo.RemoteFolder(remote.GitURL, remote.Ref)
info, err := l.Fs.Stat(filepath.Join(remotePath, ".git", "FETCH_HEAD"))

if err != nil {
if errors.Is(err, os.ErrNotExist) {
return true
}

log.Warnf("Failed to detect last fetch time: %s", err)
return false
}

lastFetchTime = info.ModTime()
return time.Now().After(lastFetchTime.Add(timedelta))

}

func (l *Lefthook) createHooksIfNeeded(cfg *config.Config, checkHashSum, force bool) error {
if checkHashSum && l.hooksSynchronized(cfg) {
return nil
Expand Down
127 changes: 127 additions & 0 deletions internal/lefthook/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,3 +410,130 @@ post-commit:
})
}
}

func TestShouldRefetch(t *testing.T) {
root, err := filepath.Abs("src")
if err != nil {
t.Errorf("unexpected error: %s", err)
}

configPath := filepath.Join(root, "lefthook.yml")
fetchHeadPath := func(lefthook *Lefthook, remote *config.Remote) string {
remotePath := lefthook.repo.RemoteFolder(remote.GitURL, remote.Ref)
return filepath.Join(remotePath, ".git", "FETCH_HEAD")
}

repo := &git.Repository{
HooksPath: filepath.Join(root, ".git", "hooks"),
RootPath: root,
InfoPath: filepath.Join(root, ".git", "info"),
}
for n, tt := range [...]struct {
name, config string
shouldRefetchInitially, shouldRefetchAfter, shouldRefetchBefore bool
}{
{
name: "with refetch frequency configured to always",
config: `
remotes:
- git_url: https://github.com/evilmartians/lefthook
refetch_frequency: always
configs:
- examples/remote/ping.yml
`,
shouldRefetchInitially: true,
shouldRefetchAfter: true,
shouldRefetchBefore: true,
},
{
name: "with refetch frequency configured to 1 minute",
config: `
remotes:
- git_url: https://github.com/evilmartians/lefthook
refetch_frequency: 1m
configs:
- examples/remote/ping.yml
`,
shouldRefetchInitially: true,
shouldRefetchAfter: true,
shouldRefetchBefore: false,
},
{
name: "with refetch frequency configured to never",
config: `
remotes:
- git_url: https://github.com/evilmartians/lefthook
refetch_frequency: never
configs:
- examples/remote/ping.yml
`,
shouldRefetchInitially: false,
shouldRefetchAfter: false,
shouldRefetchBefore: false,
},
{
name: "with refetch frequency not configured",
config: `
remotes:
- git_url: https://github.com/evilmartians/lefthook
configs:
- examples/remote/ping.yml
`,
shouldRefetchInitially: false,
shouldRefetchAfter: false,
shouldRefetchBefore: false,
},
} {
fs := afero.NewMemMapFs()
lefthook := &Lefthook{
Options: &Options{Fs: fs},
repo: repo,
}

t.Run(fmt.Sprintf("%d: %s", n, tt.name), func(t *testing.T) {
// Create configuration file
if len(tt.config) > 0 {
if err := afero.WriteFile(fs, configPath, []byte(tt.config), 0o644); err != nil {
t.Errorf("unexpected error: %s", err)
}
timestamp := time.Date(2022, time.June, 22, 10, 40, 10, 1, time.UTC)
if err := fs.Chtimes(configPath, timestamp, timestamp); err != nil {
t.Errorf("unexpected error: %s", err)
}
}

cfg, err := config.Load(lefthook.Fs, repo)
if err != nil {
t.Errorf("unexpected error: %s", err)
}

remote := cfg.Remotes[0]

if lefthook.shouldRefetch(remote) != tt.shouldRefetchInitially {
t.Errorf("unexpected shouldRefetch return before first fetch")
}

if err := afero.WriteFile(fs, fetchHeadPath(lefthook, remote), []byte(""), 0o644); err != nil {
t.Errorf("unexpected error: %s", err)
}

firstFetchTime := time.Now().Add(-2 * time.Duration(time.Minute))

if err := fs.Chtimes(fetchHeadPath(lefthook, remote), firstFetchTime, firstFetchTime); err != nil {
t.Errorf("unexpected error: %s", err)
}

if lefthook.shouldRefetch(remote) != tt.shouldRefetchAfter {
t.Errorf("unexpected shouldRefetch return after refetch period")
}

if err := fs.Chtimes(fetchHeadPath(lefthook, remote), firstFetchTime, time.Now()); err != nil {
t.Errorf("unexpected error: %s", err)
}

if lefthook.shouldRefetch(remote) != tt.shouldRefetchBefore {
t.Errorf("unexpected shouldRefetch return before refetch period")
}
})
}
}
Loading