diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1a4473..0f2d9b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,8 +10,8 @@ jobs: matrix: platform: - ubuntu-22.04 - - macos-13 - - windows-2022 +# - macos-13 +# - windows-2022 fail-fast: false runs-on: ${{ matrix.platform }} steps: @@ -98,7 +98,7 @@ jobs: ${{ steps.setup-go.outputs.GOMODCACHE }} key: ${{ runner.os }}-go-release-${{ hashFiles('**/go.sum') }} restore-keys: ${{ runner.os }}-go-release - - uses: WillAbides/release-train@v3.2.1 + - uses: WillAbides/release-train@v3.3.0 id: release-train with: create-release: true diff --git a/go.mod b/go.mod index 4cdabc4..eeef80c 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/mholt/archiver/v4 v4.0.0-alpha.8 github.com/posener/complete v1.2.3 github.com/rogpeppe/go-internal v1.10.0 + github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 github.com/santhosh-tekuri/jsonschema/v5 v5.3.0 github.com/stretchr/testify v1.7.1 github.com/willabides/kongplete v0.3.0 diff --git a/go.sum b/go.sum index 30d635a..24a6cb9 100644 --- a/go.sum +++ b/go.sum @@ -167,6 +167,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= +github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI= +github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= github.com/santhosh-tekuri/jsonschema/v5 v5.3.0 h1:uIkTLo0AGRc8l7h5l9r+GcYi9qfVPt6lD4/bhmzfiKo= github.com/santhosh-tekuri/jsonschema/v5 v5.3.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/internal/bindown/config.go b/internal/bindown/config.go index baaee9b..bf66355 100644 --- a/internal/bindown/config.go +++ b/internal/bindown/config.go @@ -656,7 +656,10 @@ func NewConfig(ctx context.Context, cfgSrc string, noDefaultDirs bool) (*Config, return cfg, nil } if cfg.Cache == "" { - cfg.Cache = filepath.Join(filepath.Dir(cfgSrc), ".cache") + cfg.Cache, err = findCacheDir(filepath.Dir(cfgSrc)) + if err != nil { + return nil, err + } } if cfg.InstallDir == "" { cfg.InstallDir = filepath.Join(filepath.Dir(cfgSrc), "bin") @@ -664,6 +667,41 @@ func NewConfig(ctx context.Context, cfgSrc string, noDefaultDirs bool) (*Config, return cfg, nil } +// findCacheDir decides between .bindown and .cache for the cache directory to use when +// none is specified. This is necessary because v4 mistakenly made .cache the default. +// We want to use .bindown, but will revert to .cache if it is in .gitignore and .bindown +// does not exist. +func findCacheDir(cfgDir string) (string, error) { + // if .bindown exists, use it + bindownDir := filepath.Join(cfgDir, ".bindown") + info, err := os.Stat(bindownDir) + if err != nil && !os.IsNotExist(err) { + return "", err + } + if err == nil && info.IsDir() { + return bindownDir, nil + } + // if .bindown is in .gitignore, use it + ig, err := dirIsGitIgnored(bindownDir) + if err != nil { + return "", err + } + if ig { + return bindownDir, nil + } + // if .cache is in .gitignore, use it + cacheDir := filepath.Join(cfgDir, ".cache") + ig, err = dirIsGitIgnored(cacheDir) + if err != nil { + return "", err + } + if ig { + return cacheDir, nil + } + // default to .bindown + return bindownDir, nil +} + func configFromHTTP(ctx context.Context, src string) (*Config, error) { req, err := http.NewRequestWithContext(ctx, "GET", src, http.NoBody) if err != nil { diff --git a/internal/bindown/util.go b/internal/bindown/util.go index d8f15ca..e8734e9 100644 --- a/internal/bindown/util.go +++ b/internal/bindown/util.go @@ -15,6 +15,7 @@ import ( "text/template" "github.com/Masterminds/semver/v3" + ignore "github.com/sabhiram/go-gitignore" "golang.org/x/exp/maps" "golang.org/x/exp/slices" "gopkg.in/yaml.v3" @@ -228,3 +229,76 @@ func Unique[V comparable](vals, buf []V) []V { } return buf } + +func dirIsGitIgnored(dir string) (bool, error) { + ig, err := fileIsGitignored(dir) + if err != nil { + return false, err + } + if ig { + return true, nil + } + return fileIsGitignored(filepath.Join(dir, "x")) +} + +// fileIsGitignored returns true if the file is ignored by a .gitignore file in the same directory or any parent +// directory from the same git repo. +func fileIsGitignored(filename string) (bool, error) { + dir := filepath.Dir(filename) + repoBase, err := gitRepo(dir) + if err != nil { + return false, err + } + if repoBase == "" { + return false, nil + } + for { + ignoreFile := filepath.Join(dir, ".gitignore") + var info os.FileInfo + info, err = os.Stat(ignoreFile) + if err != nil { + if !os.IsNotExist(err) { + return false, err + } + } else if info.Mode().Type().IsRegular() { + var ig *ignore.GitIgnore + ig, err = ignore.CompileIgnoreFile(ignoreFile) + if err != nil { + return false, err + } + var relFile string + relFile, err = filepath.Rel(dir, filename) + if err != nil { + return false, err + } + if ig.MatchesPath(relFile) { + return true, nil + } + } + if dir == repoBase { + break + } + dir = filepath.Dir(dir) + } + return false, nil +} + +// gitRepo returns the path of the base git repo for a dir. Returns "" +// if dir is not in a git repo. +// Does not use git commands. Just checks for .git directory. +func gitRepo(dir string) (string, error) { + dir = filepath.Clean(dir) + info, err := os.Stat(filepath.Join(dir, ".git")) + if err != nil { + if !os.IsNotExist(err) { + return "", err + } + } else if info.IsDir() { + return dir, nil + } + parent := filepath.Dir(dir) + if parent == dir { + return "", nil + } + return gitRepo(parent) +}