git package

Added git package with functions needed by branch util.

TEST=unit tests
BUG=chromium:980346

Change-Id: I283cf411316c2f7a96fa713b67148bb209a97c35
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/infra/go/+/1688112
Reviewed-by: Jack Neus <jackneus@google.com>
Reviewed-by: Sean Abraham <seanabraham@chromium.org>
Tested-by: Jack Neus <jackneus@google.com>
Commit-Queue: Jack Neus <jackneus@google.com>
diff --git a/internal/git/git.go b/internal/git/git.go
new file mode 100644
index 0000000..640bd3e
--- /dev/null
+++ b/internal/git/git.go
@@ -0,0 +1,124 @@
+// Copyright 2019 The Chromium OS 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 git
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"os/exec"
+	"regexp"
+	"strings"
+)
+
+var (
+	commandRunnerImpl commandRunner = realCommandRunner{}
+)
+
+type commandRunner interface {
+	runCommand(ctx context.Context, stdoutBuf, stderrBuf *bytes.Buffer, dir, name string, args ...string) error
+}
+
+type realCommandRunner struct{}
+
+func (c realCommandRunner) runCommand(ctx context.Context, stdoutBuf, stderrBuf *bytes.Buffer, dir, name string, args ...string) error {
+	cmd := exec.CommandContext(ctx, name, args...)
+	cmd.Stdout = stdoutBuf
+	cmd.Stderr = stderrBuf
+	cmd.Dir = dir
+	return cmd.Run()
+}
+
+type CommandOutput struct {
+	Stdout string
+	Stderr string
+}
+
+// Struct representing a remote ref.
+type RemoteRef struct {
+	Remote string
+	Ref    string
+}
+
+// RunGit the specified git command in the specified repo. It returns
+// stdout and stderr.
+func RunGit(gitRepo string, cmd []string) (CommandOutput, error) {
+	ctx := context.Background()
+	var stdoutBuf, stderrBuf bytes.Buffer
+	err := commandRunnerImpl.runCommand(ctx, &stdoutBuf, &stderrBuf, gitRepo, "git", cmd...)
+	cmdOutput := CommandOutput{stdoutBuf.String(), stderrBuf.String()}
+	return cmdOutput, err
+}
+
+// GetCurrentBranch returns current branch of a repo, and an empty string
+// if repo is on detached HEAD.
+func GetCurrentBranch(cwd string) string {
+	output, err := RunGit(cwd, []string{"symbolic-ref", "-q", "HEAD"})
+	if err != nil {
+		return ""
+	}
+	return StripRefsHead(strings.TrimSpace(output.Stdout))
+}
+
+// MatchBranchName returns the names of branches who match the specified
+// regular expression.
+func MatchBranchName(gitRepo string, pattern *regexp.Regexp) ([]string, error) {
+	// Regex should be case insensitive.
+	if !strings.HasPrefix(pattern.String(), "(?i)") {
+		pattern = regexp.MustCompile("(?i)" + pattern.String())
+	}
+
+	output, err := RunGit(gitRepo, []string{"ls-remote", gitRepo})
+	if err != nil {
+		// Could not read branches.
+		return []string{}, fmt.Errorf("git error: %s\nstderr: %s", err.Error(), output.Stderr)
+	}
+	// Find all branches that match the pattern.
+	branches := strings.Split(output.Stdout, "\n")
+	matchedBranches := []string{}
+	for _, branch := range branches {
+		branch = strings.TrimSpace(branch)
+		if branch == "" {
+			continue
+		}
+		branch = strings.Fields(branch)[1]
+		if pattern.Match([]byte(branch)) {
+			matchedBranches = append(matchedBranches, branch)
+		}
+	}
+	return matchedBranches, nil
+}
+
+// GetGitRepoRevision finds and returns the revision of a branch.
+func GetGitRepoRevision(cwd string) (string, error) {
+	output, err := RunGit(cwd, []string{"rev-parse", "HEAD"})
+	return strings.TrimSpace(output.Stdout), err
+}
+
+// StipRefsHead removes leading 'refs/heads/' from a ref name.
+func StripRefsHead(ref string) string {
+	return strings.TrimPrefix(ref, "refs/heads/")
+}
+
+// NormalizeRef converts git branch refs into fully qualified form.
+func NormalizeRef(ref string) string {
+	if ref == "" || strings.HasPrefix(ref, "refs/") {
+		return ref
+	}
+	return fmt.Sprintf("refs/heads/%s", ref)
+}
+
+// StripRefs removes leading 'refs/heads/', 'refs/remotes/[^/]+/' from a ref name.
+func StripRefs(ref string) string {
+	ref = StripRefsHead(ref)
+	// If the ref starts with ref/remotes/, then we want the part of the string
+	// that comes after the third "/".
+	// Example: refs/remotes/origin/master --> master
+	// Example: refs/remotse/origin/foo/bar --> foo/bar
+	if strings.HasPrefix(ref, "refs/remotes/") {
+		refParts := strings.SplitN(ref, "/", 4)
+		return refParts[len(refParts)-1]
+	}
+	return ref
+}