From 1bcc75cccafb910dc9872674b93adfcbb55a694b Mon Sep 17 00:00:00 2001 From: Gabriel Silveira <26633512+gabriel-ss@users.noreply.github.com> Date: Tue, 29 Oct 2024 04:19:03 -0300 Subject: [PATCH] feat: add refetch_frequency parameter to settings (#857) --- docs/configuration.md | 24 ++++++ internal/config/remote.go | 7 +- internal/lefthook/install.go | 35 +++++++- internal/lefthook/install_test.go | 127 ++++++++++++++++++++++++++++++ 4 files changed, 189 insertions(+), 4 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index cfd34111..4b4c9452 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -33,6 +33,7 @@ Config options: - [`git_url`](#git_url) - [`ref`](#ref-1) - [`refetch`](#refetch) + - [`refetch_frequency`](#refetch_frequency) - [`configs`](#configs) - [``](#hook-name) hook name - [`files` (global)](#files-global) @@ -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]` diff --git a/internal/config/remote.go b/internal/config/remote.go index 69dcab17..3cb29637 100644 --- a/internal/config/remote.go +++ b/internal/config/remote.go @@ -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 { diff --git a/internal/lefthook/install.go b/internal/lefthook/install.go index 6a2c202d..ea1f96c7 100644 --- a/internal/lefthook/install.go +++ b/internal/lefthook/install.go @@ -9,6 +9,7 @@ import ( "regexp" "strconv" "strings" + "time" "github.com/gobwas/glob" "github.com/spf13/afero" @@ -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 @@ -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 diff --git a/internal/lefthook/install_test.go b/internal/lefthook/install_test.go index 10d79985..77a7319c 100644 --- a/internal/lefthook/install_test.go +++ b/internal/lefthook/install_test.go @@ -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") + } + }) + } +}