pax_global_header00006660000000000000000000000064144540321010014504gustar00rootroot0000000000000052 comment=a71b4793fd302de7029da975920c9ecae40bf220 gitcha-0.3.0/000077500000000000000000000000001445403210100127435ustar00rootroot00000000000000gitcha-0.3.0/.github/000077500000000000000000000000001445403210100143035ustar00rootroot00000000000000gitcha-0.3.0/.github/FUNDING.yml000066400000000000000000000000171445403210100161160ustar00rootroot00000000000000github: muesli gitcha-0.3.0/.github/dependabot.yml000066400000000000000000000004231445403210100171320ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "gomod" directory: "/" schedule: interval: "daily" labels: - "dependencies" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" labels: - "dependencies" gitcha-0.3.0/.github/workflows/000077500000000000000000000000001445403210100163405ustar00rootroot00000000000000gitcha-0.3.0/.github/workflows/build.yml000066400000000000000000000011171445403210100201620ustar00rootroot00000000000000name: build on: [push, pull_request] jobs: build: strategy: matrix: go-version: [~1.15, ^1] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} env: GO111MODULE: "on" steps: - name: Install Go uses: actions/setup-go@v4 with: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v3 - name: Download Go modules run: go mod download - name: Build run: go build -v ./... - name: Test run: go test ./... gitcha-0.3.0/.github/workflows/coverage.yml000066400000000000000000000013211445403210100206530ustar00rootroot00000000000000name: coverage on: [push, pull_request] jobs: coverage: strategy: matrix: go-version: [^1] os: [ubuntu-latest] runs-on: ${{ matrix.os }} env: GO111MODULE: "on" steps: - name: Install Go uses: actions/setup-go@v4 with: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v3 - name: Coverage env: COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | go test -race -covermode atomic -coverprofile=profile.cov ./... GO111MODULE=off go get github.com/mattn/goveralls $(go env GOPATH)/bin/goveralls -coverprofile=profile.cov -service=github gitcha-0.3.0/.gitignore000066400000000000000000000004151445403210100147330ustar00rootroot00000000000000# Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ gitcha-0.3.0/LICENSE000066400000000000000000000020671445403210100137550ustar00rootroot00000000000000MIT License Copyright (c) 2020 Christian Muehlhaeuser Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. gitcha-0.3.0/README.md000066400000000000000000000025111445403210100142210ustar00rootroot00000000000000# gitcha [![Latest Release](https://img.shields.io/github/release/muesli/gitcha.svg)](https://github.com/muesli/gitcha/releases) [![Build Status](https://github.com/muesli/gitcha/workflows/build/badge.svg)](https://github.com/muesli/gitcha/actions) [![Coverage Status](https://coveralls.io/repos/github/muesli/gitcha/badge.svg?branch=master)](https://coveralls.io/github/muesli/gitcha?branch=master) [![Go ReportCard](https://goreportcard.com/badge/muesli/gitcha)](https://goreportcard.com/report/muesli/gitcha) [![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://pkg.go.dev/github.com/muesli/gitcha) Go helpers to work with git repositories ## Examples ```go import "github.com/muesli/gitcha" // returns the directory of the git repository path is a member of: repo, err := gitcha.GitRepoForPath(path) // finds files from list in path. It respects all .gitignores it finds while // traversing paths: ch, err := gitcha.FindFiles(path, []string{"*.md"}) for v := range ch { fmt.Println(v.Path) } // finds files, excluding any matches in a given set of ignore patterns: ch, err := gitcha.FindFilesExcept(path, []string{"*.md"}, []string{".*"}) ... // if you are only interested in the first match: result, err := gitcha.FindFirstFile(path, []string{"*.md"}) ... // just for convenience: ok := gitcha.IsPathInGit(path) ``` gitcha-0.3.0/gitcha.go000066400000000000000000000103161445403210100145320ustar00rootroot00000000000000package gitcha import ( "os" "path/filepath" "strings" ignore "github.com/sabhiram/go-gitignore" ) // SearchResult combines the absolute path of a file with a FileInfo struct. type SearchResult struct { Path string Info os.FileInfo } // GitRepoForPath returns the directory of the git repository path is a member // of, or an error. func GitRepoForPath(path string) (string, error) { dir, err := filepath.Abs(path) if err != nil { return "", err } for { st, err := os.Stat(filepath.Join(dir, ".git")) if err == nil && st.IsDir() { return dir, nil } // reached root? if dir == filepath.Dir(dir) { return "", nil } // check parent dir dir = filepath.Dir(dir) } } // IsPathInGit returns true when a path is part of a git repository. func IsPathInGit(path string) bool { p, err := GitRepoForPath(path) if err != nil { return false } return len(p) > 0 } // FindAllFiles finds all files from list in path. It does not respect any // gitignore files. func FindAllFiles(path string, list []string) (chan SearchResult, error) { return findFiles(path, list, nil, false) } // FindAllFilesExcept finds all files from list in path. It does not respect any // gitignore files. func FindAllFilesExcept(path string, list, ignorePatterns []string) (chan SearchResult, error) { return findFiles(path, list, ignorePatterns, false) } // FindFiles finds files from list in path. It respects all .gitignores it finds // while traversing paths. func FindFiles(path string, list []string) (chan SearchResult, error) { return findFiles(path, list, nil, true) } // FindFilesExcept finds files from a list in a path, excluding any matches in // a given set of ignore patterns. It also respects all .gitignores it finds // while traversing paths. func FindFilesExcept(path string, list, ignorePatterns []string) (chan SearchResult, error) { return findFiles(path, list, ignorePatterns, true) } // FindFirstFile looks for files from a list in a path, returning the first // match it finds. It respects all .gitignores it finds along the way. func FindFirstFile(path string, list []string) (SearchResult, error) { ch, err := FindFilesExcept(path, list, nil) if err != nil { return SearchResult{}, err } for v := range ch { return v, nil } return SearchResult{}, nil } func findFiles(path string, list, ignorePatterns []string, respectGitIgnore bool) (chan SearchResult, error) { path, err := filepath.Abs(path) if err != nil { return nil, err } path, err = filepath.EvalSymlinks(path) if err != nil { return nil, err } st, err := os.Stat(path) if err != nil { return nil, err } if !st.IsDir() { return nil, err } ch := make(chan SearchResult) go func() { defer close(ch) var lastGit string var gi *ignore.GitIgnore _ = filepath.Walk(path, func(path string, info os.FileInfo, err error) error { if respectGitIgnore { git, _ := GitRepoForPath(path) if git != "" && git != path { if lastGit != git { lastGit = git gi, err = ignore.CompileIgnoreFile(filepath.Join(git, ".gitignore")) } if err == nil && gi != nil && gi.MatchesPath(strings.TrimPrefix(path, lastGit)) { if info.IsDir() { return filepath.SkipDir } return nil } } } for _, pattern := range ignorePatterns { // If there's no path separator in the pattern try to match // against the directory we're currently walking. if !strings.Contains(pattern, string(os.PathSeparator)) { dir := filepath.Dir(path) if dir == "." { continue // path is empty } pattern = filepath.Join(dir, pattern) } matched, err := filepath.Match(pattern, path) if err != nil { continue } if matched && info.IsDir() { return filepath.SkipDir } if matched { return nil } } for _, v := range list { matched := strings.EqualFold(filepath.Base(path), v) if !matched { matched, _ = filepath.Match(strings.ToLower(v), strings.ToLower(filepath.Base(path))) } if matched { res, err := filepath.Abs(path) if err == nil { ch <- SearchResult{ Path: res, Info: info, } } // only match each path once return nil } } return nil }) }() return ch, nil } gitcha-0.3.0/gitcha_test.go000066400000000000000000000066261445403210100156020ustar00rootroot00000000000000package gitcha import ( "io/ioutil" "os" "path/filepath" "testing" ) func TestGitRepoForPath(t *testing.T) { abs, _ := filepath.Abs(".") tt := []struct { path string exp string }{ {"/", ""}, {".", abs}, {"gitcha.go", abs}, } for _, test := range tt { r, err := GitRepoForPath(test.path) if err != nil { t.Error(err) } if test.exp != r { t.Errorf("Expected %v, got %v for %s", test.exp, r, test.path) } } } func TestFindAllFiles(t *testing.T) { tmp := t.TempDir() gitignore, err := os.Create(filepath.Join(tmp, ".gitignore")) if err != nil { t.Fatal(err) } defer gitignore.Close() _, err = gitignore.WriteString("*.test") if err != nil { t.Fatal(err) } tt := []struct { path string list []string exp string }{ {tmp, []string{"*.test"}, "ignore.test"}, } for _, test := range tt { f, err := os.Create(filepath.Join(tmp, test.exp)) if err != nil { t.Fatal(err) } defer f.Close() ch, err := FindAllFiles(test.path, test.list) if err != nil { t.Fatal(err) } var counter int for v := range ch { counter++ if test.exp != v.Info.Name() { t.Errorf("Expected %v, got %v for %s", test.exp, v.Path, test.path) } } if counter != 1 { t.Errorf("Expected 1 file found, got %d for %s", counter, test.path) } } } func TestFindFiles(t *testing.T) { tlink, err := tempLink(".") if err != nil { t.Fatal(err) } defer os.Remove(tlink) tt := []struct { path string list []string exp string }{ {"../", []string{"gitcha.go"}, "gitcha.go"}, {".", []string{"gitcha_test.go"}, "gitcha_test.go"}, {".", []string{"README.MD"}, "README.md"}, {".", []string{"*.md"}, "README.md"}, {".", []string{"*.MD"}, "README.md"}, {tlink, []string{"gitcha.go"}, "gitcha.go"}, } for _, test := range tt { ch, err := FindFiles(test.path, test.list) if err != nil { t.Fatal(err) } for v := range ch { var err error test.exp, err = filepath.Abs(test.exp) if err != nil { t.Fatal(err) } if test.exp != v.Path { t.Errorf("Expected %v, got %v for %s", test.exp, v, test.path) } } } } func TestFindFirstFile(t *testing.T) { tt := []struct { path string list []string exp string expErr bool }{ {"../", []string{"gitcha.go"}, "gitcha.go", false}, {".", []string{"gitcha_test.go"}, "gitcha_test.go", false}, {".", []string{"README.MD"}, "README.md", false}, {".", []string{"*.md"}, "README.md", false}, {".", []string{"*.MD"}, "README.md", false}, } for _, test := range tt { r, err := FindFirstFile(test.path, test.list) if err != nil && !test.expErr { t.Error(err) } if err == nil && test.expErr { t.Errorf("Expected error, got none for %s", test.path) } if err != nil && test.expErr { continue } test.exp, err = filepath.Abs(test.exp) if err != nil { t.Fatal(err) } if test.exp != r.Path { t.Errorf("Expected %v, got %v for %s", test.exp, r, test.path) } } } // tempLink creates a temporary symbolic link pointing to "dest". func tempLink(dest string) (string, error) { // Use tempfile just to create a name and remove it afterwards. tmp, err := ioutil.TempFile("", "gitcha_test") if err != nil { return "", err } tmp.Close() if err := os.Remove(tmp.Name()); err != nil { return "", err } d, err := filepath.Abs(dest) if err != nil { return "", err } if err := os.Symlink(d, tmp.Name()); err != nil { return "", err } return tmp.Name(), nil } gitcha-0.3.0/go.mod000066400000000000000000000002531445403210100140510ustar00rootroot00000000000000module github.com/muesli/gitcha go 1.15 require ( github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94 github.com/stretchr/testify v1.6.1 // indirect ) gitcha-0.3.0/go.sum000066400000000000000000000023571445403210100141050ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94 h1:G04eS0JkAIVZfaJLjla9dNxkJCPiKIGZlw9AfOhzOD0= github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94/go.mod h1:b18R55ulyQ/h3RaWyloPyER7fWQVZvimKKhnI5OfrJQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=