blob: 9e0cfc3be37c642f39b2af8f82a87c2a8ad72b50 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package gitutil
import (
"bytes"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"go.chromium.org/luci/common/data/stringset"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/sync/parallel"
)
// RefExists returns true if the git ref exists.
func RefExists(repoDir, ref string) (bool, error) {
// Pass -- so that git knows that the argument after rev-parse is a ref
// and not a file path.
switch _, err := Exec(repoDir)("rev-parse", ref, "--"); {
case err == nil:
return true, nil
case strings.Contains(err.Error(), "bad revision"):
return false, nil
default:
return false, err
}
}
// EnsureSameRepo ensures that all files belong to the same git repository
// and returns its absolute path.
func EnsureSameRepo(files ...string) (repoDir string, err error) {
if len(files) == 0 {
return "", errors.New("no files")
}
// Read the repo dir of the first file.
if repoDir, err = TopLevel(files[0]); err != nil {
return "", errors.Annotate(err, "file %q", files[0]).Err()
}
// Check the rest of the files concurrently.
fileSet := stringset.NewFromSlice(files[1:]...)
fileSet.Del(files[0]) // already checked.
if len(fileSet) == 0 {
return repoDir, nil
}
workers := runtime.GOMAXPROCS(0)
if workers > len(fileSet) {
workers = len(fileSet)
}
err = parallel.WorkPool(workers, func(work chan<- func() error) {
for f := range fileSet {
f := f
work <- func() error {
switch fRepo, err := TopLevel(f); {
case err != nil:
return errors.Annotate(err, "file %q", f).Err()
case repoDir != fRepo:
return errors.Reason("%q and %q reside in different git repositories", files[0], f).Err()
default:
return nil
}
}
}
})
// On Windows, git produces slash-based paths.
repoDir = filepath.FromSlash(repoDir)
return repoDir, err
}
// TopLevel returns the repository dir for the path.
func TopLevel(path string) (string, error) {
return Exec(path)("rev-parse", "--show-toplevel")
}
// Exec returns a function that executes a git command and returns its
// standard output.
// The context must be a path to an existing file or directory.
//
// It is suitable only for commands that exit quickly and have small
// output, e.g. rev-parse.
func Exec(context string) func(args ...string) (out string, err error) {
exe := "git"
if runtime.GOOS == "windows" {
exe = "git.exe"
}
return func(args ...string) (string, error) {
dir, err := dirFromPath(context)
if err != nil {
return "", err
}
args = append([]string{"-C", dir}, args...)
cmd := exec.Command(exe, args...)
var stderr bytes.Buffer
cmd.Stderr = &stderr
outBytes, err := cmd.Output()
out := strings.TrimSuffix(string(outBytes), "\n")
return out, errors.Annotate(err, "git %q failed; output: %q", args, stderr.Bytes()).Err()
}
}
// dirFromPath returns fileName if it is a dir, otherwise returns fileName's
// dir. The file/dir must exist.
func dirFromPath(fileName string) (dir string, err error) {
switch stat, err := os.Stat(fileName); {
case err != nil:
return "", err
case stat.IsDir():
return fileName, nil
default:
return filepath.Dir(fileName), nil
}
}