1package getter 2 3import ( 4 "bytes" 5 "context" 6 "encoding/base64" 7 "fmt" 8 "io/ioutil" 9 "net/url" 10 "os" 11 "os/exec" 12 "path/filepath" 13 "regexp" 14 "runtime" 15 "strconv" 16 "strings" 17 18 urlhelper "github.com/hashicorp/go-getter/helper/url" 19 safetemp "github.com/hashicorp/go-safetemp" 20 version "github.com/hashicorp/go-version" 21) 22 23// GitGetter is a Getter implementation that will download a module from 24// a git repository. 25type GitGetter struct { 26 getter 27} 28 29var defaultBranchRegexp = regexp.MustCompile(`\s->\sorigin/(.*)`) 30 31func (g *GitGetter) ClientMode(_ *url.URL) (ClientMode, error) { 32 return ClientModeDir, nil 33} 34 35func (g *GitGetter) Get(dst string, u *url.URL) error { 36 ctx := g.Context() 37 if _, err := exec.LookPath("git"); err != nil { 38 return fmt.Errorf("git must be available and on the PATH") 39 } 40 41 // The port number must be parseable as an integer. If not, the user 42 // was probably trying to use a scp-style address, in which case the 43 // ssh:// prefix must be removed to indicate that. 44 // 45 // This is not necessary in versions of Go which have patched 46 // CVE-2019-14809 (e.g. Go 1.12.8+) 47 if portStr := u.Port(); portStr != "" { 48 if _, err := strconv.ParseUint(portStr, 10, 16); err != nil { 49 return fmt.Errorf("invalid port number %q; if using the \"scp-like\" git address scheme where a colon introduces the path instead, remove the ssh:// portion and use just the git:: prefix", portStr) 50 } 51 } 52 53 // Extract some query parameters we use 54 var ref, sshKey string 55 var depth int 56 q := u.Query() 57 if len(q) > 0 { 58 ref = q.Get("ref") 59 q.Del("ref") 60 61 sshKey = q.Get("sshkey") 62 q.Del("sshkey") 63 64 if n, err := strconv.Atoi(q.Get("depth")); err == nil { 65 depth = n 66 } 67 q.Del("depth") 68 69 // Copy the URL 70 var newU url.URL = *u 71 u = &newU 72 u.RawQuery = q.Encode() 73 } 74 75 var sshKeyFile string 76 if sshKey != "" { 77 // Check that the git version is sufficiently new. 78 if err := checkGitVersion("2.3"); err != nil { 79 return fmt.Errorf("Error using ssh key: %v", err) 80 } 81 82 // We have an SSH key - decode it. 83 raw, err := base64.StdEncoding.DecodeString(sshKey) 84 if err != nil { 85 return err 86 } 87 88 // Create a temp file for the key and ensure it is removed. 89 fh, err := ioutil.TempFile("", "go-getter") 90 if err != nil { 91 return err 92 } 93 sshKeyFile = fh.Name() 94 defer os.Remove(sshKeyFile) 95 96 // Set the permissions prior to writing the key material. 97 if err := os.Chmod(sshKeyFile, 0600); err != nil { 98 return err 99 } 100 101 // Write the raw key into the temp file. 102 _, err = fh.Write(raw) 103 fh.Close() 104 if err != nil { 105 return err 106 } 107 } 108 109 // Clone or update the repository 110 _, err := os.Stat(dst) 111 if err != nil && !os.IsNotExist(err) { 112 return err 113 } 114 if err == nil { 115 err = g.update(ctx, dst, sshKeyFile, ref, depth) 116 } else { 117 err = g.clone(ctx, dst, sshKeyFile, u, depth) 118 } 119 if err != nil { 120 return err 121 } 122 123 // Next: check out the proper tag/branch if it is specified, and checkout 124 if ref != "" { 125 if err := g.checkout(dst, ref); err != nil { 126 return err 127 } 128 } 129 130 // Lastly, download any/all submodules. 131 return g.fetchSubmodules(ctx, dst, sshKeyFile, depth) 132} 133 134// GetFile for Git doesn't support updating at this time. It will download 135// the file every time. 136func (g *GitGetter) GetFile(dst string, u *url.URL) error { 137 td, tdcloser, err := safetemp.Dir("", "getter") 138 if err != nil { 139 return err 140 } 141 defer tdcloser.Close() 142 143 // Get the filename, and strip the filename from the URL so we can 144 // just get the repository directly. 145 filename := filepath.Base(u.Path) 146 u.Path = filepath.Dir(u.Path) 147 148 // Get the full repository 149 if err := g.Get(td, u); err != nil { 150 return err 151 } 152 153 // Copy the single file 154 u, err = urlhelper.Parse(fmtFileURL(filepath.Join(td, filename))) 155 if err != nil { 156 return err 157 } 158 159 fg := &FileGetter{Copy: true} 160 return fg.GetFile(dst, u) 161} 162 163func (g *GitGetter) checkout(dst string, ref string) error { 164 cmd := exec.Command("git", "checkout", ref) 165 cmd.Dir = dst 166 return getRunCommand(cmd) 167} 168 169func (g *GitGetter) clone(ctx context.Context, dst, sshKeyFile string, u *url.URL, depth int) error { 170 args := []string{"clone"} 171 172 if depth > 0 { 173 args = append(args, "--depth", strconv.Itoa(depth)) 174 } 175 176 args = append(args, u.String(), dst) 177 cmd := exec.CommandContext(ctx, "git", args...) 178 setupGitEnv(cmd, sshKeyFile) 179 return getRunCommand(cmd) 180} 181 182func (g *GitGetter) update(ctx context.Context, dst, sshKeyFile, ref string, depth int) error { 183 // Determine if we're a branch. If we're NOT a branch, then we just 184 // switch to master prior to checking out 185 cmd := exec.CommandContext(ctx, "git", "show-ref", "-q", "--verify", "refs/heads/"+ref) 186 cmd.Dir = dst 187 188 if getRunCommand(cmd) != nil { 189 // Not a branch, switch to default branch. This will also catch 190 // non-existent branches, in which case we want to switch to default 191 // and then checkout the proper branch later. 192 ref = findDefaultBranch(dst) 193 } 194 195 // We have to be on a branch to pull 196 if err := g.checkout(dst, ref); err != nil { 197 return err 198 } 199 200 if depth > 0 { 201 cmd = exec.Command("git", "pull", "--depth", strconv.Itoa(depth), "--ff-only") 202 } else { 203 cmd = exec.Command("git", "pull", "--ff-only") 204 } 205 206 cmd.Dir = dst 207 setupGitEnv(cmd, sshKeyFile) 208 return getRunCommand(cmd) 209} 210 211// fetchSubmodules downloads any configured submodules recursively. 212func (g *GitGetter) fetchSubmodules(ctx context.Context, dst, sshKeyFile string, depth int) error { 213 args := []string{"submodule", "update", "--init", "--recursive"} 214 if depth > 0 { 215 args = append(args, "--depth", strconv.Itoa(depth)) 216 } 217 cmd := exec.CommandContext(ctx, "git", args...) 218 cmd.Dir = dst 219 setupGitEnv(cmd, sshKeyFile) 220 return getRunCommand(cmd) 221} 222 223// findDefaultBranch checks the repo's origin remote for its default branch 224// (generally "master"). "master" is returned if an origin default branch 225// can't be determined. 226func findDefaultBranch(dst string) string { 227 var stdoutbuf bytes.Buffer 228 cmd := exec.Command("git", "branch", "-r", "--points-at", "refs/remotes/origin/HEAD") 229 cmd.Dir = dst 230 cmd.Stdout = &stdoutbuf 231 err := cmd.Run() 232 matches := defaultBranchRegexp.FindStringSubmatch(stdoutbuf.String()) 233 if err != nil || matches == nil { 234 return "master" 235 } 236 return matches[len(matches)-1] 237} 238 239// setupGitEnv sets up the environment for the given command. This is used to 240// pass configuration data to git and ssh and enables advanced cloning methods. 241func setupGitEnv(cmd *exec.Cmd, sshKeyFile string) { 242 const gitSSHCommand = "GIT_SSH_COMMAND=" 243 var sshCmd []string 244 245 // If we have an existing GIT_SSH_COMMAND, we need to append our options. 246 // We will also remove our old entry to make sure the behavior is the same 247 // with versions of Go < 1.9. 248 env := os.Environ() 249 for i, v := range env { 250 if strings.HasPrefix(v, gitSSHCommand) && len(v) > len(gitSSHCommand) { 251 sshCmd = []string{v} 252 253 env[i], env[len(env)-1] = env[len(env)-1], env[i] 254 env = env[:len(env)-1] 255 break 256 } 257 } 258 259 if len(sshCmd) == 0 { 260 sshCmd = []string{gitSSHCommand + "ssh"} 261 } 262 263 if sshKeyFile != "" { 264 // We have an SSH key temp file configured, tell ssh about this. 265 if runtime.GOOS == "windows" { 266 sshKeyFile = strings.Replace(sshKeyFile, `\`, `/`, -1) 267 } 268 sshCmd = append(sshCmd, "-i", sshKeyFile) 269 } 270 271 env = append(env, strings.Join(sshCmd, " ")) 272 cmd.Env = env 273} 274 275// checkGitVersion is used to check the version of git installed on the system 276// against a known minimum version. Returns an error if the installed version 277// is older than the given minimum. 278func checkGitVersion(min string) error { 279 want, err := version.NewVersion(min) 280 if err != nil { 281 return err 282 } 283 284 out, err := exec.Command("git", "version").Output() 285 if err != nil { 286 return err 287 } 288 289 fields := strings.Fields(string(out)) 290 if len(fields) < 3 { 291 return fmt.Errorf("Unexpected 'git version' output: %q", string(out)) 292 } 293 v := fields[2] 294 if runtime.GOOS == "windows" && strings.Contains(v, ".windows.") { 295 // on windows, git version will return for example: 296 // git version 2.20.1.windows.1 297 // Which does not follow the semantic versionning specs 298 // https://semver.org. We remove that part in order for 299 // go-version to not error. 300 v = v[:strings.Index(v, ".windows.")] 301 } 302 303 have, err := version.NewVersion(v) 304 if err != nil { 305 return err 306 } 307 308 if have.LessThan(want) { 309 return fmt.Errorf("Required git version = %s, have %s", want, have) 310 } 311 312 return nil 313} 314