blob: 7fe0c5d44c0724248a9004dc18569a1d30a21e5e [file] [log] [blame]
package git
/*
Common utils used by Repo and Checkout.
*/
import (
"context"
"fmt"
"os"
"path"
"regexp"
"strconv"
"strings"
"time"
"go.skia.org/infra/go/exec"
"go.skia.org/infra/go/git/git_common"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/vcsinfo"
)
const gitlinkMode = "160000"
var ErrorNotSubmodule = skerr.Fmt("not a submodule")
var ErrorNotFound = skerr.Fmt("file not found")
var lsTreeRe = regexp.MustCompile(`(\d+) (\w+) ([[:xdigit:]]+)\t(.*)`)
// Branch describes a Git branch.
type Branch struct {
// The human-readable name of the branch.
Name string `json:"name"`
// The commit hash pointed to by this branch.
Head string `json:"head"`
}
// BranchList is a slice of Branch objects which implements sort.Interface.
type BranchList []*Branch
func (bl BranchList) Len() int { return len(bl) }
func (bl BranchList) Less(a, b int) bool { return bl[a].Name < bl[b].Name }
func (bl BranchList) Swap(a, b int) { bl[a], bl[b] = bl[b], bl[a] }
// Executable returns the path to Git.
func Executable(ctx context.Context) (string, error) {
git, _, _, err := git_common.FindGit(ctx)
return git, err
}
// GitDir is a directory in which one may run Git commands.
type GitDir string
// newGitDir creates a GitDir instance based in the given directory.
func newGitDir(ctx context.Context, repoUrl, workdir string, mirror bool) (GitDir, error) {
dest := path.Join(workdir, strings.TrimSuffix(path.Base(repoUrl), ".git"))
if _, err := os.Stat(dest); err != nil {
if os.IsNotExist(err) {
if err := Clone(ctx, repoUrl, dest, mirror); err != nil {
return "", err
}
} else {
return "", fmt.Errorf("There is a problem with the git directory: %s", err)
}
}
return GitDir(dest), nil
}
// Dir returns the working directory of the GitDir.
func (g GitDir) Dir() string {
return string(g)
}
// Git runs the given git command in the GitDir.
func (g GitDir) Git(ctx context.Context, cmd ...string) (string, error) {
git, err := Executable(ctx)
if err != nil {
return "", skerr.Wrap(err)
}
return exec.RunCwd(ctx, g.Dir(), append([]string{git}, cmd...)...)
}
// Details returns a vcsinfo.LongCommit instance representing the given commit.
func (g GitDir) Details(ctx context.Context, name string) (*vcsinfo.LongCommit, error) {
output, err := g.Git(ctx, "log", "-n", "1", "--format=format:%H%n%P%n%an%x20(%ae)%n%s%n%ct%n%b", name)
if err != nil {
return nil, err
}
lines := strings.SplitN(output, "\n", 6)
if len(lines) != 6 {
return nil, fmt.Errorf("Failed to parse output of 'git log'.")
}
var parents []string
if lines[1] != "" {
parents = strings.Split(lines[1], " ")
}
ts, err := strconv.ParseInt(lines[4], 10, 64)
if err != nil {
return nil, err
}
return &vcsinfo.LongCommit{
ShortCommit: &vcsinfo.ShortCommit{
Hash: lines[0],
Author: lines[2],
Subject: lines[3],
},
Parents: parents,
Body: strings.TrimRight(lines[5], "\n"),
Timestamp: time.Unix(ts, 0).UTC(),
}, nil
}
// RevParse runs "git rev-parse <name>" and returns the result.
func (g GitDir) RevParse(ctx context.Context, args ...string) (string, error) {
out, err := g.Git(ctx, append([]string{"rev-parse"}, args...)...)
if err != nil {
return "", err
}
// Ensure that we got a single, 40-character commit hash.
split := strings.Fields(out)
if len(split) != 1 {
return "", fmt.Errorf("Unable to parse commit hash from output: %s", out)
}
if len(split[0]) != 40 {
return "", fmt.Errorf("rev-parse returned invalid commit hash: %s", out)
}
return split[0], nil
}
// RevList runs "git rev-list <name>" and returns a slice of commit hashes.
func (g GitDir) RevList(ctx context.Context, args ...string) ([]string, error) {
out, err := g.Git(ctx, append([]string{"rev-list"}, args...)...)
if err != nil {
return nil, err
}
return strings.Fields(out), nil
}
// GetBranchHead returns the commit hash at the HEAD of the given branch.
func (g GitDir) GetBranchHead(ctx context.Context, branchName string) (string, error) {
return g.RevParse(ctx, "--verify", fmt.Sprintf("refs/heads/%s^{commit}", branchName))
}
// Branches runs "git branch" and returns a slice of Branch instances.
func (g GitDir) Branches(ctx context.Context) ([]*Branch, error) {
out, err := g.Git(ctx, "branch")
if err != nil {
return nil, err
}
branchNames := strings.Fields(out)
branches := make([]*Branch, 0, len(branchNames))
for _, name := range branchNames {
if name == "*" {
continue
}
head, err := g.GetBranchHead(ctx, name)
if err != nil {
return nil, err
}
branches = append(branches, &Branch{
Head: head,
Name: name,
})
}
return branches, nil
}
// GetFile returns the contents of the given file at the given commit.
func (g GitDir) GetFile(ctx context.Context, fileName, commit string) (string, error) {
return g.Git(ctx, "show", commit+":"+fileName)
}
// IsSubmodule returns true if the given path is submodule, ie contains gitlink.
func (g GitDir) IsSubmodule(ctx context.Context, path, commit string) (bool, error) {
_, err := g.ReadSubmodule(ctx, path, commit)
switch skerr.Unwrap(err) {
case ErrorNotSubmodule:
return false, nil
case nil:
return true, nil
default:
return false, err
}
}
// ReadSubmodule returns commit hash of the given path, if the path is git
// submodule. ErrorNotFound is returned if path is not found in the git
// worktree. ErrorNotSubmodule is returned if path exists, but it's not a
// submodule.
func (g GitDir) ReadSubmodule(ctx context.Context, path, commit string) (string, error) {
// Detect if we are dealing with submodules or regular files.
// Expected output for submodules:
// <mode> SP <type> SP <object> TAB <file>
out, err := g.Git(ctx, "ls-tree", commit, "--", path)
if err != nil {
return "", skerr.Wrap(err)
}
matches := lsTreeRe.FindAllStringSubmatch(out, -1)
if len(matches) != 1 {
// We expect one match. If is non one, it's either not found or
// it's a tree. In either case, we return not found.
return "", skerr.Wrap(ErrorNotFound)
}
if matches[0][1] != gitlinkMode {
return "", skerr.Wrap(ErrorNotSubmodule)
}
return matches[0][3], nil
}
// UpdateSubmodule updates git submodule of the given path to the given commit.
// If submodule doesn't exist, it returns ErrorNotFound since it doesn't have
// all necessary information to create a valid submodule (requires an entry in
// .gitmodules).
func (g GitDir) UpdateSubmodule(ctx context.Context, path, commit string) error {
if _, err := g.ReadSubmodule(ctx, path, "HEAD"); err != nil {
return err
}
cacheInfo := fmt.Sprintf("%s,%s,%s", gitlinkMode, commit, path)
_, err := g.Git(ctx, "update-index", "--add", "--cacheinfo", cacheInfo)
return err
}
// NumCommits returns the number of commits in the repo.
func (g GitDir) NumCommits(ctx context.Context) (int64, error) {
out, err := g.Git(ctx, "rev-list", "--all", "--count")
if err != nil {
return 0, err
}
return strconv.ParseInt(strings.TrimSpace(out), 10, 64)
}
// IsAncestor returns true iff A is an ancestor of B.
func (g GitDir) IsAncestor(ctx context.Context, a, b string) (bool, error) {
out, err := g.Git(ctx, "merge-base", "--is-ancestor", a, b)
if err != nil {
// Either a is not an ancestor of b, or we got a real error. If
// the output is empty, assume it's the former case.
if out == "" {
return false, nil
}
// "Not a valid commit name" indicates that the given commit does not
// exist and thus history was probably changed upstream.
// A non-existent commit cannot be an ancestor of one which does exist.
if strings.Contains(out, fmt.Sprintf("Not a valid commit name %s", a)) {
return false, nil
}
// Otherwise, return the presumably real error.
return false, fmt.Errorf("%s: %s", err, out)
}
return true, nil
}
// Version returns the Git version.
func (g GitDir) Version(ctx context.Context) (int, int, error) {
_, maj, min, err := git_common.FindGit(ctx)
return maj, min, err
}
// FullHash gives the full commit hash for the given ref.
func (g GitDir) FullHash(ctx context.Context, ref string) (string, error) {
output, err := g.RevParse(ctx, fmt.Sprintf("%s^{commit}", ref))
if err != nil {
return "", fmt.Errorf("Failed to obtain full hash: %s", err)
}
return output, nil
}
// CatFile runs "git cat-file -p <ref>:<path>".
func (g GitDir) CatFile(ctx context.Context, ref, path string) ([]byte, error) {
output, err := g.Git(ctx, "cat-file", "-p", fmt.Sprintf("%s:%s", ref, path))
if err != nil {
return nil, skerr.Wrap(err)
}
return []byte(output), nil
}
// ReadDir is analogous to os.File.Readdir for a particular ref.
func (g GitDir) ReadDir(ctx context.Context, ref, path string) ([]os.FileInfo, error) {
contents, err := g.CatFile(ctx, ref, path)
if err != nil {
return nil, skerr.Wrap(err)
}
return ParseDir(contents)
}
// GetRemotes returns a mapping of remote repo name to URL.
func (g GitDir) GetRemotes(ctx context.Context) (map[string]string, error) {
output, err := g.Git(ctx, "remote", "-v")
if err != nil {
return nil, skerr.Wrap(err)
}
lines := strings.Split(strings.TrimSpace(output), "\n")
rv := make(map[string]string, len(lines))
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) != 3 {
return nil, skerr.Fmt("Got invalid output from `git remote -v`:\n%s", output)
}
// First field is the remote name, second is the URL. The third field
// indicates whether the URL is used for fetching or pushing. In some
// cases the same remote name might use different URLs for fetching and
// pushing, in which case the return value will be incorrect. For our
// use cases this implementation is enough, but if that changes we may
// need to return a slice of structs containing the remote name and the
// fetch and push URLs.
rv[fields[0]] = fields[1]
}
return rv, nil
}