infra/go/internal/git: Fix bug with GetGitRepoRevision and add new functions

Also moved git-specific assert functions from test_util package to git package
to avoid circular imports.

BUG=chromium:980346
TEST=run_tests.sh

Change-Id: I21890c4b4dff2c62af5b90a4bbe0921810c54bd3
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/infra/go/+/1742413
Reviewed-by: Benjamin Gordon <bmgordon@chromium.org>
Reviewed-by: Evan Hernandez <evanhernandez@chromium.org>
Commit-Queue: Jack Neus <jackneus@google.com>
Tested-by: Jack Neus <jackneus@google.com>
diff --git a/internal/git/git.go b/internal/git/git.go
index 990b5a7..62dba2f 100644
--- a/internal/git/git.go
+++ b/internal/git/git.go
@@ -12,6 +12,8 @@
 	"strings"
 
 	"go.chromium.org/chromiumos/infra/go/internal/cmd"
+	"go.chromium.org/chromiumos/infra/go/internal/test_util"
+	"go.chromium.org/luci/common/errors"
 )
 
 var (
@@ -109,11 +111,11 @@
 func GetGitRepoRevision(cwd, branch string) (string, error) {
 	if branch == "" {
 		branch = "HEAD"
-	} else {
+	} else if branch != "HEAD" {
 		branch = NormalizeRef(branch)
 	}
 	output, err := RunGit(cwd, []string{"rev-parse", branch})
-	return strings.TrimSpace(output.Stdout), err
+	return strings.TrimSpace(output.Stdout), errors.Annotate(err, output.Stderr).Err()
 }
 
 // IsReachable determines whether one commit ref is reachable from another.
@@ -301,3 +303,71 @@
 	}
 	return nil
 }
+
+// RemoteBranches returns a list of branches on the specified remote.
+func RemoteBranches(gitRepo, remote string) ([]string, error) {
+	output, err := RunGit(gitRepo, []string{"ls-remote", remote})
+	if err != nil {
+		if strings.Contains(output.Stderr, "not appear to be a git repository") {
+			return []string{}, fmt.Errorf("%s is not a valid remote", remote)
+		}
+		return []string{}, err
+	}
+	remotes := []string{}
+	for _, line := range strings.Split(strings.TrimSpace(output.Stdout), "\n") {
+		if line == "" {
+			continue
+		}
+		remotes = append(remotes, StripRefs(strings.Fields(line)[1]))
+	}
+	return remotes, nil
+}
+
+// RemoteHasBranch checks whether or not a branch exists on a remote.
+func RemoteHasBranch(gitRepo, remote, branch string) (bool, error) {
+	branches, err := RemoteBranches(gitRepo, remote)
+	if err != nil {
+		return false, err
+	}
+	branch = StripRefs(branch)
+	for _, remoteBranch := range branches {
+		if branch == remoteBranch {
+			return true, nil
+		}
+	}
+	return false, nil
+}
+
+// AssertGitBranches asserts that the git repo has the given branches (it may have others, too).
+func AssertGitBranches(gitRepo string, branches []string) error {
+	actual, err := MatchBranchNameWithNamespace(gitRepo, regexp.MustCompile(".*"), regexp.MustCompile("refs/heads/"))
+	if err != nil {
+		return errors.Annotate(err, "error getting branches").Err()
+	}
+	if !test_util.UnorderedContains(actual, branches) {
+		return fmt.Errorf("project branch mismatch. expected: %v got %v", branches, actual)
+	}
+	return nil
+}
+
+// AssertGitBranches asserts that the git repo has only the correct branches.
+func AssertGitBranchesExact(gitRepo string, branches []string) error {
+	actual, err := MatchBranchNameWithNamespace(gitRepo, regexp.MustCompile(".*"), regexp.MustCompile("refs/heads/"))
+	if err != nil {
+		return errors.Annotate(err, "error getting branches").Err()
+	}
+	// Remove duplicates from branches. This is OK because branch names are unique identifiers
+	// and so having a branch name twice in branches doesn't mean anything special.
+	branchMap := make(map[string]bool)
+	for _, branch := range branches {
+		branchMap[branch] = true
+	}
+	branches = []string{}
+	for branch := range branchMap {
+		branches = append(branches, branch)
+	}
+	if !test_util.UnorderedEqual(actual, branches) {
+		return fmt.Errorf("project branch mismatch. expected: %v got %v", branches, actual)
+	}
+	return nil
+}