package git import ( "fmt" "os" "path/filepath" "strings" "syscall" "github.com/karrick/godirwalk" "github.com/pkg/errors" ) // errSkipNode is used as an error indicating that .git directory has been found. // It's handled by ErrorsCallback to tell the WalkCallback to skip this dir. var errSkipNode = errors.New(".git directory found, skipping this node") // errDirectoryAccess indicates a directory doesn't exists or can't be accessed var errDirectoryAccess = errors.New("directory doesn't exist or can't be accessed") // Exists returns true if a directory exists. If it doesn't or the directory can't be accessed it returns an error. func Exists(path string) (bool, error) { _, err := os.Stat(path) if err == nil { return true, nil } if err != nil { if os.IsNotExist(err) { return false, errDirectoryAccess } } // Directory exists but can't be accessed return true, errDirectoryAccess } // RepoFinder finds paths to git repos inside given path. type RepoFinder struct { root string repos []string } // NewRepoFinder returns a RepoFinder pointed at given root path. func NewRepoFinder(root string) *RepoFinder { return &RepoFinder{ root: root, } } // Find returns a sorted list of paths to git repos found inside a given root path. // Returns error if root repo path can't be found or accessed. func (r *RepoFinder) Find() ([]string, error) { if _, err := Exists(r.root); err != nil { return nil, err } walkOpts := &godirwalk.Options{ ErrorCallback: r.errorCb, Callback: r.walkCb, // Use Unsorted to improve speed because repos will be processed by goroutines in a random order anyway. Unsorted: true, } err := godirwalk.Walk(r.root, walkOpts) if err != nil { return nil, err } if len(r.repos) == 0 { return nil, fmt.Errorf("no git repos found in root path %s", r.root) } return r.repos, nil } func (r *RepoFinder) walkCb(path string, ent *godirwalk.Dirent) error { // Do not traverse .git directories if ent.IsDir() && ent.Name() == ".git" { r.repos = append(r.repos, strings.TrimSuffix(path, ".git")) return errSkipNode } // Do not traverse directories containing a .git directory if ent.IsDir() { _, err := os.Stat(filepath.Join(path, ".git")) if err == nil { r.repos = append(r.repos, strings.TrimSuffix(path, ".git")) return ErrSkipNode } } return nil } func (r *RepoFinder) errorCb(_ string, err error) godirwalk.ErrorAction { // Skip .git directory and directories we don't have permissions to access // TODO: Will syscall.EACCES work on windows? if errors.Is(err, errSkipNode) || errors.Is(err, syscall.EACCES) { return godirwalk.SkipNode } return godirwalk.Halt }