@@ -15,6 +15,7 @@ import (
1515 tea "github.com/charmbracelet/bubbletea"
1616 "github.com/charmbracelet/lipgloss"
1717 "github.com/go-git/go-git/v5"
18+ "github.com/go-git/go-git/v5/plumbing/format/gitignore"
1819 "github.com/mattn/go-isatty"
1920)
2021
@@ -49,7 +50,7 @@ func getTTY() (in, out *os.File, cleanup func()) {
4950// yapiFilePattern matches *.yapi, *.yapi.yaml or *.yapi.yml in subdirectories only
5051var yapiFilePattern = regexp .MustCompile (`^.+/.+\.yapi(\.ya?ml)?$` )
5152
52- // FindConfigFiles returns all git-tracked yapi config files relative to the current directory
53+ // FindConfigFiles returns all non-gitignored yapi config files relative to the current directory
5354func FindConfigFiles () ([]string , error ) {
5455 return findFiles (false )
5556}
@@ -60,69 +61,99 @@ func findFiles(includeProjectConfig bool) ([]string, error) {
6061 return nil , fmt .Errorf ("failed to get current working directory: %w" , err )
6162 }
6263
63- // Open the git repository (searches up for .git)
64- repo , err := git .PlainOpenWithOptions (cwd , & git.PlainOpenOptions {
64+ var configFiles []string
65+ err = findFilesInRepo (cwd , cwd , includeProjectConfig , & configFiles )
66+ if err != nil {
67+ return nil , err
68+ }
69+
70+ if len (configFiles ) == 0 {
71+ return nil , fmt .Errorf ("no .yapi/.yapi.yaml/.yapi.yml files found in subdirectories" )
72+ }
73+
74+ sort .Strings (configFiles )
75+ return configFiles , nil
76+ }
77+
78+ // findFilesInRepo walks a directory, respecting gitignore rules and handling submodules.
79+ func findFilesInRepo (dir , searchRoot string , includeProjectConfig bool , results * []string ) error {
80+ // Open the git repository from this directory
81+ repo , err := git .PlainOpenWithOptions (dir , & git.PlainOpenOptions {
6582 DetectDotGit : true ,
6683 })
6784 if err != nil {
68- return nil , fmt .Errorf ("not in a git repository: %w" , err )
85+ return fmt .Errorf ("not in a git repository: %w" , err )
6986 }
7087
71- // Get worktree to find repo root
7288 wt , err := repo .Worktree ()
7389 if err != nil {
74- return nil , fmt .Errorf ("failed to get worktree: %w" , err )
90+ return fmt .Errorf ("failed to get worktree: %w" , err )
7591 }
7692 repoRoot := wt .Filesystem .Root ()
7793
78- // Read the git index (staged files = tracked files)
79- idx , err := repo . Storer . Index ( )
94+ // Read gitignore patterns for this repo
95+ patterns , err := gitignore . ReadPatterns ( wt . Filesystem , nil )
8096 if err != nil {
81- return nil , fmt . Errorf ( "failed to read git index: %w" , err )
97+ patterns = nil
8298 }
99+ matcher := gitignore .NewMatcher (patterns )
83100
84- // Calculate relative path from repo root to cwd
85- relCwd , err := filepath .Rel (repoRoot , cwd )
86- if err != nil {
87- return nil , fmt .Errorf ("failed to calculate relative path: %w" , err )
88- }
89- if relCwd == "." {
90- relCwd = ""
91- }
101+ return filepath .Walk (dir , func (path string , info os.FileInfo , err error ) error {
102+ if err != nil {
103+ return nil
104+ }
92105
93- var configFiles []string
94- for _ , entry := range idx .Entries {
95- path := entry .Name
106+ // Get path relative to repo root for gitignore matching
107+ relToRepo , err := filepath .Rel (repoRoot , path )
108+ if err != nil {
109+ return nil
110+ }
96111
97- // Skip if not under current directory
98- if relCwd != "" && ! strings .HasPrefix (path , relCwd + "/" ) {
99- continue
112+ pathComponents := strings .Split (filepath .ToSlash (relToRepo ), "/" )
113+
114+ if info .IsDir () {
115+ if info .Name () == ".git" {
116+ return filepath .SkipDir
117+ }
118+
119+ // Check if this is a submodule (has its own .git)
120+ if path != dir {
121+ gitPath := filepath .Join (path , ".git" )
122+ if _ , err := os .Stat (gitPath ); err == nil {
123+ // This is a submodule - recurse with new repo context
124+ _ = findFilesInRepo (path , searchRoot , includeProjectConfig , results )
125+ return filepath .SkipDir
126+ }
127+ }
128+
129+ // Check if directory is gitignored
130+ if matcher .Match (pathComponents , true ) {
131+ return filepath .SkipDir
132+ }
133+ return nil
100134 }
101135
102- // Get path relative to cwd
103- var relPath string
104- if relCwd != "" {
105- relPath = strings .TrimPrefix (path , relCwd + "/" )
106- } else {
107- relPath = path
136+ // Check if file is gitignored
137+ if matcher .Match (pathComponents , false ) {
138+ return nil
139+ }
140+
141+ // Get path relative to search root for display
142+ relPath , err := filepath .Rel (searchRoot , path )
143+ if err != nil {
144+ return nil
108145 }
109146
110147 // Match .yapi.yml files
111148 base := filepath .Base (relPath )
112149 if yapiFilePattern .MatchString (relPath ) {
113- configFiles = append (configFiles , relPath )
150+ * results = append (* results , relPath )
114151 } else if includeProjectConfig && (base == "yapi.config.yml" || base == "yapi.config.yaml" ) {
115- // Only include yapi.config.yml if explicitly requested
116- configFiles = append (configFiles , relPath )
152+ * results = append (* results , relPath )
117153 }
118- }
119154
120- if len (configFiles ) == 0 {
121- return nil , fmt .Errorf ("no .yapi/.yapi.yaml/.yapi.yml files found in subdirectories" )
122- }
123-
124- sort .Strings (configFiles )
125- return configFiles , nil
155+ return nil
156+ })
126157}
127158
128159// FindConfigFileSingle prompts the user to select a single config file.
0 commit comments