ghorg/git/git.go

364 lines
8.6 KiB
Go

package git
import (
"errors"
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/gabrie30/ghorg/scm"
)
type Gitter interface {
Clone(scm.Repo) error
Reset(scm.Repo) error
Pull(scm.Repo) error
SetOrigin(scm.Repo) error
SetOriginWithCredentials(scm.Repo) error
Clean(scm.Repo) error
Checkout(scm.Repo) error
CheckoutBranch(scm.Repo, string) error
GetCurrentBranch(scm.Repo) (string, error)
RevListCompare(scm.Repo, string, string) (string, error)
ShortStatus(scm.Repo) (string, error)
Branch(scm.Repo) (string, error)
UpdateRemote(scm.Repo) error
FetchAll(scm.Repo) error
FetchCloneBranch(scm.Repo) error
RepoCommitCount(scm.Repo) (int, error)
HasRemoteHeads(scm.Repo) (bool, error)
}
type GitClient struct{}
func NewGit() GitClient {
return GitClient{}
}
func printDebugCmd(cmd *exec.Cmd, repo scm.Repo) error {
fmt.Println("------------- GIT DEBUG -------------")
fmt.Printf("GHORG_OUTPUT_DIR=%v\n", os.Getenv("GHORG_OUTPUT_DIR"))
fmt.Printf("GHORG_ABSOLUTE_PATH_TO_CLONE_TO=%v\n", os.Getenv("GHORG_ABSOLUTE_PATH_TO_CLONE_TO"))
fmt.Print("Repo Data: ")
spew.Dump(repo)
fmt.Print("Command Ran: ")
spew.Dump(*cmd)
fmt.Println("")
output, err := cmd.CombinedOutput()
fmt.Printf("Command Output: %s\n", string(output))
if err != nil {
fmt.Printf("Error: %v\n", err)
}
return err
}
func (g GitClient) HasRemoteHeads(repo scm.Repo) (bool, error) {
cmd := exec.Command("git", "ls-remote", "--heads", "--quiet", "--exit-code")
cmd.Dir = repo.HostPath
err := cmd.Run()
if err == nil {
// successfully listed the remote heads
return true, nil
}
var exitError *exec.ExitError
if !errors.As(err, &exitError) {
// error, but no exit code, return err
return false, err
}
exitCode := exitError.ExitCode()
if exitCode == 0 {
// ls-remote did successfully list the remote heads
return true, nil
} else if exitCode == 2 {
// repository is empty
return false, nil
} else {
// another exit code, simply return err
return false, err
}
}
func cloneGitArgs(repo scm.Repo) []string {
args := []string{"clone", repo.CloneURL, repo.HostPath}
if os.Getenv("GHORG_INCLUDE_SUBMODULES") == "true" {
index := 1
args = append(args[:index+1], args[index:]...)
args[index] = "--recursive"
}
if os.Getenv("GHORG_CLONE_DEPTH") != "" {
index := 1
args = append(args[:index+1], args[index:]...)
args[index] = fmt.Sprintf("--depth=%v", os.Getenv("GHORG_CLONE_DEPTH"))
}
if os.Getenv("GHORG_GIT_FILTER") != "" {
index := 1
args = append(args[:index+1], args[index:]...)
args[index] = fmt.Sprintf("--filter=%v", os.Getenv("GHORG_GIT_FILTER"))
}
if os.Getenv("GHORG_BACKUP") == "true" {
args = append(args, "--mirror")
}
return args
}
// cloneRetryDelays are sleeps before each retry after a failed git clone (three retries, two attempts total).
var cloneRetryDelays = []time.Duration{1 * time.Second}
func (g GitClient) Clone(repo scm.Repo) error {
args := cloneGitArgs(repo)
if os.Getenv("GHORG_DEBUG") != "" {
cmd := exec.Command("git", args...)
return printDebugCmd(cmd, repo)
}
maxAttempts := 1 + len(cloneRetryDelays)
var lastErr error
for attempt := 0; attempt < maxAttempts; attempt++ {
if attempt > 0 {
_ = os.RemoveAll(repo.HostPath)
time.Sleep(cloneRetryDelays[attempt-1])
}
cmd := exec.Command("git", args...)
lastErr = cmd.Run()
if lastErr == nil {
return nil
}
}
return lastErr
}
func (g GitClient) SetOriginWithCredentials(repo scm.Repo) error {
args := []string{"remote", "set-url", "origin", repo.CloneURL}
cmd := exec.Command("git", args...)
cmd.Dir = repo.HostPath
if os.Getenv("GHORG_DEBUG") != "" {
return printDebugCmd(cmd, repo)
}
return cmd.Run()
}
func (g GitClient) SetOrigin(repo scm.Repo) error {
args := []string{"remote", "set-url", "origin", repo.URL}
cmd := exec.Command("git", args...)
cmd.Dir = repo.HostPath
if os.Getenv("GHORG_DEBUG") != "" {
return printDebugCmd(cmd, repo)
}
return cmd.Run()
}
func (g GitClient) Checkout(repo scm.Repo) error {
cmd := exec.Command("git", "checkout", repo.CloneBranch)
cmd.Dir = repo.HostPath
if os.Getenv("GHORG_DEBUG") != "" {
return printDebugCmd(cmd, repo)
}
return cmd.Run()
}
func (g GitClient) CheckoutBranch(repo scm.Repo, branch string) error {
cmd := exec.Command("git", "checkout", branch)
cmd.Dir = repo.HostPath
if os.Getenv("GHORG_DEBUG") != "" {
return printDebugCmd(cmd, repo)
}
return cmd.Run()
}
func (g GitClient) GetCurrentBranch(repo scm.Repo) (string, error) {
cmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD")
cmd.Dir = repo.HostPath
if os.Getenv("GHORG_DEBUG") != "" {
if err := printDebugCmd(cmd, repo); err != nil {
return "", err
}
}
output, err := cmd.Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(output)), nil
}
func (g GitClient) Clean(repo scm.Repo) error {
cmd := exec.Command("git", "clean", "-f", "-d")
cmd.Dir = repo.HostPath
if os.Getenv("GHORG_DEBUG") != "" {
return printDebugCmd(cmd, repo)
}
return cmd.Run()
}
func (g GitClient) UpdateRemote(repo scm.Repo) error {
cmd := exec.Command("git", "remote", "update")
cmd.Dir = repo.HostPath
if os.Getenv("GHORG_DEBUG") != "" {
return printDebugCmd(cmd, repo)
}
return cmd.Run()
}
func (g GitClient) Pull(repo scm.Repo) error {
args := []string{"pull", "origin", repo.CloneBranch}
if os.Getenv("GHORG_INCLUDE_SUBMODULES") == "true" {
index := 1
args = append(args[:index+1], args[index:]...)
args[index] = "--recurse-submodules"
}
if os.Getenv("GHORG_CLONE_DEPTH") != "" {
index := 1
args = append(args[:index+1], args[index:]...)
args[index] = fmt.Sprintf("--depth=%v", os.Getenv("GHORG_CLONE_DEPTH"))
}
cmd := exec.Command("git", args...)
cmd.Dir = repo.HostPath
if os.Getenv("GHORG_DEBUG") != "" {
return printDebugCmd(cmd, repo)
}
return cmd.Run()
}
func (g GitClient) Reset(repo scm.Repo) error {
cmd := exec.Command("git", "reset", "--hard", "origin/"+repo.CloneBranch)
cmd.Dir = repo.HostPath
if os.Getenv("GHORG_DEBUG") != "" {
return printDebugCmd(cmd, repo)
}
return cmd.Run()
}
func (g GitClient) FetchAll(repo scm.Repo) error {
args := []string{"fetch", "--all"}
if os.Getenv("GHORG_CLONE_DEPTH") != "" {
index := 1
args = append(args[:index+1], args[index:]...)
args[index] = fmt.Sprintf("--depth=%v", os.Getenv("GHORG_CLONE_DEPTH"))
}
if os.Getenv("GHORG_FETCH_PRUNE") == "true" {
args = append(args, "--prune")
}
cmd := exec.Command("git", args...)
cmd.Dir = repo.HostPath
if os.Getenv("GHORG_DEBUG") != "" {
return printDebugCmd(cmd, repo)
}
return cmd.Run()
}
func (g GitClient) Branch(repo scm.Repo) (string, error) {
args := []string{"branch"}
cmd := exec.Command("git", args...)
cmd.Dir = repo.HostPath
if os.Getenv("GHORG_DEBUG") != "" {
if err := printDebugCmd(cmd, repo); err != nil {
return "", err
}
}
output, err := cmd.Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(output)), nil
}
// RevListCompare returns the list of commits in the local branch that are not in the remote branch.
func (g GitClient) RevListCompare(repo scm.Repo, localBranch string, remoteBranch string) (string, error) {
cmd := exec.Command("git", "-C", repo.HostPath, "rev-list", localBranch, "^"+remoteBranch)
output, err := cmd.CombinedOutput()
if err != nil {
return "", err
}
return strings.TrimSpace(string(output)), nil
}
func (g GitClient) FetchCloneBranch(repo scm.Repo) error {
args := []string{"fetch", "origin", repo.CloneBranch}
if os.Getenv("GHORG_CLONE_DEPTH") != "" {
index := 1
args = append(args[:index+1], args[index:]...)
args[index] = fmt.Sprintf("--depth=%v", os.Getenv("GHORG_CLONE_DEPTH"))
}
cmd := exec.Command("git", args...)
cmd.Dir = repo.HostPath
if os.Getenv("GHORG_DEBUG") != "" {
return printDebugCmd(cmd, repo)
}
return cmd.Run()
}
func (g GitClient) ShortStatus(repo scm.Repo) (string, error) {
args := []string{"status", "--short"}
cmd := exec.Command("git", args...)
cmd.Dir = repo.HostPath
if os.Getenv("GHORG_DEBUG") != "" {
if err := printDebugCmd(cmd, repo); err != nil {
return "", err
}
}
output, err := cmd.Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(output)), nil
}
func (g GitClient) RepoCommitCount(repo scm.Repo) (int, error) {
args := []string{"rev-list", "--count", repo.CloneBranch, "--"}
cmd := exec.Command("git", args...)
cmd.Dir = repo.HostPath
if os.Getenv("GHORG_DEBUG") != "" {
err := printDebugCmd(cmd, repo)
if err != nil {
return 0, err
}
}
output, err := cmd.Output()
if err != nil {
return 0, err
}
count, err := strconv.Atoi(strings.TrimSpace(string(output)))
if err != nil {
return 0, err
}
return count, nil
}