func (repo *GitRepo) ListCommitsBetween(from, to string) ([]string, error) {
	out, err := repo.runGitCommand("rev-list", "--reverse", from+".."+to)
	if err != nil {
		return nil, err
	}
	if out == "" {
		return nil, nil
	}
	return strings.Split(out, "\n"), nil
}
ListCommitsBetween returns the list of commits between the two given revisions.

The "from" parameter is the starting point (exclusive), and the "to"
parameter is the ending point (inclusive).

The "from" commit does not need to be an ancestor of the "to" commit. If it
is not, then the merge base of the two is used as the starting point.
Admittedly, this makes calling these the "between" commits is a bit of a
misnomer, but it also makes the method easier to use when you want to
generate the list of changes in a feature branch, as it eliminates the need
to explicitly calculate the merge base. This also makes the semantics of the
method compatible with git's built-in "rev-list" command.

The generated list is in chronological order (with the oldest commit first).
package repository
import (
	"bufio"
	"bytes"
	"crypto/sha1"
	"encoding/json"
	"fmt"
	"io"
	"os"
	exec "golang.org/x/sys/execabs"
	"sort"
	"strconv"
	"strings"
)
const (
	branchRefPrefix         = "refs/heads/"
	notesRefPrefix          = "refs/notes/"
	devtoolsRefPrefix       = "refs/devtools/"
	remoteDevtoolsRefPrefix = "refs/remoteDevtools/"
)
type GitRepo struct {
	Path string
}
func (repo *GitRepo) runGitCommandWithIOAndEnv(stdin io.Reader, stdout, stderr io.Writer, env []string, args ...string) error {
	cmd := exec.Command("git", args...)
	cmd.Dir = repo.Path
	cmd.Stdin = stdin
	cmd.Stdout = stdout
	cmd.Stderr = stderr
	cmd.Env = env
	return cmd.Run()
}
func (repo *GitRepo) runGitCommandWithIO(stdin io.Reader, stdout, stderr io.Writer, args ...string) error {
	return repo.runGitCommandWithIOAndEnv(stdin, stdout, stderr, nil, args...)
}
func (repo *GitRepo) runGitCommandRaw(args ...string) (string, string, error) {
	var stdout bytes.Buffer
	var stderr bytes.Buffer
	err := repo.runGitCommandWithIO(nil, &stdout, &stderr, args...)
	return strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String()), err
}
func (repo *GitRepo) runGitCommand(args ...string) (string, error) {
	stdout, stderr, err := repo.runGitCommandRaw(args...)
	if err != nil {
		if stderr == "" {
			stderr = "Error running git command: " + strings.Join(args, " ")
		}
		err = fmt.Errorf(stderr)
	}
	return stdout, err
}
func (repo *GitRepo) runGitCommandWithEnv(env []string, args ...string) (string, error) {
	var stdout bytes.Buffer
	var stderr bytes.Buffer
	err := repo.runGitCommandWithIOAndEnv(nil, &stdout, &stderr, env, args...)
	if err != nil {
		stderrStr := strings.TrimSpace(stderr.String())
		if stderrStr == "" {
			stderrStr = "Error running git command: " + strings.Join(args, " ")
		}
		err = fmt.Errorf(stderrStr)
	}
	return strings.TrimSpace(stdout.String()), err
}
func (repo *GitRepo) runGitCommandInline(args ...string) error {
	return repo.runGitCommandWithIO(os.Stdin, os.Stdout, os.Stderr, args...)
}
func NewGitRepo(path string) (*GitRepo, error) {
	repo := &GitRepo{Path: path}
	_, _, err := repo.runGitCommandRaw("rev-parse")
	if err == nil {
		return repo, nil
	}
	if _, ok := err.(*exec.ExitError); ok {
		return nil, err
	}
	return nil, err
}
func (repo *GitRepo) HasRef(ref string) (bool, error) {
	_, _, err := repo.runGitCommandRaw("show-ref", "--verify", "--quiet", ref)
	if err == nil {
		return true, nil
	}
	if _, ok := err.(*exec.ExitError); ok {
		return false, nil
	}
	return false, err
}
func (repo *GitRepo) HasObject(hash string) (bool, error) {
	_, err := repo.runGitCommand("cat-file", "-e", hash)
	if err == nil {
		return true, nil
	}
	if _, ok := err.(*exec.ExitError); ok {
		return false, nil
	}
	return false, err
}
func (repo *GitRepo) GetPath() string {
	return repo.Path
}
func (repo *GitRepo) GetRepoStateHash() (string, error) {
	stateSummary, error := repo.runGitCommand("show-ref")
	return fmt.Sprintf("%x", sha1.Sum([]byte(stateSummary))), error
}
func (repo *GitRepo) GetUserEmail() (string, error) {
	return repo.runGitCommand("config", "user.email")
}
func (repo *GitRepo) GetUserSigningKey() (string, error) {
	return repo.runGitCommand("config", "user.signingKey")
}
func (repo *GitRepo) GetCoreEditor() (string, error) {
	return repo.runGitCommand("var", "GIT_EDITOR")
}
func (repo *GitRepo) GetSubmitStrategy() (string, error) {
	submitStrategy, _ := repo.runGitCommand("config", "appraise.submit")
	return submitStrategy, nil
}
func (repo *GitRepo) HasUncommittedChanges() (bool, error) {
	out, err := repo.runGitCommand("status", "--porcelain")
	if err != nil {
		return false, err
	}
	if len(out) > 0 {
		return true, nil
	}
	return false, nil
}
func (repo *GitRepo) VerifyCommit(hash string) error {
	out, err := repo.runGitCommand("cat-file", "-t", hash)
	if err != nil {
		return err
	}
	objectType := strings.TrimSpace(string(out))
	if objectType != "commit" {
		return fmt.Errorf("Hash %q points to a non-commit object of type %q", hash, objectType)
	}
	return nil
}
func (repo *GitRepo) VerifyGitRef(ref string) error {
	_, err := repo.runGitCommand("show-ref", "--verify", ref)
	return err
}
func (repo *GitRepo) GetHeadRef() (string, error) {
	return repo.runGitCommand("symbolic-ref", "HEAD")
}
func (repo *GitRepo) GetCommitHash(ref string) (string, error) {
	return repo.runGitCommand("show", "-s", "--format=%H", ref)
}
func (repo *GitRepo) ResolveRefCommit(ref string) (string, error) {
	if err := repo.VerifyGitRef(ref); err == nil {
		return repo.GetCommitHash(ref)
	}
	if strings.HasPrefix(ref, "refs/heads/") {
		pattern := strings.Replace(ref, "refs/heads", "**", 1)
		matchingOutput, err := repo.runGitCommand("for-each-ref", "--format=%(refname)", pattern)
		if err != nil {
			return "", err
		}
		matchingRefs := strings.Split(matchingOutput, "\n")
		if len(matchingRefs) == 1 && matchingRefs[0] != "" {
			return repo.GetCommitHash(matchingRefs[0])
		}
		return "", fmt.Errorf("Unable to find a git ref matching the pattern %q", pattern)
	}
	return "", fmt.Errorf("Unknown git ref %q", ref)
}
func (repo *GitRepo) GetCommitMessage(ref string) (string, error) {
	return repo.runGitCommand("show", "-s", "--format=%B", ref)
}
func (repo *GitRepo) GetCommitTime(ref string) (string, error) {
	return repo.runGitCommand("show", "-s", "--format=%ct", ref)
}
func (repo *GitRepo) GetLastParent(ref string) (string, error) {
	return repo.runGitCommand("rev-list", "--skip", "1", "-n", "1", ref)
}
func (repo GitRepo) GetCommitDetails(ref string) (*CommitDetails, error) {
	var err error
	show := func(formatString string) (result string) {
		if err != nil {
			return ""
		}
		result, err = repo.runGitCommand("show", "-s", ref, fmt.Sprintf("--format=tformat:%s", formatString))
		return result
	}
	jsonFormatString := "{\"tree\":\"%T\", \"time\": \"%at\"}"
	detailsJSON := show(jsonFormatString)
	if err != nil {
		return nil, err
	}
	var details CommitDetails
	err = json.Unmarshal([]byte(detailsJSON), &details)
	if err != nil {
		return nil, err
	}
	details.Author = show("%an")
	details.AuthorEmail = show("%ae")
	details.Committer = show("%cn")
	details.CommitterEmail = show("%ce")
	details.Summary = show("%s")
	parentsString := show("%P")
	details.Parents = strings.Split(parentsString, " ")
	if err != nil {
		return nil, err
	}
	return &details, nil
}
func (repo *GitRepo) MergeBase(a, b string) (string, error) {
	return repo.runGitCommand("merge-base", a, b)
}
func (repo *GitRepo) IsAncestor(ancestor, descendant string) (bool, error) {
	_, _, err := repo.runGitCommandRaw("merge-base", "--is-ancestor", ancestor, descendant)
	if err == nil {
		return true, nil
	}
	if _, ok := err.(*exec.ExitError); ok {
		return false, nil
	}
	return false, fmt.Errorf("Error while trying to determine commit ancestry: %v", err)
}
func (repo *GitRepo) Diff(left, right string, diffArgs ...string) (string, error) {
	args := []string{"diff"}
	args = append(args, diffArgs...)
	args = append(args, fmt.Sprintf("%s..%s", left, right))
	return repo.runGitCommand(args...)
}
func (repo *GitRepo) Show(commit, path string) (string, error) {
	return repo.runGitCommand("show", fmt.Sprintf("%s:%s", commit, path))
}
func (repo *GitRepo) SwitchToRef(ref string) error {
	if strings.HasPrefix(ref, branchRefPrefix) {
		ref = ref[len(branchRefPrefix):]
	}
	_, err := repo.runGitCommand("checkout", ref)
	return err
}
func (repo *GitRepo) mergeArchives(archive, remoteArchive string) error {
	hasRemote, err := repo.HasRef(remoteArchive)
	if err != nil {
		return err
	}
	if !hasRemote {
		return nil
	}
	remoteHash, err := repo.GetCommitHash(remoteArchive)
	if err != nil {
		return err
	}
	hasLocal, err := repo.HasRef(archive)
	if err != nil {
		return err
	}
	if !hasLocal {
		_, err := repo.runGitCommand("update-ref", archive, remoteHash)
		return err
	}
	archiveHash, err := repo.GetCommitHash(archive)
	if err != nil {
		return err
	}
	isAncestor, err := repo.IsAncestor(archiveHash, remoteHash)
	if err != nil {
		return err
	}
	if isAncestor {
		_, err := repo.runGitCommand("update-ref", archive, remoteHash, archiveHash)
		return err
	}
	refDetails, err := repo.GetCommitDetails(remoteArchive)
	if err != nil {
		return err
	}
	newArchiveHash, err := repo.runGitCommand("commit-tree", "-p", remoteHash, "-p", archiveHash, "-m", "Merge local and remote archives", refDetails.Tree)
	if err != nil {
		return err
	}
	newArchiveHash = strings.TrimSpace(newArchiveHash)
	_, err = repo.runGitCommand("update-ref", archive, newArchiveHash, archiveHash)
	return err
}
func (repo *GitRepo) ArchiveRef(ref, archive string) error {
	refHash, err := repo.GetCommitHash(ref)
	if err != nil {
		return err
	}
	refDetails, err := repo.GetCommitDetails(ref)
	if err != nil {
		return err
	}
	commitTreeArgs := []string{"commit-tree"}
	archiveHash, err := repo.GetCommitHash(archive)
	if err != nil {
		archiveHash = ""
	} else {
		if isAncestor, err := repo.IsAncestor(refHash, archiveHash); err != nil {
			return err
		} else if isAncestor {
			return nil
		}
		commitTreeArgs = append(commitTreeArgs, "-p", archiveHash)
	}
	commitTreeArgs = append(commitTreeArgs, "-p", refHash, "-m", fmt.Sprintf("Archive %s", refHash), refDetails.Tree)
	newArchiveHash, err := repo.runGitCommand(commitTreeArgs...)
	if err != nil {
		return err
	}
	newArchiveHash = strings.TrimSpace(newArchiveHash)
	updateRefArgs := []string{"update-ref", archive, newArchiveHash}
	if archiveHash != "" {
		updateRefArgs = append(updateRefArgs, archiveHash)
	}
	_, err = repo.runGitCommand(updateRefArgs...)
	return err
}
func (repo *GitRepo) MergeRef(ref string, fastForward bool, messages ...string) error {
	args := []string{"merge"}
	if fastForward {
		args = append(args, "--ff", "--ff-only")
	} else {
		args = append(args, "--no-ff")
	}
	if len(messages) > 0 {
		commitMessage := strings.Join(messages, "\n\n")
		args = append(args, "-e", "-m", commitMessage)
	}
	args = append(args, ref)
	return repo.runGitCommandInline(args...)
}
func (repo *GitRepo) MergeAndSignRef(ref string, fastForward bool,
	messages ...string) error {
	args := []string{"merge"}
	if fastForward {
		args = append(args, "--ff", "--ff-only", "-S")
	} else {
		args = append(args, "--no-ff", "-S")
	}
	if len(messages) > 0 {
		commitMessage := strings.Join(messages, "\n\n")
		args = append(args, "-e", "-m", commitMessage)
	}
	args = append(args, ref)
	return repo.runGitCommandInline(args...)
}
func (repo *GitRepo) RebaseRef(ref string) error {
	return repo.runGitCommandInline("rebase", "-i", ref)
}
func (repo *GitRepo) RebaseAndSignRef(ref string) error {
	return repo.runGitCommandInline("rebase", "-S", "-i", ref)
}
func (repo *GitRepo) ListCommits(ref string) []string {
	var stdout bytes.Buffer
	var stderr bytes.Buffer
	if err := repo.runGitCommandWithIO(nil, &stdout, &stderr, "rev-list", "--reverse", ref); err != nil {
		return nil
	}
	byteLines := bytes.Split(stdout.Bytes(), []byte("\n"))
	var commits []string
	for _, byteLine := range byteLines {
		commits = append(commits, string(byteLine))
	}
	return commits
}