]> git.immae.eu Git - github/fretlink/terraform-provider-statuscake.git/blob - vendor/github.com/hashicorp/go-getter/get_git.go
Upgrade to 0.12
[github/fretlink/terraform-provider-statuscake.git] / vendor / github.com / hashicorp / go-getter / get_git.go
1 package getter
2
3 import (
4 "context"
5 "encoding/base64"
6 "fmt"
7 "io/ioutil"
8 "net/url"
9 "os"
10 "os/exec"
11 "path/filepath"
12 "runtime"
13 "strconv"
14 "strings"
15
16 urlhelper "github.com/hashicorp/go-getter/helper/url"
17 safetemp "github.com/hashicorp/go-safetemp"
18 version "github.com/hashicorp/go-version"
19 )
20
21 // GitGetter is a Getter implementation that will download a module from
22 // a git repository.
23 type GitGetter struct {
24 getter
25 }
26
27 func (g *GitGetter) ClientMode(_ *url.URL) (ClientMode, error) {
28 return ClientModeDir, nil
29 }
30
31 func (g *GitGetter) Get(dst string, u *url.URL) error {
32 ctx := g.Context()
33 if _, err := exec.LookPath("git"); err != nil {
34 return fmt.Errorf("git must be available and on the PATH")
35 }
36
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)
43 }
44 }
45
46 // Extract some query parameters we use
47 var ref, sshKey string
48 var depth int
49 q := u.Query()
50 if len(q) > 0 {
51 ref = q.Get("ref")
52 q.Del("ref")
53
54 sshKey = q.Get("sshkey")
55 q.Del("sshkey")
56
57 if n, err := strconv.Atoi(q.Get("depth")); err == nil {
58 depth = n
59 }
60 q.Del("depth")
61
62 // Copy the URL
63 var newU url.URL = *u
64 u = &newU
65 u.RawQuery = q.Encode()
66 }
67
68 var sshKeyFile string
69 if sshKey != "" {
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)
73 }
74
75 // We have an SSH key - decode it.
76 raw, err := base64.StdEncoding.DecodeString(sshKey)
77 if err != nil {
78 return err
79 }
80
81 // Create a temp file for the key and ensure it is removed.
82 fh, err := ioutil.TempFile("", "go-getter")
83 if err != nil {
84 return err
85 }
86 sshKeyFile = fh.Name()
87 defer os.Remove(sshKeyFile)
88
89 // Set the permissions prior to writing the key material.
90 if err := os.Chmod(sshKeyFile, 0600); err != nil {
91 return err
92 }
93
94 // Write the raw key into the temp file.
95 _, err = fh.Write(raw)
96 fh.Close()
97 if err != nil {
98 return err
99 }
100 }
101
102 // Clone or update the repository
103 _, err := os.Stat(dst)
104 if err != nil && !os.IsNotExist(err) {
105 return err
106 }
107 if err == nil {
108 err = g.update(ctx, dst, sshKeyFile, ref, depth)
109 } else {
110 err = g.clone(ctx, dst, sshKeyFile, u, depth)
111 }
112 if err != nil {
113 return err
114 }
115
116 // Next: check out the proper tag/branch if it is specified, and checkout
117 if ref != "" {
118 if err := g.checkout(dst, ref); err != nil {
119 return err
120 }
121 }
122
123 // Lastly, download any/all submodules.
124 return g.fetchSubmodules(ctx, dst, sshKeyFile, depth)
125 }
126
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")
131 if err != nil {
132 return err
133 }
134 defer tdcloser.Close()
135
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)
140
141 // Get the full repository
142 if err := g.Get(td, u); err != nil {
143 return err
144 }
145
146 // Copy the single file
147 u, err = urlhelper.Parse(fmtFileURL(filepath.Join(td, filename)))
148 if err != nil {
149 return err
150 }
151
152 fg := &FileGetter{Copy: true}
153 return fg.GetFile(dst, u)
154 }
155
156 func (g *GitGetter) checkout(dst string, ref string) error {
157 cmd := exec.Command("git", "checkout", ref)
158 cmd.Dir = dst
159 return getRunCommand(cmd)
160 }
161
162 func (g *GitGetter) clone(ctx context.Context, dst, sshKeyFile string, u *url.URL, depth int) error {
163 args := []string{"clone"}
164
165 if depth > 0 {
166 args = append(args, "--depth", strconv.Itoa(depth))
167 }
168
169 args = append(args, u.String(), dst)
170 cmd := exec.CommandContext(ctx, "git", args...)
171 setupGitEnv(cmd, sshKeyFile)
172 return getRunCommand(cmd)
173 }
174
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)
179 cmd.Dir = dst
180
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.
185 ref = "master"
186 }
187
188 // We have to be on a branch to pull
189 if err := g.checkout(dst, ref); err != nil {
190 return err
191 }
192
193 if depth > 0 {
194 cmd = exec.Command("git", "pull", "--depth", strconv.Itoa(depth), "--ff-only")
195 } else {
196 cmd = exec.Command("git", "pull", "--ff-only")
197 }
198
199 cmd.Dir = dst
200 setupGitEnv(cmd, sshKeyFile)
201 return getRunCommand(cmd)
202 }
203
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"}
207 if depth > 0 {
208 args = append(args, "--depth", strconv.Itoa(depth))
209 }
210 cmd := exec.CommandContext(ctx, "git", args...)
211 cmd.Dir = dst
212 setupGitEnv(cmd, sshKeyFile)
213 return getRunCommand(cmd)
214 }
215
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="
220 var sshCmd []string
221
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.
225 env := os.Environ()
226 for i, v := range env {
227 if strings.HasPrefix(v, gitSSHCommand) && len(v) > len(gitSSHCommand) {
228 sshCmd = []string{v}
229
230 env[i], env[len(env)-1] = env[len(env)-1], env[i]
231 env = env[:len(env)-1]
232 break
233 }
234 }
235
236 if len(sshCmd) == 0 {
237 sshCmd = []string{gitSSHCommand + "ssh"}
238 }
239
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)
244 }
245 sshCmd = append(sshCmd, "-i", sshKeyFile)
246 }
247
248 env = append(env, strings.Join(sshCmd, " "))
249 cmd.Env = env
250 }
251
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)
257 if err != nil {
258 return err
259 }
260
261 out, err := exec.Command("git", "version").Output()
262 if err != nil {
263 return err
264 }
265
266 fields := strings.Fields(string(out))
267 if len(fields) < 3 {
268 return fmt.Errorf("Unexpected 'git version' output: %q", string(out))
269 }
270 v := fields[2]
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.")]
278 }
279
280 have, err := version.NewVersion(v)
281 if err != nil {
282 return err
283 }
284
285 if have.LessThan(want) {
286 return fmt.Errorf("Required git version = %s, have %s", want, have)
287 }
288
289 return nil
290 }