]>
Commit | Line | Data |
---|---|---|
bae9f6d2 JC |
1 | package getter |
2 | ||
3 | import ( | |
107c1cdb | 4 | "context" |
bae9f6d2 JC |
5 | "encoding/base64" |
6 | "fmt" | |
7 | "io/ioutil" | |
8 | "net/url" | |
9 | "os" | |
10 | "os/exec" | |
11 | "path/filepath" | |
107c1cdb ND |
12 | "runtime" |
13 | "strconv" | |
bae9f6d2 JC |
14 | "strings" |
15 | ||
16 | urlhelper "github.com/hashicorp/go-getter/helper/url" | |
107c1cdb ND |
17 | safetemp "github.com/hashicorp/go-safetemp" |
18 | version "github.com/hashicorp/go-version" | |
bae9f6d2 JC |
19 | ) |
20 | ||
21 | // GitGetter is a Getter implementation that will download a module from | |
22 | // a git repository. | |
107c1cdb ND |
23 | type GitGetter struct { |
24 | getter | |
25 | } | |
bae9f6d2 JC |
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 { | |
107c1cdb | 32 | ctx := g.Context() |
bae9f6d2 JC |
33 | if _, err := exec.LookPath("git"); err != nil { |
34 | return fmt.Errorf("git must be available and on the PATH") | |
35 | } | |
36 | ||
107c1cdb ND |
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 | ||
bae9f6d2 JC |
46 | // Extract some query parameters we use |
47 | var ref, sshKey string | |
107c1cdb | 48 | var depth int |
bae9f6d2 JC |
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 | ||
107c1cdb ND |
57 | if n, err := strconv.Atoi(q.Get("depth")); err == nil { |
58 | depth = n | |
59 | } | |
60 | q.Del("depth") | |
61 | ||
bae9f6d2 JC |
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 { | |
107c1cdb | 108 | err = g.update(ctx, dst, sshKeyFile, ref, depth) |
bae9f6d2 | 109 | } else { |
107c1cdb | 110 | err = g.clone(ctx, dst, sshKeyFile, u, depth) |
bae9f6d2 JC |
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. | |
107c1cdb | 124 | return g.fetchSubmodules(ctx, dst, sshKeyFile, depth) |
bae9f6d2 JC |
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 { | |
15c0b25d | 130 | td, tdcloser, err := safetemp.Dir("", "getter") |
bae9f6d2 JC |
131 | if err != nil { |
132 | return err | |
133 | } | |
15c0b25d | 134 | defer tdcloser.Close() |
bae9f6d2 JC |
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 | ||
107c1cdb ND |
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...) | |
bae9f6d2 JC |
171 | setupGitEnv(cmd, sshKeyFile) |
172 | return getRunCommand(cmd) | |
173 | } | |
174 | ||
107c1cdb | 175 | func (g *GitGetter) update(ctx context.Context, dst, sshKeyFile, ref string, depth int) error { |
bae9f6d2 JC |
176 | // Determine if we're a branch. If we're NOT a branch, then we just |
177 | // switch to master prior to checking out | |
107c1cdb | 178 | cmd := exec.CommandContext(ctx, "git", "show-ref", "-q", "--verify", "refs/heads/"+ref) |
bae9f6d2 JC |
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 | ||
107c1cdb ND |
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 | ||
bae9f6d2 JC |
199 | cmd.Dir = dst |
200 | setupGitEnv(cmd, sshKeyFile) | |
201 | return getRunCommand(cmd) | |
202 | } | |
203 | ||
204 | // fetchSubmodules downloads any configured submodules recursively. | |
107c1cdb ND |
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...) | |
bae9f6d2 JC |
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) { | |
15c0b25d AP |
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 { | |
107c1cdb | 227 | if strings.HasPrefix(v, gitSSHCommand) && len(v) > len(gitSSHCommand) { |
15c0b25d AP |
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 | } | |
bae9f6d2 JC |
239 | |
240 | if sshKeyFile != "" { | |
241 | // We have an SSH key temp file configured, tell ssh about this. | |
107c1cdb ND |
242 | if runtime.GOOS == "windows" { |
243 | sshKeyFile = strings.Replace(sshKeyFile, `\`, `/`, -1) | |
244 | } | |
15c0b25d | 245 | sshCmd = append(sshCmd, "-i", sshKeyFile) |
bae9f6d2 JC |
246 | } |
247 | ||
15c0b25d AP |
248 | env = append(env, strings.Join(sshCmd, " ")) |
249 | cmd.Env = env | |
bae9f6d2 JC |
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)) | |
107c1cdb | 267 | if len(fields) < 3 { |
bae9f6d2 JC |
268 | return fmt.Errorf("Unexpected 'git version' output: %q", string(out)) |
269 | } | |
107c1cdb ND |
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 | } | |
bae9f6d2 | 279 | |
107c1cdb | 280 | have, err := version.NewVersion(v) |
bae9f6d2 JC |
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 | } |