func getGitDir(wd string) (string, error) {
	gitDir, err := captureAbs(wd, "git", "rev-parse", "--git-dir")
	if err != nil {
		return "", fmt.Errorf("failed to find .git dir: %s", err)
	}
	return gitDir, err
}
getGitDir returns the .git directory path.
package scm
import (
	"errors"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strings"
	"sync"
	"github.com/maruel/pre-commit-go/internal"
)
type Commit string
const (
	Initial Commit = "<initial>"
	Head Commit = "<head>"
	Current Commit = "<current>"
	Upstream Commit = "<upstream>"
	Invalid Commit = "<invalid>"
)
type ReadOnlyRepo interface {
	Root() string
	ScmDir() (string, error)
	HookPath() (string, error)
	Ref(c Commit) string
	Eval(refish string) Commit
	Between(recent, old Commit, ignorePatterns IgnorePatterns) (Change, error)
	GOPATH() string
}
type Repo interface {
	ReadOnlyRepo
	Stash() (bool, error)
	Restore() error
	Checkout(refish string) error
}
func GetRepo(wd, gopath string) (Repo, error) {
	return getRepo(wd, gopath)
}
type IgnorePatterns []string
func (i *IgnorePatterns) Match(p string) bool {
	chunks := strings.Split(p, pathSeparator)
	for _, ignorePattern := range *i {
		for _, chunk := range chunks {
			if matched, err := filepath.Match(ignorePattern, chunk); matched {
				log.Printf("%s: ignored due to %q", p, ignorePattern)
				return true
			} else if err != nil {
				log.Printf("bad pattern %q", ignorePattern)
			}
		}
	}
	return false
}
func (i *IgnorePatterns) String() string {
	return fmt.Sprintf("%s", *i)
}
func (i *IgnorePatterns) Set(value string) error {
	*i = append(*i, value)
	return nil
}
var reCommit = regexp.MustCompile("^[0-9a-f]{40}$")
type repo interface {
	Repo
	untracked() []string
	unstaged() []string
	staged() []string
}
func getRepo(wd, gopath string) (repo, error) {
	root, err := captureAbs(wd, "git", "rev-parse", "--show-cdup")
	if err == nil {
		if gopath == "" {
			gopath = os.Getenv("GOPATH")
		}
		return &git{root: root, gopath: gopath}, nil
	}
	return nil, fmt.Errorf("failed to find git checkout root")
}
type gitCommit Commit
const (
	gitInitial  gitCommit = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
	gitHead     gitCommit = "HEAD"
	gitCurrent  gitCommit = "<current>"
	gitUpstream gitCommit = "@{upstream}"
	gitInvalid  gitCommit = "<invalid>"
)
func toGitCommit(c Commit) gitCommit {
	switch c {
	case Initial:
		return gitInitial
	case Head:
		return gitHead
	case Current:
		return gitCurrent
	case Upstream:
		return gitUpstream
	case Invalid, "":
		return gitInvalid
	default:
		return gitCommit(c)
	}
}
type git struct {
	root   string
	gopath string
	lock   sync.Mutex
	gitDir string
}
func (g *git) Root() string {
	return g.root
}
func (g *git) ScmDir() (string, error) {
	g.lock.Lock()
	defer g.lock.Unlock()
	if g.gitDir == "" {
		var err error
		g.gitDir, err = getGitDir(g.root)
		if err != nil {
			return "", fmt.Errorf("failed to find .git dir: %s", err)
		}
	}
	return g.gitDir, nil
}
func (g *git) HookPath() (string, error) {
	d, err := g.ScmDir()
	if err != nil {
		return "", err
	}
	return filepath.Join(d, "hooks"), nil
}
func (g *git) Ref(c Commit) string {
	gc := toGitCommit(c)
	if gc == gitInvalid {
		return string(Invalid)
	}
	if gc == gitCurrent {
		gc = gitHead
	}
	out, code, _ := g.capture("symbolic-ref", "--short", string(gc))
	if code == 0 {
		return out
	}
	log.Println(out)
	return ""
}
func (g *git) Eval(refish string) Commit {
	c := toGitCommit(Commit(refish))
	if c == gitCurrent {
		c = gitHead
	}
	if c == gitInitial {
		return Commit(gitInitial)
	}
	if c == gitInvalid {
		return Invalid
	}
	out, code, _ := g.capture("log", "-1", "--format=%H", string(c))
	if code == 0 {
		return Commit(out)
	}
	if c == gitHead {
		return Commit(gitInitial)
	}
	log.Println(out)
	return Invalid
}
func (g *git) Between(recent, old Commit, ignorePatterns IgnorePatterns) (Change, error) {
	log.Printf("Between(%q, %q, %s)", recent, old, ignorePatterns)
	grecent := toGitCommit(recent)
	if grecent == gitInvalid {
		return nil, errors.New("invalid recent commit")
	}
	if grecent != gitCurrent && !g.isValid(grecent) {
		return nil, errors.New("invalid recent commit")
	}
	gold := toGitCommit(old)
	if gold == gitInvalid {
		return nil, errors.New("invalid old commit")
	}
	if gold == gitCurrent {
		return nil, errors.New("can't use Current as old commit")
	}
	if gold != gitUpstream && gold != gitHead && !g.isValid(gold) {
		return nil, errors.New("invalid old commit")
	}
	allFilesCh := make(chan []string)
	var allFiles []string
	var files []string
	filesEqualsAllFiles := false
	if grecent == gitCurrent {
		go func() {
			allFilesCh <- g.captureList(ignorePatterns, "ls-files", "-z")
		}()
		if gold == gitInitial {
			allFiles = <-allFilesCh
			files = allFiles
			filesEqualsAllFiles = true
		} else {
			unstagedCh := make(chan []string)
			go func() {
				unstagedCh <- g.unstaged()
			}()
			stagedCh := make(chan []string)
			go func() {
				stagedCh <- g.staged()
			}()
			filesSet := map[string]struct{}{}
			for _, f := range g.captureList(ignorePatterns, "diff-tree", "--no-commit-id", "--name-only", "-z", "-r", "--diff-filter=ACMRT", "--no-renames", "--no-ext-diff", string(gold), string(gitHead)) {
				filesSet[f] = struct{}{}
			}
			for _, f := range <-unstagedCh {
				filesSet[f] = struct{}{}
			}
			for _, f := range <-stagedCh {
				filesSet[f] = struct{}{}
			}
			files = make([]string, 0, len(filesSet))
			for f := range filesSet {
				files = append(files, f)
			}
			allFiles = <-allFilesCh
		}
	} else {
		go func() {
			allFilesCh <- g.captureList(ignorePatterns, "ls-files", "-z", "--with-tree="+string(grecent))
		}()
		files = g.captureList(ignorePatterns, "diff-tree", "--no-commit-id", "--name-only", "-z", "-r", "--diff-filter=ACMRT", "--no-renames", "--no-ext-diff", string(gold), string(grecent))
		allFiles = <-allFilesCh
	}
	if len(files) == 0 {
		return nil, nil
	}
	var wg sync.WaitGroup
	if !filesEqualsAllFiles {
		wg.Add(1)
		go func() {
			defer wg.Done()
			sort.Strings(files)
		}()
	}
	sort.Strings(allFiles)
	wg.Wait()
	return newChange(g, files, allFiles, ignorePatterns), nil
}
func (g *git) GOPATH() string {
	return g.gopath
}
func (g *git) Stash() (bool, error) {
	errUntrackedCh := make(chan error)
	go func() {
		if untracked := g.untracked(); untracked == nil {
			errUntrackedCh <- errors.New("failed to get list of untracked files")
		} else if len(untracked) != 0 {
			errUntrackedCh <- fmt.Errorf("can't stash if there are untracked files: %q", untracked)
		} else {
			errUntrackedCh <- nil
		}
	}()
	errUnstagedCh := make(chan error)
	ignore := errors.New("ignore")
	go func() {
		if unstaged := g.unstaged(); unstaged == nil {
			errUnstagedCh <- errors.New("failed to get list of unstaged files")
		} else if len(unstaged) == 0 {
			errUnstagedCh <- ignore
		} else {
			errUnstagedCh <- nil
		}
	}()
	oldStashCh := make(chan string)
	go func() {
		o, _, _ := g.capture("rev-parse", "-q", "--verify", "refs/stash")
		oldStashCh <- o
	}()
	if err := <-errUntrackedCh; err != nil {
		return false, err
	}
	if err := <-errUnstagedCh; err == ignore {
		return false, nil
	} else if err != nil {
		return false, err
	}
	oldStash := <-oldStashCh
	if out, e, err := g.capture("stash", "save", "-q", "--keep-index"); e != 0 || err != nil {
		if gitCommit(g.Eval(string(gitHead))) == gitInitial {
			return false, errors.New("Can't stash until there's at least one commit")
		}
		return false, fmt.Errorf("failed to stash:\n%s", out)
	}
	newStash, e, err := g.capture("rev-parse", "-q", "--verify", "refs/stash")
	if e != 0 || err != nil {
		return false, fmt.Errorf("failed to parse stash: %s\n%s", err, newStash)
	}
	return oldStash != newStash, err
}
func (g *git) Restore() error {
	if out, e, err := g.capture("reset", "--hard", "-q"); e != 0 || err != nil {
		return fmt.Errorf("git reset failed:\n%s", out)
	}
	if out, e, err := g.capture("stash", "apply", "--index", "-q"); e != 0 || err != nil {
		return fmt.Errorf("stash reapplication failed:\n%s", out)
	}
	if out, e, err := g.capture("stash", "drop", "-q"); e != 0 || err != nil {
		return fmt.Errorf("dropping temporary stash failed:\n%s", out)
	}
	return nil
}
func (g *git) Checkout(refish string) error {
	c := toGitCommit(Commit(refish))
	if c == gitInvalid {
		return errors.New("invalid commit")
	}
	if out, e, err := g.capture("checkout", "-f", "-q", string(c)); e != 0 || err != nil {
		return fmt.Errorf("checkout failed:\n%s", out)
	}
	return nil
}
func (g *git) untracked() []string {
	return g.captureList(nil, "ls-files", "--others", "--exclude-standard", "-z")
}
func (g *git) unstaged() []string {
	return g.captureList(nil, "diff", "--name-only", "--no-color", "--no-ext-diff", "-z")
}
func (g *git) staged() []string {
	return g.captureList(nil, "diff", "--name-only", "--no-color", "--no-ext-diff", "--cached", "--diff-filter=ACMRT", "-z")
}
func (g *git) capture(args ...string) (string, int, error) {
	return g.captureEnv(nil, args...)
}
func (g *git) captureEnv(env []string, args ...string) (string, int, error) {
	out, code, err := internal.Capture(g.root, env, append([]string{"git"}, args...)...)
	return strings.TrimRight(out, "\n\r"), code, err
}
func (g *git) captureList(ignorePatterns IgnorePatterns, args ...string) []string {
	out, code, err := g.capture(args...)
	if code != 0 || err != nil {
		return nil
	}
	list := make([]string, 0, 128)
	for {
		i := strings.IndexByte(out, 0)
		if i <= 0 {
			break
		}
		s := out[:i]
		if !ignorePatterns.Match(s) {
			list = append(list, s)
		}
		out = out[i+1:]
	}
	return list
}
func (g *git) isValid(c gitCommit) bool {
	return reCommit.MatchString(string(c))
}