Initial work on generic repo test harness

Initial work on a test harness for the CrOS team. This test harness
allows a developer to specify a number of remote repos, the projects
within those repos, and the files within those projects. The harness
properly sets up each forest of git repos and populates each git repo
with the appropriate files.

BUG=chromium:980346
TEST=run_tests.sh

Change-Id: Ic8efc12afb95333e86b96ac9dde2e6e2e139937c
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/infra/go/+/1706930
Commit-Queue: Jack Neus <jackneus@google.com>
Tested-by: Jack Neus <jackneus@google.com>
Reviewed-by: Evan Hernandez <evanhernandez@chromium.org>
diff --git a/internal/git/git.go b/internal/git/git.go
index be5e613..0306f33 100644
--- a/internal/git/git.go
+++ b/internal/git/git.go
@@ -7,6 +7,7 @@
 	"bytes"
 	"context"
 	"fmt"
+	"path/filepath"
 	"regexp"
 	"strings"
 
@@ -112,17 +113,42 @@
 
 // CreateBranch creates a branch.
 func CreateBranch(gitRepo, branch string) error {
-	_, err := RunGit(gitRepo, []string{"checkout", "-B", branch, "HEAD"})
+	output, err := RunGit(gitRepo, []string{"checkout", "-B", branch})
+	if err != nil && strings.Contains(output.Stderr, "not a valid branch name") {
+		return fmt.Errorf("%s is not a valid branch name", branch)
+	}
+	return err
+}
+
+// CreateTrackingBranch creates a tracking branch.
+func CreateTrackingBranch(gitRepo, branch string, remoteRef RemoteRef) error {
+	refspec := fmt.Sprintf("%s/%s", remoteRef.Remote, remoteRef.Ref)
+	output, err := RunGit(gitRepo, []string{"fetch", remoteRef.Remote, remoteRef.Ref})
+	if err != nil {
+		return fmt.Errorf("could not fetch %s: %s", refspec, output.Stderr)
+	}
+	output, err = RunGit(gitRepo, []string{"checkout", "-b", branch, "-t", refspec})
+	if err != nil {
+		if strings.Contains(output.Stderr, "not a valid branch name") {
+			return fmt.Errorf("%s is not a valid branch name", branch)
+		} else {
+			return fmt.Errorf(output.Stderr)
+		}
+	}
 	return err
 }
 
 // CommitAll adds all local changes and commits them.
 func CommitAll(gitRepo, commitMsg string) error {
-	if _, err := RunGit(gitRepo, []string{"add", "-A"}); err != nil {
-		return err
+	if output, err := RunGit(gitRepo, []string{"add", "-A"}); err != nil {
+		return fmt.Errorf(output.Stderr)
 	}
-	if _, err := RunGit(gitRepo, []string{"commit", "-m", commitMsg}); err != nil {
-		return err
+	if output, err := RunGit(gitRepo, []string{"commit", "-m", commitMsg}); err != nil {
+		if strings.Contains(output.Stdout, "nothing to commit") {
+			return fmt.Errorf(output.Stdout)
+		} else {
+			return fmt.Errorf(output.Stderr)
+		}
 	}
 	return nil
 }
@@ -130,7 +156,9 @@
 // PushGitChanges stages and commits any local changes before pushing the commit
 // to the specified remote ref.
 func PushChanges(gitRepo, localRef, commitMsg string, dryRun bool, pushTo RemoteRef) error {
-	if err := CommitAll(gitRepo, commitMsg); err != nil {
+	err := CommitAll(gitRepo, commitMsg)
+	// It's ok if there's nothing to commit, we can still try to push.
+	if err != nil && !strings.Contains(err.Error(), "nothing to commit") {
 		return err
 	}
 	ref := fmt.Sprintf("%s:%s", localRef, pushTo.Ref)
@@ -138,6 +166,65 @@
 	if dryRun {
 		cmd = append(cmd, "--dry-run")
 	}
+	_, err = RunGit(gitRepo, cmd)
+	return err
+}
+
+// Init initializes a repo.
+func Init(gitRepo string, bare bool) error {
+	cmd := []string{"init"}
+	if bare {
+		cmd = append(cmd, "--bare")
+	}
 	_, err := RunGit(gitRepo, cmd)
 	return err
 }
+
+// AddRemote adds a remote.
+func AddRemote(gitRepo, remote, remoteLocation string) error {
+	output, err := RunGit(gitRepo, []string{"remote", "add", remote, remoteLocation})
+	if err != nil {
+		fmt.Printf("remote error: %s\n", output.Stderr)
+	}
+	return err
+}
+
+// Checkout checkouts a branch.
+func Checkout(gitRepo, branch string) error {
+	output, err := RunGit(gitRepo, []string{"checkout", branch})
+	if err != nil && strings.Contains(output.Stderr, "did not match any") {
+		return fmt.Errorf(output.Stderr)
+	}
+	return err
+}
+
+// DeleteBranch checks out to master and then deletes the current branch.
+func DeleteBranch(gitRepo, branch string, force bool) error {
+	cmd := []string{"branch"}
+	if force {
+		cmd = append(cmd, "-D")
+	} else {
+		cmd = append(cmd, "-d")
+	}
+	cmd = append(cmd, branch)
+	output, err := RunGit(gitRepo, cmd)
+
+	if err != nil {
+		if strings.Contains(output.Stderr, "checked out at") {
+			return fmt.Errorf(output.Stderr)
+		}
+		if strings.Contains(output.Stderr, "not fully merged") {
+			return fmt.Errorf("branch %s is not fully merged. use the force parameter if you wish to proceed", branch)
+		}
+	}
+	return err
+}
+
+// Clone clones the remote into the specified dir.
+func Clone(remote, dir string) error {
+	output, err := RunGit(filepath.Dir(dir), []string{"clone", remote, filepath.Base(dir)})
+	if err != nil {
+		return fmt.Errorf(output.Stderr)
+	}
+	return nil
+}