github.com/devtron-labs/ci-runner@v0.0.0-20240518055909-b2672f3349d7/helper/GitCliManager.go (about)

     1  package helper
     2  
     3  import (
     4  	"fmt"
     5  	"github.com/devtron-labs/ci-runner/util"
     6  	"log"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"strings"
    11  )
    12  
    13  type GitCliManager interface {
    14  	Fetch(gitContext GitContext, rootDir string) (response, errMsg string, err error)
    15  	Checkout(gitContext GitContext, rootDir string, checkout string) (response, errMsg string, err error)
    16  	RunCommandWithCred(cmd *exec.Cmd, userName, password string) (response, errMsg string, err error)
    17  	RunCommand(cmd *exec.Cmd) (response, errMsg string, err error)
    18  	runCommandForSuppliedNullifiedEnv(cmd *exec.Cmd, setHomeEnvToNull bool) (response, errMsg string, err error)
    19  	Init(rootDir string, remoteUrl string, isBare bool) error
    20  	Clone(gitContext GitContext, prj CiProjectDetails) (response, errMsg string, err error)
    21  	Merge(rootDir string, commit string) (response, errMsg string, err error)
    22  	RecursiveFetchSubmodules(rootDir string) (response, errMsg string, error error)
    23  	UpdateCredentialHelper(rootDir string) (response, errMsg string, error error)
    24  	UnsetCredentialHelper(rootDir string) (response, errMsg string, error error)
    25  	GitCheckout(gitContext GitContext, checkoutPath string, targetCheckout string, authMode AuthMode, fetchSubmodules bool, gitRepository string) (errMsg string, error error)
    26  }
    27  
    28  type GitCliManagerImpl struct {
    29  }
    30  
    31  func NewGitCliManager() *GitCliManagerImpl {
    32  	return &GitCliManagerImpl{}
    33  }
    34  
    35  const GIT_AKS_PASS = "/git-ask-pass.sh"
    36  const DefaultRemoteName = "origin"
    37  
    38  func (impl *GitCliManagerImpl) Fetch(gitContext GitContext, rootDir string) (response, errMsg string, err error) {
    39  	log.Println(util.DEVTRON, "git fetch ", "location", rootDir)
    40  	cmd := exec.Command("git", "-C", rootDir, "fetch", "origin", "--tags", "--force")
    41  	output, errMsg, err := impl.RunCommandWithCred(cmd, gitContext.Auth.Username, gitContext.Auth.Password)
    42  	log.Println(util.DEVTRON, "fetch output", "root", rootDir, "opt", output, "errMsg", errMsg, "error", err)
    43  	return output, "", nil
    44  }
    45  
    46  func (impl *GitCliManagerImpl) Checkout(gitContext GitContext, rootDir string, checkout string) (response, errMsg string, err error) {
    47  	log.Println(util.DEVTRON, "git checkout ", "location", rootDir)
    48  	cmd := exec.Command("git", "-C", rootDir, "checkout", checkout, "--force")
    49  	output, errMsg, err := impl.RunCommandWithCred(cmd, gitContext.Auth.Username, gitContext.Auth.Password)
    50  	log.Println(util.DEVTRON, "checkout output", "root", rootDir, "opt", output, "errMsg", errMsg, "error", err)
    51  	return output, "", nil
    52  }
    53  
    54  func (impl *GitCliManagerImpl) RunCommandWithCred(cmd *exec.Cmd, userName, password string) (response, errMsg string, err error) {
    55  	cmd.Env = append(os.Environ(),
    56  		fmt.Sprintf("GIT_ASKPASS=%s", GIT_AKS_PASS),
    57  		fmt.Sprintf("GIT_USERNAME=%s", userName), // ignored
    58  		fmt.Sprintf("GIT_PASSWORD=%s", password), // this value is used
    59  	)
    60  	return impl.RunCommand(cmd)
    61  }
    62  
    63  func (impl *GitCliManagerImpl) RunCommand(cmd *exec.Cmd) (response, errMsg string, err error) {
    64  	return impl.runCommandForSuppliedNullifiedEnv(cmd, true)
    65  }
    66  
    67  func (impl *GitCliManagerImpl) runCommandForSuppliedNullifiedEnv(cmd *exec.Cmd, setHomeEnvToNull bool) (response, errMsg string, err error) {
    68  	if setHomeEnvToNull {
    69  		cmd.Env = append(cmd.Env, "HOME=/dev/null")
    70  	}
    71  	// https://stackoverflow.com/questions/18159704/how-to-debug-exit-status-1-error-when-running-exec-command-in-golang
    72  	// in CombinedOutput, both stdOut and stdError are returned in single output
    73  	outBytes, err := cmd.CombinedOutput()
    74  	output := string(outBytes)
    75  	output = strings.Replace(output, "\n", "", -1)
    76  	output = strings.TrimSpace(output)
    77  	if err != nil {
    78  		exErr, ok := err.(*exec.ExitError)
    79  		if !ok {
    80  			return "", output, err
    81  		}
    82  		errOutput := string(exErr.Stderr)
    83  		return "", fmt.Sprintf("%s\n%s", output, errOutput), err
    84  	}
    85  	return output, "", nil
    86  }
    87  
    88  func (impl *GitCliManagerImpl) Init(rootDir string, remoteUrl string, isBare bool) error {
    89  
    90  	//-----------------
    91  
    92  	err := os.MkdirAll(rootDir, 0755)
    93  	if err != nil {
    94  		return err
    95  	}
    96  	err = impl.AddRepo(rootDir, remoteUrl)
    97  	return err
    98  }
    99  func (impl *GitCliManagerImpl) AddRepo(rootDir string, remoteUrl string) error {
   100  	err := impl.gitInit(rootDir)
   101  	if err != nil {
   102  		return err
   103  	}
   104  	return impl.gitCreateRemote(rootDir, remoteUrl)
   105  }
   106  
   107  func (impl *GitCliManagerImpl) gitInit(rootDir string) error {
   108  	log.Println(util.DEVTRON, "git", "-C", rootDir, "init")
   109  	cmd := exec.Command("git", "-C", rootDir, "init")
   110  	output, errMsg, err := impl.RunCommand(cmd)
   111  	log.Println(util.DEVTRON, "root", rootDir, "opt", output, "errMsg", errMsg, "error", err)
   112  	return err
   113  }
   114  
   115  func (impl *GitCliManagerImpl) gitCreateRemote(rootDir string, url string) error {
   116  	log.Println(util.DEVTRON, "git", "-C", rootDir, "remote", "add", DefaultRemoteName, url)
   117  	cmd := exec.Command("git", "-C", rootDir, "remote", "add", DefaultRemoteName, url)
   118  	output, errMsg, err := impl.RunCommand(cmd)
   119  	log.Println(util.DEVTRON, "url", url, "opt", output, "errMsg", errMsg, "error", err)
   120  	return err
   121  }
   122  
   123  func (impl *GitCliManagerImpl) Clone(gitContext GitContext, prj CiProjectDetails) (response, errMsg string, err error) {
   124  	rootDir := filepath.Join(util.WORKINGDIR, prj.CheckoutPath)
   125  	remoteUrl := prj.GitRepository
   126  	err = impl.Init(rootDir, remoteUrl, false)
   127  	if err != nil {
   128  		return "", "", err
   129  	}
   130  
   131  	response, errMsg, err = impl.Fetch(gitContext, rootDir)
   132  	return response, errMsg, err
   133  }
   134  
   135  // setting user.name and user.email as for non-fast-forward merge, git ask for user.name and email
   136  func (impl *GitCliManagerImpl) Merge(rootDir string, commit string) (response, errMsg string, err error) {
   137  	log.Println(util.DEVTRON, "git merge ", "location", rootDir)
   138  	command := "cd " + rootDir + " && git config user.email git@devtron.com && git config user.name Devtron && git merge " + commit + " --no-commit"
   139  	cmd := exec.Command("/bin/sh", "-c", command)
   140  	output, errMsg, err := impl.RunCommand(cmd)
   141  	log.Println(util.DEVTRON, "merge output", "root", rootDir, "opt", output, "errMsg", errMsg, "error", err)
   142  	return output, errMsg, err
   143  }
   144  
   145  func (impl *GitCliManagerImpl) RecursiveFetchSubmodules(rootDir string) (response, errMsg string, error error) {
   146  	log.Println(util.DEVTRON, "git recursive fetch submodules ", "location", rootDir)
   147  	cmd := exec.Command("git", "-C", rootDir, "submodule", "update", "--init", "--recursive")
   148  	output, eMsg, err := impl.runCommandForSuppliedNullifiedEnv(cmd, false)
   149  	log.Println(util.DEVTRON, "recursive fetch submodules output", "root", rootDir, "opt", output, "errMsg", errMsg, "error", err)
   150  	return output, eMsg, err
   151  }
   152  
   153  func (impl *GitCliManagerImpl) UpdateCredentialHelper(rootDir string) (response, errMsg string, error error) {
   154  	log.Println(util.DEVTRON, "git credential helper store ", "location", rootDir)
   155  	cmd := exec.Command("git", "-C", rootDir, "config", "--global", "credential.helper", "store")
   156  	output, eMsg, err := impl.runCommandForSuppliedNullifiedEnv(cmd, false)
   157  	log.Println(util.DEVTRON, "git credential helper store output", "root", rootDir, "opt", output, "errMsg", errMsg, "error", err)
   158  	return output, eMsg, err
   159  }
   160  
   161  func (impl *GitCliManagerImpl) UnsetCredentialHelper(rootDir string) (response, errMsg string, error error) {
   162  	log.Println(util.DEVTRON, "git credential helper unset ", "location", rootDir)
   163  	cmd := exec.Command("git", "-C", rootDir, "config", "--global", "--unset", "credential.helper")
   164  	output, eMsg, err := impl.runCommandForSuppliedNullifiedEnv(cmd, false)
   165  	log.Println(util.DEVTRON, "git credential helper unset output", "root", rootDir, "opt", output, "errMsg", errMsg, "error", err)
   166  	return output, eMsg, err
   167  }
   168  
   169  func (impl *GitCliManagerImpl) GitCheckout(gitContext GitContext, checkoutPath string, targetCheckout string, authMode AuthMode, fetchSubmodules bool, gitRepository string) (errMsg string, error error) {
   170  
   171  	rootDir := filepath.Join(util.WORKINGDIR, checkoutPath)
   172  
   173  	// checkout target hash
   174  	_, eMsg, cErr := impl.Checkout(gitContext, rootDir, targetCheckout)
   175  	if cErr != nil {
   176  		return eMsg, cErr
   177  	}
   178  
   179  	log.Println(util.DEVTRON, " fetchSubmodules ", fetchSubmodules, " authMode ", authMode)
   180  
   181  	if fetchSubmodules {
   182  		httpsAuth := (authMode == AUTH_MODE_USERNAME_PASSWORD) || (authMode == AUTH_MODE_ACCESS_TOKEN)
   183  		if httpsAuth {
   184  			// first remove protocol
   185  			modifiedUrl := strings.ReplaceAll(gitRepository, "https://", "")
   186  			// for bitbucket - if git repo url is started with username, then we need to remove username
   187  			if strings.Contains(modifiedUrl, "bitbucket.org") && !strings.HasPrefix(modifiedUrl, "bitbucket.org") {
   188  				modifiedUrl = modifiedUrl[strings.Index(modifiedUrl, "bitbucket.org"):]
   189  			}
   190  			// build url
   191  			modifiedUrl = "https://" + gitContext.Auth.Username + ":" + gitContext.Auth.Password + "@" + modifiedUrl
   192  
   193  			_, errMsg, cErr = impl.UpdateCredentialHelper(rootDir)
   194  			if cErr != nil {
   195  				return errMsg, cErr
   196  			}
   197  
   198  			cErr = util.CreateGitCredentialFileAndWriteData(modifiedUrl)
   199  			if cErr != nil {
   200  				return "Error in creating git credential file", cErr
   201  			}
   202  
   203  		}
   204  
   205  		_, errMsg, cErr = impl.RecursiveFetchSubmodules(rootDir)
   206  		if cErr != nil {
   207  			return errMsg, cErr
   208  		}
   209  
   210  		// cleanup
   211  
   212  		if httpsAuth {
   213  			_, errMsg, cErr = impl.UnsetCredentialHelper(rootDir)
   214  			if cErr != nil {
   215  				return errMsg, cErr
   216  			}
   217  
   218  			// delete file (~/.git-credentials) (which was created above)
   219  			cErr = util.CleanupAfterFetchingHttpsSubmodules()
   220  			if cErr != nil {
   221  				return "", cErr
   222  			}
   223  		}
   224  	}
   225  
   226  	return "", nil
   227  
   228  }