on main help sign in
fix(ls): respect git's `core.excludesfile` config if set (#2314)

See: https://git-scm.com/docs/gitignore Co-authored-by: Andrey Nering <andreynering@users.noreply.github.com>

commit3412194cc489ff259ee2e46059930830f5c54857 authorMartin <1224973+mavaa@users.noreply.github.com> dateTue 31 Mar 2026, 22:04 (7 months ago) parentbf21a59
3 files changed+57−12
go.mod+2 go.sum+6 internal/fsext/ls.go+49−12
go.mod+2
@@ -105,6 +105,7 @@ require (
105105 github.com/charmbracelet/x/json v0.2.0 // indirect
106106 github.com/charmbracelet/x/termios v0.1.1 // indirect
107107 github.com/charmbracelet/x/windows v0.2.2 // indirect
108+ github.com/cyphar/filepath-securejoin v0.4.1 // indirect
108109 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
109110 github.com/dlclark/regexp2 v1.11.5 // indirect
110111 github.com/ebitengine/purego v0.10.0 // indirect
@@ -156,6 +157,7 @@ require (
156157 github.com/ncruces/julianday v1.0.0 // indirect
157158 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
158159 github.com/pierrec/lz4/v4 v4.1.25 // indirect
160+ github.com/pjbgf/sha1cd v0.3.2 // indirect
159161 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
160162 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
161163 github.com/segmentio/asm v1.2.1 // indirect
go.sum+6
@@ -141,6 +141,8 @@ github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7X
141141github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
142142github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
143143github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
144+github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
145+github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
144146github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
145147github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
146148github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -303,10 +305,14 @@ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6
303305github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
304306github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
305307github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
308+github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
309+github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
306310github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
307311github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
308312github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
309313github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
314+github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
315+github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
310316github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
311317github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
312318github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
internal/fsext/ls.go+49−12
@@ -1,6 +1,7 @@
11package fsext
22
33import (
4+ "cmp"
45 "errors"
56 "log/slog"
67 "os"
@@ -12,6 +13,7 @@ import (
1213 "github.com/charlievieth/fastwalk"
1314 "github.com/charmbracelet/crush/internal/csync"
1415 "github.com/charmbracelet/crush/internal/home"
16+ gitconfig "github.com/go-git/go-git/v5/config"
1517 "github.com/go-git/go-git/v5/plumbing/format/gitignore"
1618)
1719
@@ -80,18 +82,52 @@ var commonIgnorePatterns = sync.OnceValue(func() []gitignore.Pattern {
8082 return parsePatterns(patterns, nil)
8183})
8284
83−var homeIgnorePatterns = sync.OnceValue(func() []gitignore.Pattern {
84− homeDir := home.Dir()
85− var lines []string
86− for _, name := range []string{
87− filepath.Join(homeDir, ".gitignore"),
88− filepath.Join(homeDir, ".config", "git", "ignore"),
89− filepath.Join(homeDir, ".config", "crush", "ignore"),
90− } {
91− if bts, err := os.ReadFile(name); err == nil {
92− lines = append(lines, strings.Split(string(bts), "\n")...)
85+// gitGlobalIgnorePatterns returns patterns from git's global excludes file
86+// (core.excludesFile), following git's config resolution order.
87+var gitGlobalIgnorePatterns = sync.OnceValue(func() []gitignore.Pattern {
88+ cfg, err := gitconfig.LoadConfig(gitconfig.GlobalScope)
89+ if err != nil {
90+ slog.Debug("Failed to load global git config", "error", err)
91+ return nil
92+ }
93+
94+ configPath := cmp.Or(
95+ os.Getenv("XDG_CONFIG_HOME"),
96+ filepath.Join(home.Dir(), ".config"),
97+ )
98+ excludesFilePath := cmp.Or(
99+ cfg.Raw.Section("core").Options.Get("excludesfile"),
100+ filepath.Join(configPath, "git", "ignore"),
101+ )
102+ excludesFilePath = home.Long(excludesFilePath)
103+
104+ bts, err := os.ReadFile(excludesFilePath)
105+ if err != nil {
106+ if !os.IsNotExist(err) {
107+ slog.Debug("Failed to read git global excludes file", "path", excludesFilePath, "error", err)
93108 }
109+ return nil
110+ }
111+
112+ return parsePatterns(strings.Split(string(bts), "\n"), nil)
113+})
114+
115+// crushGlobalIgnorePatterns returns patterns from the user's
116+// ~/.config/crush/ignore file.
117+var crushGlobalIgnorePatterns = sync.OnceValue(func() []gitignore.Pattern {
118+ configPath := cmp.Or(
119+ os.Getenv("XDG_CONFIG_HOME"),
120+ filepath.Join(home.Dir(), ".config"),
121+ )
122+ name := filepath.Join(configPath, "crush", "ignore")
123+ bts, err := os.ReadFile(name)
124+ if err != nil {
125+ if !os.IsNotExist(err) {
126+ slog.Debug("Failed to read crush global ignore file", "path", name, "error", err)
127+ }
128+ return nil
94129 }
130+ lines := strings.Split(string(bts), "\n")
95131 return parsePatterns(lines, nil)
96132})
97133
@@ -169,8 +205,9 @@ func (dl *directoryLister) getCombinedMatcher(dir string) gitignore.Matcher {
169205 // Add common patterns first (lowest priority).
170206 allPatterns = append(allPatterns, commonIgnorePatterns()...)
171207
172− // Add home ignore patterns.
173− allPatterns = append(allPatterns, homeIgnorePatterns()...)
208+ // Add global ignore patterns (git core.excludesFile + crush global ignore).
209+ allPatterns = append(allPatterns, gitGlobalIgnorePatterns()...)
210+ allPatterns = append(allPatterns, crushGlobalIgnorePatterns()...)
174211
175212 // Collect patterns from root to this directory.
176213 relDir, _ := filepath.Rel(dl.rootPath, dir)