diff --git a/pkg/config/config.go b/pkg/config/config.go index 9789a06..1cddc1f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -130,6 +130,8 @@ func (c *Config) Set(keys []string, value string) { } func (c *Config) deepCopy() *Config { + c.mu.RLock() + defer c.mu.RUnlock() return ReadFromString(c.entries.String()) } @@ -145,6 +147,28 @@ var Read = func(fallback *Config) (*Config, error) { return cfg, loadErr } +// Reload re-reads the gh configuration files from disk and refreshes the +// configuration returned by Read in place. The cached *Config keeps the same +// pointer, so every existing holder of it observes the new values. +// +// Any in-memory configuration changes that have not been written to disk are +// discarded by the refresh. Callers that need such changes preserved must Write +// them before calling Reload. +var Reload = func(fallback *Config) error { + cached, err := Read(fallback) + if err != nil { + return err + } + fresh, err := load(generalConfigFile(), hostsConfigFile(), fallback) + if err != nil { + return err + } + cached.mu.Lock() + defer cached.mu.Unlock() + cached.entries = fresh.entries + return nil +} + // ReadFromString takes a yaml string and returns a Config. func ReadFromString(str string) *Config { m, _ := mapFromString(str) diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 01cb61b..08436e3 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "runtime" + "sync" "testing" "github.com/stretchr/testify/assert" @@ -290,6 +291,50 @@ func TestCacheDir(t *testing.T) { } +func TestReadAndReload(t *testing.T) { + once = sync.Once{} + cfg, loadErr = nil, nil + t.Cleanup(func() { + once = sync.Once{} + cfg, loadErr = nil, nil + }) + + dir := t.TempDir() + t.Setenv("GH_CONFIG_DIR", dir) + path := filepath.Join(dir, "config.yml") + require.NoError(t, os.WriteFile(path, []byte("editor: vim\n"), 0o600)) + + c, err := Read(nil) + require.NoError(t, err) + editor, err := c.Get([]string{"editor"}) + require.NoError(t, err) + assert.Equal(t, "vim", editor) + + require.NoError(t, os.WriteFile(path, []byte("editor: nano\n"), 0o600)) + + // Read still serves the cached value until an explicit Reload. + c, err = Read(nil) + require.NoError(t, err) + editor, err = c.Get([]string{"editor"}) + require.NoError(t, err) + assert.Equal(t, "vim", editor) + + require.NoError(t, Reload(nil)) + + // The previously obtained pointer observes the refreshed value in place. + editor, err = c.Get([]string{"editor"}) + require.NoError(t, err) + assert.Equal(t, "nano", editor) + + // Read returns the same, refreshed *Config pointer. + reread, err := Read(nil) + require.NoError(t, err) + assert.Same(t, c, reread) + editor, err = reread.Get([]string{"editor"}) + require.NoError(t, err) + assert.Equal(t, "nano", editor) +} + func TestLoad(t *testing.T) { tempDir := t.TempDir() globalFilePath := filepath.Join(tempDir, "config.yml")