|  | package git_common | 
|  |  | 
|  | /* | 
|  | Common elements used by go/git and go/git/testutils. | 
|  | */ | 
|  |  | 
|  | import ( | 
|  | "context" | 
|  | "fmt" | 
|  | osexec "os/exec" | 
|  | "regexp" | 
|  | "strconv" | 
|  | "strings" | 
|  | "sync" | 
|  |  | 
|  | "go.skia.org/infra/bazel/go/bazel" | 
|  | "go.skia.org/infra/go/exec" | 
|  | "go.skia.org/infra/go/metrics2" | 
|  | "go.skia.org/infra/go/skerr" | 
|  | "go.skia.org/infra/go/sklog" | 
|  | ) | 
|  |  | 
|  | const ( | 
|  | // Apps will gradually migrate to using MainBranch instead of | 
|  | // MasterBranch as more repositories use MainBranch (skbug.com/11842). | 
|  | MasterBranch = "master" | 
|  | MainBranch   = "main" | 
|  | // DefaultRef is the fully-qualified ref name of the default branch for most | 
|  | // repositories. | 
|  | DefaultRef = RefsHeadsPrefix + MainBranch | 
|  | // DefaultRemote is the name of the default remote repository. | 
|  | DefaultRemote = "origin" | 
|  | // DefaultRemoteBranch is the name of the default branch in the default | 
|  | // remote repository, for most repos. | 
|  | DefaultRemoteBranch = DefaultRemote + "/" + MainBranch | 
|  | // RefsHeadsPrefix is the "refs/heads/" prefix used for branches. | 
|  | RefsHeadsPrefix = "refs/heads/" | 
|  |  | 
|  | gitPathFinderKey contextKeyType = "GitPathFinder" | 
|  | ) | 
|  |  | 
|  | var ( | 
|  | gitVersionRegex = regexp.MustCompile("git version (\\d+)\\.(\\d+)\\..*") | 
|  | gitVersionMajor = 0 | 
|  | gitVersionMinor = 0 | 
|  | git             = "" | 
|  | mtx             sync.Mutex // Protects git, gitVersionMajor, and gitVersionMinor. | 
|  | ) | 
|  |  | 
|  | type contextKeyType string | 
|  |  | 
|  | func findGitPath(ctx context.Context) (string, error) { | 
|  | if f := ctx.Value(gitPathFinderKey); f != nil { | 
|  | finder := f.(func() (string, error)) | 
|  | gitPath, err := finder() | 
|  | return gitPath, skerr.Wrap(err) | 
|  | } | 
|  | if bazel.InBazelTest() { | 
|  | return "", skerr.Fmt("Use git_common.WithGitFinder(cipd_git.FindGit) instead of relying on git on $PATH") | 
|  | } | 
|  | gitPath, err := osexec.LookPath("git") | 
|  | return gitPath, skerr.Wrap(err) | 
|  | } | 
|  |  | 
|  | func hasGitFinderOverride(ctx context.Context) bool { | 
|  | if f := ctx.Value(gitPathFinderKey); f != nil { | 
|  | return true | 
|  | } | 
|  | return false | 
|  | } | 
|  |  | 
|  | // WithGitFinder overrides how the git_common.FindGit() function locates the git executable. | 
|  | // By default, it looks on the PATH, but this can allow other behavior. The primary case is tests, | 
|  | // where we load in a hermetic version of git. | 
|  | func WithGitFinder(ctx context.Context, finder func() (string, error)) context.Context { | 
|  | return context.WithValue(ctx, gitPathFinderKey, finder) | 
|  | } | 
|  |  | 
|  | // FindGit returns the path to the Git executable and the major and minor | 
|  | // version numbers, or any error which occurred. | 
|  | func FindGit(ctx context.Context) (string, int, int, error) { | 
|  | mtx.Lock() | 
|  | defer mtx.Unlock() | 
|  | if git != "" && !hasGitFinderOverride(ctx) { | 
|  | // return cached version (unless there is a GitFinder on the context). | 
|  | // Since the override is primarily used by tests, we do not want to cache the results and | 
|  | // have test behavior depend on the order tests were executed (e.g. if one test uses | 
|  | // mockGitA and another uses mockGitB, caching would make both use A or both use B). | 
|  | return git, gitVersionMajor, gitVersionMinor, nil | 
|  | } | 
|  | gitPath, err := findGitPath(ctx) | 
|  | if err != nil { | 
|  | return "", 0, 0, skerr.Wrapf(err, "Failed to find git") | 
|  | } | 
|  | maj, min, err := Version(ctx, gitPath) | 
|  | if err != nil { | 
|  | return "", 0, 0, skerr.Wrapf(err, "Failed to obtain git version") | 
|  | } | 
|  | sklog.Infof("Git is %s; version %d.%d", gitPath, maj, min) | 
|  | isFromCIPD := IsFromCIPD(gitPath) | 
|  | isFromCIPDVal := 0 | 
|  | if isFromCIPD { | 
|  | isFromCIPDVal = 1 | 
|  | } | 
|  | metrics2.GetInt64Metric("git_from_cipd").Update(int64(isFromCIPDVal)) | 
|  | git = gitPath | 
|  | gitVersionMajor = maj | 
|  | gitVersionMinor = min | 
|  | return git, gitVersionMajor, gitVersionMinor, nil | 
|  | } | 
|  |  | 
|  | // IsFromCIPD returns a bool indicating whether or not the given version of Git | 
|  | // appears to be obtained via CIPD. | 
|  | func IsFromCIPD(git string) bool { | 
|  | return strings.Contains(git, "cipd_bin_packages") || (bazel.InBazel() && strings.Contains(git, bazel.RunfilesDir())) | 
|  | } | 
|  |  | 
|  | // EnsureGitIsFromCIPD returns an error if the version of Git in PATH does not | 
|  | // appear to be obtained via CIPD. | 
|  | func EnsureGitIsFromCIPD(ctx context.Context) error { | 
|  | git, _, _, err := FindGit(ctx) | 
|  | if err != nil { | 
|  | return skerr.Wrap(err) | 
|  | } | 
|  | if !IsFromCIPD(git) { | 
|  | return skerr.Fmt("Git does not appear to be obtained via CIPD: %s", git) | 
|  | } | 
|  | return nil | 
|  | } | 
|  |  | 
|  | // Version returns the installed Git version, in the form: | 
|  | // (major, minor), or an error if it could not be determined. | 
|  | func Version(ctx context.Context, git string) (int, int, error) { | 
|  | out, err := exec.RunCwd(ctx, ".", git, "--version") | 
|  | if err != nil { | 
|  | return -1, -1, err | 
|  | } | 
|  | m := gitVersionRegex.FindStringSubmatch(out) | 
|  | if m == nil { | 
|  | return -1, -1, fmt.Errorf("Failed to parse the git version from output: %q", out) | 
|  | } | 
|  | if len(m) != 3 { | 
|  | return -1, -1, fmt.Errorf("Failed to parse the git version from output: %q", out) | 
|  | } | 
|  | major, err := strconv.Atoi(m[1]) | 
|  | if err != nil { | 
|  | return -1, -1, fmt.Errorf("Failed to parse the git version from output: %q", out) | 
|  | } | 
|  | minor, err := strconv.Atoi(m[2]) | 
|  | if err != nil { | 
|  | return -1, -1, fmt.Errorf("Failed to parse the git version from output: %q", out) | 
|  | } | 
|  | return major, minor, nil | 
|  | } | 
|  |  | 
|  | // MocksForFindGit returns a DelegateRun func which can be passed to | 
|  | // exec.CommandCollector.SetDelegateRun so that FindGit will succeed when calls | 
|  | // to exec are fully mocked out. | 
|  | func MocksForFindGit(ctx context.Context, cmd *exec.Command) error { | 
|  | if strings.Contains(cmd.Name, "git") && len(cmd.Args) == 1 && cmd.Args[0] == "--version" { | 
|  | _, err := cmd.CombinedOutput.Write([]byte("git version 99.99.1")) | 
|  | return err | 
|  | } | 
|  | return nil | 
|  | } |