From 3e6ca3c8a0ea83290ed8c88d1138ac813bcdff03 Mon Sep 17 00:00:00 2001 From: Naoto Takai Date: Wed, 1 Apr 2026 06:03:45 +0900 Subject: [PATCH] feat(config): prefer git-ai-commit.toml over hidden file --- README.md | 4 +- .git-ai-commit.toml => git-ai-commit.toml | 0 internal/config/config.go | 25 +++++---- internal/config/config_test.go | 63 +++++++++++++++++++++++ 4 files changed, 80 insertions(+), 12 deletions(-) rename .git-ai-commit.toml => git-ai-commit.toml (100%) diff --git a/README.md b/README.md index 48f585b..4dabae6 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Configuration is layered. Later layers override earlier ones: | 1 (lowest) | System git config (`/etc/gitconfig`) | | 2 | Global git config (`~/.gitconfig`) | | 3 | User TOML (`~/.config/git-ai-commit/config.toml`) | -| 4 | Repo TOML (`.git-ai-commit.toml` at repo root) | +| 4 | Repo TOML (`git-ai-commit.toml` or `.git-ai-commit.toml` at repo root) | | 5 | Local git config (`.git/config`) | | 6 | Worktree git config | | 7 (highest) | Command-line flags | @@ -69,7 +69,7 @@ Repository TOML config is applied only after an initial trust prompt, since it i ### TOML config files -`~/.config/git-ai-commit/config.toml` for user-wide defaults, `.git-ai-commit.toml` at the repo root for project defaults. +`~/.config/git-ai-commit/config.toml` for user-wide defaults, `git-ai-commit.toml` (or `.git-ai-commit.toml`) at the repo root for project defaults. Example: Use Codex with Conventional Commits by default diff --git a/.git-ai-commit.toml b/git-ai-commit.toml similarity index 100% rename from .git-ai-commit.toml rename to git-ai-commit.toml diff --git a/internal/config/config.go b/internal/config/config.go index c7e7584..fa7de67 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -309,6 +309,8 @@ func configPath() (string, error) { return filepath.Join(dir, "config.toml"), nil } +var repoConfigNames = []string{"git-ai-commit.toml", ".git-ai-commit.toml"} + func repoConfigPath() (string, string, error) { cmd := exec.Command("git", "rev-parse", "--show-toplevel") var stdout bytes.Buffer @@ -320,18 +322,21 @@ func repoConfigPath() (string, string, error) { if root == "" { return "", "", nil } - path := filepath.Join(root, ".git-ai-commit.toml") - info, err := os.Stat(path) - if err != nil { - if os.IsNotExist(err) { - return root, "", nil + for _, name := range repoConfigNames { + path := filepath.Join(root, name) + info, err := os.Stat(path) + if err != nil { + if os.IsNotExist(err) { + continue + } + return "", "", fmt.Errorf("stat repo config: %w", err) } - return "", "", fmt.Errorf("stat repo config: %w", err) - } - if info.IsDir() { - return "", "", fmt.Errorf("repo config is a directory: %s", path) + if info.IsDir() { + return "", "", fmt.Errorf("repo config is a directory: %s", path) + } + return root, path, nil } - return root, path, nil + return root, "", nil } func LoadPromptPreset(name string) (string, error) { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 52edc01..c1627d8 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -509,6 +509,69 @@ func containsHelper(s, substr string) bool { return false } +func TestNonHiddenRepoConfig(t *testing.T) { + base := t.TempDir() + repo := filepath.Join(base, "repo") + if err := os.MkdirAll(repo, 0o755); err != nil { + t.Fatalf("mkdir repo: %v", err) + } + runGit(t, repo, "init") + + repoConfig := filepath.Join(repo, "git-ai-commit.toml") + if err := os.WriteFile(repoConfig, []byte("engine = 'codex'\n"), 0o644); err != nil { + t.Fatalf("write repo config: %v", err) + } + + configHome := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", configHome) + + trustRepoConfig(t, repo, repoConfig) + + withDir(t, repo, func() { + cfg, err := Load() + if err != nil { + t.Fatalf("Load error: %v", err) + } + if cfg.DefaultEngine != "codex" { + t.Fatalf("DefaultEngine = %q, want codex", cfg.DefaultEngine) + } + }) +} + +func TestNonHiddenRepoConfigPrecedence(t *testing.T) { + base := t.TempDir() + repo := filepath.Join(base, "repo") + if err := os.MkdirAll(repo, 0o755); err != nil { + t.Fatalf("mkdir repo: %v", err) + } + runGit(t, repo, "init") + + // Both files exist; non-hidden should win + nonHidden := filepath.Join(repo, "git-ai-commit.toml") + if err := os.WriteFile(nonHidden, []byte("engine = 'codex'\n"), 0o644); err != nil { + t.Fatalf("write non-hidden config: %v", err) + } + hidden := filepath.Join(repo, ".git-ai-commit.toml") + if err := os.WriteFile(hidden, []byte("engine = 'gemini'\n"), 0o644); err != nil { + t.Fatalf("write hidden config: %v", err) + } + + configHome := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", configHome) + + trustRepoConfig(t, repo, nonHidden) + + withDir(t, repo, func() { + cfg, err := Load() + if err != nil { + t.Fatalf("Load error: %v", err) + } + if cfg.DefaultEngine != "codex" { + t.Fatalf("DefaultEngine = %q, want codex (non-hidden should take precedence)", cfg.DefaultEngine) + } + }) +} + func trustRepoConfig(t *testing.T, repoRoot, repoConfigPath string) { t.Helper() data, err := os.ReadFile(repoConfigPath)