@@ -105,6 +105,7 @@ require (
github.com/charmbracelet/x/json v0.2.0 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
+ github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
@@ -156,6 +157,7 @@ require (
github.com/ncruces/julianday v1.0.0 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/pierrec/lz4/v4 v4.1.25 // indirect
+ github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/segmentio/asm v1.2.1 // indirect
@@ -141,6 +141,8 @@ github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7X
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
+github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
+github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.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
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
+github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
+github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
+github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
+github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -1,6 +1,7 @@
package fsext
import (
+ "cmp"
"errors"
"log/slog"
"os"
@@ -12,6 +13,7 @@ import (
"github.com/charlievieth/fastwalk"
"github.com/charmbracelet/crush/internal/csync"
"github.com/charmbracelet/crush/internal/home"
+ gitconfig "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing/format/gitignore"
)
@@ -80,18 +82,52 @@ var commonIgnorePatterns = sync.OnceValue(func() []gitignore.Pattern {
return parsePatterns(patterns, nil)
})
-var homeIgnorePatterns = sync.OnceValue(func() []gitignore.Pattern {
- homeDir := home.Dir()
- var lines []string
- for _, name := range []string{
- filepath.Join(homeDir, ".gitignore"),
- filepath.Join(homeDir, ".config", "git", "ignore"),
- filepath.Join(homeDir, ".config", "crush", "ignore"),
- } {
- if bts, err := os.ReadFile(name); err == nil {
- lines = append(lines, strings.Split(string(bts), "\n")...)
+// gitGlobalIgnorePatterns returns patterns from git's global excludes file
+// (core.excludesFile), following git's config resolution order.
+var gitGlobalIgnorePatterns = sync.OnceValue(func() []gitignore.Pattern {
+ cfg, err := gitconfig.LoadConfig(gitconfig.GlobalScope)
+ if err != nil {
+ slog.Debug("Failed to load global git config", "error", err)
+ return nil
+ }
+
+ configPath := cmp.Or(
+ os.Getenv("XDG_CONFIG_HOME"),
+ filepath.Join(home.Dir(), ".config"),
+ )
+ excludesFilePath := cmp.Or(
+ cfg.Raw.Section("core").Options.Get("excludesfile"),
+ filepath.Join(configPath, "git", "ignore"),
+ )
+ excludesFilePath = home.Long(excludesFilePath)
+
+ bts, err := os.ReadFile(excludesFilePath)
+ if err != nil {
+ if !os.IsNotExist(err) {
+ slog.Debug("Failed to read git global excludes file", "path", excludesFilePath, "error", err)
}
+ return nil
+ }
+
+ return parsePatterns(strings.Split(string(bts), "\n"), nil)
+})
+
+// crushGlobalIgnorePatterns returns patterns from the user's
+// ~/.config/crush/ignore file.
+var crushGlobalIgnorePatterns = sync.OnceValue(func() []gitignore.Pattern {
+ configPath := cmp.Or(
+ os.Getenv("XDG_CONFIG_HOME"),
+ filepath.Join(home.Dir(), ".config"),
+ )
+ name := filepath.Join(configPath, "crush", "ignore")
+ bts, err := os.ReadFile(name)
+ if err != nil {
+ if !os.IsNotExist(err) {
+ slog.Debug("Failed to read crush global ignore file", "path", name, "error", err)
+ }
+ return nil
}
+ lines := strings.Split(string(bts), "\n")
return parsePatterns(lines, nil)
})
@@ -169,8 +205,9 @@ func (dl *directoryLister) getCombinedMatcher(dir string) gitignore.Matcher {
// Add common patterns first (lowest priority).
allPatterns = append(allPatterns, commonIgnorePatterns()...)
- // Add home ignore patterns.
- allPatterns = append(allPatterns, homeIgnorePatterns()...)
+ // Add global ignore patterns (git core.excludesFile + crush global ignore).
+ allPatterns = append(allPatterns, gitGlobalIgnorePatterns()...)
+ allPatterns = append(allPatterns, crushGlobalIgnorePatterns()...)
// Collect patterns from root to this directory.
relDir, _ := filepath.Rel(dl.rootPath, dir)