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
37 changes: 33 additions & 4 deletions cmd/gortex/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,38 @@ func toEnvSkills(src []genskills.GeneratedSkill) []agents.GeneratedSkill {
return out
}

// initRepoConfig resolves the layered per-repo config for root exactly the way
// the daemon does — builtin baseline, the repo's own `.gitignore` (unless
// `respect_gitignore: false`), global config, and workspace excludes — and
// returns it with Index.Exclude populated.
//
// init previously loaded the config with an empty *config file* path, which
// carries no repo, so the `.gitignore` layer never ran. indexer.New then saw an
// empty Index.Exclude and fell back to excludes.Builtin alone, so the init walk
// admitted gitignored trees (nested worktrees under an ignored directory,
// generated output) that the daemon's indexer prunes (#324).
//
// globalPath is empty in production (the default ~/.gortex/config.yaml); tests
// pass a temp path so they never read the developer's real global config.
func initRepoConfig(globalPath, root string, logger *zap.Logger) *config.Config {
cm, err := config.NewConfigManager(globalPath)
if err != nil {
// Preserve the previous best-effort behaviour: an unreadable global
// config should not block indexing outright.
cfg, loadErr := config.Load("")
if loadErr != nil {
return &config.Config{}
}
return cfg
}
if logger != nil {
cm.SetLogger(logger)
}
prefix := config.ResolvePrefix(config.RepoEntry{Path: root})
cm.LoadWorkspaceConfig(prefix, root)
return cm.GetRepoConfig(prefix)
}

// indexRepoForInit runs a one-shot index of the repo. Kept inside
// cmd/gortex (not an adapter) because the indexer pulls in many
// gortex-internal packages we'd rather not leak into internal/agents.
Expand All @@ -431,10 +463,7 @@ func indexRepoForInit(ctx context.Context, root string, logger *zap.Logger) (gra
}
defer func() { _ = logger.Sync() }()

cfg, err := config.Load("")
if err != nil {
cfg = &config.Config{}
}
cfg := initRepoConfig("", root, logger)

tmpDir, err := os.MkdirTemp("", "gortex-init-store-*")
if err != nil {
Expand Down
60 changes: 60 additions & 0 deletions cmd/gortex/init_gitignore_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package main

import (
"os"
"path/filepath"
"testing"

"github.com/zzet/gortex/internal/excludes"
)

// TestInitRepoConfigLayersRepoGitignore pins #324. `gortex init` resolved its
// config without a repo path, so the repo's own .gitignore never reached
// indexer.New; the init walk then admitted gitignored trees (nested worktrees
// under an ignored directory, generated output) that the daemon's indexer
// prunes. Builtin-only exclusion is not enough — it covers node_modules/ and
// .next/ but not repo-specific ignores.
func TestInitRepoConfigLayersRepoGitignore(t *testing.T) {
root := t.TempDir()
writeInitConfigFile(t, filepath.Join(root, ".gitignore"), "tmp/\n/generated-out/\n")
globalPath := filepath.Join(t.TempDir(), "config.yaml")
writeInitConfigFile(t, globalPath, "")

cfg := initRepoConfig(globalPath, root, nil)
matcher := excludes.New(cfg.Index.Exclude)

for _, dir := range []string{"tmp", "generated-out"} {
if !matcher.MatchAbsDir(filepath.Join(root, dir), root, true) {
t.Errorf("gitignored %q must be excluded from the init walk; Index.Exclude=%v",
dir, cfg.Index.Exclude)
}
}
// A tracked directory must still be walked, so the fix cannot be
// "exclude everything".
if matcher.MatchAbsDir(filepath.Join(root, "internal"), root, true) {
t.Errorf("tracked directory must not be excluded; Index.Exclude=%v", cfg.Index.Exclude)
}
}

// TestInitRepoConfigHonoursRespectGitignoreOptOut keeps the documented escape
// hatch working: a repo that opts out must still have its ignored trees walked.
func TestInitRepoConfigHonoursRespectGitignoreOptOut(t *testing.T) {
root := t.TempDir()
writeInitConfigFile(t, filepath.Join(root, ".gitignore"), "tmp/\n")
writeInitConfigFile(t, filepath.Join(root, ".gortex.yaml"), "respect_gitignore: false\n")
globalPath := filepath.Join(t.TempDir(), "config.yaml")
writeInitConfigFile(t, globalPath, "")

cfg := initRepoConfig(globalPath, root, nil)
if excludes.New(cfg.Index.Exclude).MatchAbsDir(filepath.Join(root, "tmp"), root, true) {
t.Errorf("respect_gitignore: false must not layer .gitignore; Index.Exclude=%v",
cfg.Index.Exclude)
}
}

func writeInitConfigFile(t *testing.T, path, content string) {
t.Helper()
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatalf("write %s: %v", path, err)
}
}
Loading