16 urlhelper "github.com/hashicorp/go-getter/helper/url"
17 safetemp "github.com/hashicorp/go-safetemp"
18 version "github.com/hashicorp/go-version"
21 // GitGetter is a Getter implementation that will download a module from
23 type GitGetter struct {
27 func (g *GitGetter) ClientMode(_ *url.URL) (ClientMode, error) {
28 return ClientModeDir, nil
31 func (g *GitGetter) Get(dst string, u *url.URL) error {
33 if _, err := exec.LookPath("git"); err != nil {
34 return fmt.Errorf("git must be available and on the PATH")
37 // The port number must be parseable as an integer. If not, the user
38 // was probably trying to use a scp-style address, in which case the
39 // ssh:// prefix must be removed to indicate that.
40 if portStr := u.Port(); portStr != "" {
41 if _, err := strconv.ParseUint(portStr, 10, 16); err != nil {
42 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)
46 // Extract some query parameters we use
47 var ref, sshKey string
54 sshKey = q.Get("sshkey")
57 if n, err := strconv.Atoi(q.Get("depth")); err == nil {
65 u.RawQuery = q.Encode()
70 // Check that the git version is sufficiently new.
71 if err := checkGitVersion("2.3"); err != nil {
72 return fmt.Errorf("Error using ssh key: %v", err)
75 // We have an SSH key - decode it.
76 raw, err := base64.StdEncoding.DecodeString(sshKey)
81 // Create a temp file for the key and ensure it is removed.
82 fh, err := ioutil.TempFile("", "go-getter")
86 sshKeyFile = fh.Name()
87 defer os.Remove(sshKeyFile)
89 // Set the permissions prior to writing the key material.
90 if err := os.Chmod(sshKeyFile, 0600); err != nil {
94 // Write the raw key into the temp file.
95 _, err = fh.Write(raw)
102 // Clone or update the repository
103 _, err := os.Stat(dst)
104 if err != nil && !os.IsNotExist(err) {
108 err = g.update(ctx, dst, sshKeyFile, ref, depth)
110 err = g.clone(ctx, dst, sshKeyFile, u, depth)
116 // Next: check out the proper tag/branch if it is specified, and checkout
118 if err := g.checkout(dst, ref); err != nil {
123 // Lastly, download any/all submodules.
124 return g.fetchSubmodules(ctx, dst, sshKeyFile, depth)
127 // GetFile for Git doesn't support updating at this time. It will download
128 // the file every time.
129 func (g *GitGetter) GetFile(dst string, u *url.URL) error {
130 td, tdcloser, err := safetemp.Dir("", "getter")
134 defer tdcloser.Close()
136 // Get the filename, and strip the filename from the URL so we can
137 // just get the repository directly.
138 filename := filepath.Base(u.Path)
139 u.Path = filepath.Dir(u.Path)
141 // Get the full repository
142 if err := g.Get(td, u); err != nil {
146 // Copy the single file
147 u, err = urlhelper.Parse(fmtFileURL(filepath.Join(td, filename)))
152 fg := &FileGetter{Copy: true}
153 return fg.GetFile(dst, u)
156 func (g *GitGetter) checkout(dst string, ref string) error {
157 cmd := exec.Command("git", "checkout", ref)
159 return getRunCommand(cmd)
162 func (g *GitGetter) clone(ctx context.Context, dst, sshKeyFile string, u *url.URL, depth int) error {
163 args := []string{"clone"}
166 args = append(args, "--depth", strconv.Itoa(depth))
169 args = append(args, u.String(), dst)
170 cmd := exec.CommandContext(ctx, "git", args...)
171 setupGitEnv(cmd, sshKeyFile)
172 return getRunCommand(cmd)
175 func (g *GitGetter) update(ctx context.Context, dst, sshKeyFile, ref string, depth int) error {
176 // Determine if we're a branch. If we're NOT a branch, then we just
177 // switch to master prior to checking out
178 cmd := exec.CommandContext(ctx, "git", "show-ref", "-q", "--verify", "refs/heads/"+ref)
181 if getRunCommand(cmd) != nil {
182 // Not a branch, switch to master. This will also catch non-existent
183 // branches, in which case we want to switch to master and then
184 // checkout the proper branch later.
188 // We have to be on a branch to pull
189 if err := g.checkout(dst, ref); err != nil {
194 cmd = exec.Command("git", "pull", "--depth", strconv.Itoa(depth), "--ff-only")
196 cmd = exec.Command("git", "pull", "--ff-only")
200 setupGitEnv(cmd, sshKeyFile)
201 return getRunCommand(cmd)
204 // fetchSubmodules downloads any configured submodules recursively.
205 func (g *GitGetter) fetchSubmodules(ctx context.Context, dst, sshKeyFile string, depth int) error {
206 args := []string{"submodule", "update", "--init", "--recursive"}
208 args = append(args, "--depth", strconv.Itoa(depth))
210 cmd := exec.CommandContext(ctx, "git", args...)
212 setupGitEnv(cmd, sshKeyFile)
213 return getRunCommand(cmd)
216 // setupGitEnv sets up the environment for the given command. This is used to
217 // pass configuration data to git and ssh and enables advanced cloning methods.
218 func setupGitEnv(cmd *exec.Cmd, sshKeyFile string) {
219 const gitSSHCommand = "GIT_SSH_COMMAND="
222 // If we have an existing GIT_SSH_COMMAND, we need to append our options.
223 // We will also remove our old entry to make sure the behavior is the same
224 // with versions of Go < 1.9.
226 for i, v := range env {
227 if strings.HasPrefix(v, gitSSHCommand) && len(v) > len(gitSSHCommand) {
230 env[i], env[len(env)-1] = env[len(env)-1], env[i]
231 env = env[:len(env)-1]
236 if len(sshCmd) == 0 {
237 sshCmd = []string{gitSSHCommand + "ssh"}
240 if sshKeyFile != "" {
241 // We have an SSH key temp file configured, tell ssh about this.
242 if runtime.GOOS == "windows" {
243 sshKeyFile = strings.Replace(sshKeyFile, `\`, `/`, -1)
245 sshCmd = append(sshCmd, "-i", sshKeyFile)
248 env = append(env, strings.Join(sshCmd, " "))
252 // checkGitVersion is used to check the version of git installed on the system
253 // against a known minimum version. Returns an error if the installed version
254 // is older than the given minimum.
255 func checkGitVersion(min string) error {
256 want, err := version.NewVersion(min)
261 out, err := exec.Command("git", "version").Output()
266 fields := strings.Fields(string(out))
268 return fmt.Errorf("Unexpected 'git version' output: %q", string(out))
271 if runtime.GOOS == "windows" && strings.Contains(v, ".windows.") {
272 // on windows, git version will return for example:
273 // git version 2.20.1.windows.1
274 // Which does not follow the semantic versionning specs
275 // https://semver.org. We remove that part in order for
276 // go-version to not error.
277 v = v[:strings.Index(v, ".windows.")]
280 have, err := version.NewVersion(v)
285 if have.LessThan(want) {
286 return fmt.Errorf("Required git version = %s, have %s", want, have)