Go 105 lines
package config
import (
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
)
// LocalFilename is the per-workspace override file name.
const LocalFilename = "config.local"
// WriteLocalKeys upserts key=value pairs into <workspace>/config.local,
// preserving comments, blank lines, unknown keys, and line order. An
// existing non-comment line whose key matches is replaced in place; a
// key not yet present is appended (appended keys in sorted order for a
// deterministic file). The workspace directory must already exist —
// settings are an initialised-workspace operation, like gc and new.
//
// It only edits config.local inside the gitignored workspace; it never
// touches the tracked tree.
func WriteLocalKeys(cfg *Config, kv map[string]string) error {
if cfg == nil {
return errors.New("WriteLocalKeys: nil config")
}
if len(kv) == 0 {
return nil
}
info, err := os.Stat(cfg.Workspace)
if err != nil || !info.IsDir() {
return fmt.Errorf("workspace %s is not initialised", cfg.Workspace)
}
return upsertKeys(filepath.Join(cfg.Workspace, LocalFilename), kv)
}
// WriteGlobalKeys upserts key=value pairs into the user-global
// config.local (GlobalConfigDir()/config.local), creating the global
// directory if absent. Same upsert semantics as WriteLocalKeys. This is
// the one writer that touches a file outside any repo, by design — it
// backs `eeco config set --global`, the cross-project settings layer.
func WriteGlobalKeys(kv map[string]string) error {
if len(kv) == 0 {
return nil
}
dir := GlobalConfigDir()
if dir == "" {
return errors.New("cannot resolve a global config directory (set EECO_CONFIG_HOME or HOME)")
}
if err := os.MkdirAll(dir, 0o755); err != nil {
return fmt.Errorf("create global config dir: %w", err)
}
return upsertKeys(filepath.Join(dir, LocalFilename), kv)
}
// upsertKeys writes key=value pairs into the config.local-format file at
// path, preserving comments, blank lines, unknown keys, and line order.
// An existing non-comment line whose key matches is replaced in place; a
// key not yet present is appended (appended keys in sorted order for a
// deterministic file).
func upsertKeys(path string, kv map[string]string) error {
existing, err := os.ReadFile(path)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("read %s: %w", LocalFilename, err)
}
written := map[string]bool{}
var out []string
if len(existing) > 0 {
for _, raw := range strings.Split(strings.TrimRight(string(existing), "\n"), "\n") {
trimmed := strings.TrimSpace(raw)
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
out = append(out, raw)
continue
}
k, _, ok := strings.Cut(trimmed, "=")
if !ok {
out = append(out, raw)
continue
}
key := strings.TrimSpace(k)
if v, replace := kv[key]; replace && !written[key] {
out = append(out, key+"="+v)
written[key] = true
continue
}
out = append(out, raw)
}
}
var fresh []string
for k := range kv {
if !written[k] {
fresh = append(fresh, k)
}
}
sort.Strings(fresh)
for _, k := range fresh {
out = append(out, k+"="+kv[k])
}
return os.WriteFile(path, []byte(strings.Join(out, "\n")+"\n"), 0o644)
}