Skip to content
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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

Expand Down
File renamed without changes.
25 changes: 15 additions & 10 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Comment on lines +334 to +335

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Continue fallback search when preferred config path is a dir

The new probe order returns an error as soon as git-ai-commit.toml exists but is a directory, so the legacy .git-ai-commit.toml file is never considered. That regresses backward compatibility in repos where a directory with the new name is present (accidentally or from tooling), because previously a valid hidden config would load successfully; now Load() fails before trust/parse can happen.

Useful? React with 👍 / 👎.

}
return root, path, nil
}
return root, path, nil
return root, "", nil
}

func LoadPromptPreset(name string) (string, error) {
Expand Down
63 changes: 63 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down