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 }