]>
Commit | Line | Data |
---|---|---|
bae9f6d2 JC |
1 | package getter |
2 | ||
3 | import ( | |
4 | "encoding/base64" | |
5 | "fmt" | |
6 | "io/ioutil" | |
7 | "net/url" | |
8 | "os" | |
9 | "os/exec" | |
10 | "path/filepath" | |
11 | "strings" | |
12 | ||
13 | urlhelper "github.com/hashicorp/go-getter/helper/url" | |
14 | "github.com/hashicorp/go-version" | |
15 | ) | |
16 | ||
17 | // GitGetter is a Getter implementation that will download a module from | |
18 | // a git repository. | |
19 | type GitGetter struct{} | |
20 | ||
21 | func (g *GitGetter) ClientMode(_ *url.URL) (ClientMode, error) { | |
22 | return ClientModeDir, nil | |
23 | } | |
24 | ||
25 | func (g *GitGetter) Get(dst string, u *url.URL) error { | |
26 | if _, err := exec.LookPath("git"); err != nil { | |
27 | return fmt.Errorf("git must be available and on the PATH") | |
28 | } | |
29 | ||
30 | // Extract some query parameters we use | |
31 | var ref, sshKey string | |
32 | q := u.Query() | |
33 | if len(q) > 0 { | |
34 | ref = q.Get("ref") | |
35 | q.Del("ref") | |
36 | ||
37 | sshKey = q.Get("sshkey") | |
38 | q.Del("sshkey") | |
39 | ||
40 | // Copy the URL | |
41 | var newU url.URL = *u | |
42 | u = &newU | |
43 | u.RawQuery = q.Encode() | |
44 | } | |
45 | ||
46 | var sshKeyFile string | |
47 | if sshKey != "" { | |
48 | // Check that the git version is sufficiently new. | |
49 | if err := checkGitVersion("2.3"); err != nil { | |
50 | return fmt.Errorf("Error using ssh key: %v", err) | |
51 | } | |
52 | ||
53 | // We have an SSH key - decode it. | |
54 | raw, err := base64.StdEncoding.DecodeString(sshKey) | |
55 | if err != nil { | |
56 | return err | |
57 | } | |
58 | ||
59 | // Create a temp file for the key and ensure it is removed. | |
60 | fh, err := ioutil.TempFile("", "go-getter") | |
61 | if err != nil { | |
62 | return err | |
63 | } | |
64 | sshKeyFile = fh.Name() | |
65 | defer os.Remove(sshKeyFile) | |
66 | ||
67 | // Set the permissions prior to writing the key material. | |
68 | if err := os.Chmod(sshKeyFile, 0600); err != nil { | |
69 | return err | |
70 | } | |
71 | ||
72 | // Write the raw key into the temp file. | |
73 | _, err = fh.Write(raw) | |
74 | fh.Close() | |
75 | if err != nil { | |
76 | return err | |
77 | } | |
78 | } | |
79 | ||
80 | // Clone or update the repository | |
81 | _, err := os.Stat(dst) | |
82 | if err != nil && !os.IsNotExist(err) { | |
83 | return err | |
84 | } | |
85 | if err == nil { | |
86 | err = g.update(dst, sshKeyFile, ref) | |
87 | } else { | |
88 | err = g.clone(dst, sshKeyFile, u) | |
89 | } | |
90 | if err != nil { | |
91 | return err | |
92 | } | |
93 | ||
94 | // Next: check out the proper tag/branch if it is specified, and checkout | |
95 | if ref != "" { | |
96 | if err := g.checkout(dst, ref); err != nil { | |
97 | return err | |
98 | } | |
99 | } | |
100 | ||
101 | // Lastly, download any/all submodules. | |
102 | return g.fetchSubmodules(dst, sshKeyFile) | |
103 | } | |
104 | ||
105 | // GetFile for Git doesn't support updating at this time. It will download | |
106 | // the file every time. | |
107 | func (g *GitGetter) GetFile(dst string, u *url.URL) error { | |
108 | td, err := ioutil.TempDir("", "getter-git") | |
109 | if err != nil { | |
110 | return err | |
111 | } | |
112 | if err := os.RemoveAll(td); err != nil { | |
113 | return err | |
114 | } | |
115 | ||
116 | // Get the filename, and strip the filename from the URL so we can | |
117 | // just get the repository directly. | |
118 | filename := filepath.Base(u.Path) | |
119 | u.Path = filepath.Dir(u.Path) | |
120 | ||
121 | // Get the full repository | |
122 | if err := g.Get(td, u); err != nil { | |
123 | return err | |
124 | } | |
125 | ||
126 | // Copy the single file | |
127 | u, err = urlhelper.Parse(fmtFileURL(filepath.Join(td, filename))) | |
128 | if err != nil { | |
129 | return err | |
130 | } | |
131 | ||
132 | fg := &FileGetter{Copy: true} | |
133 | return fg.GetFile(dst, u) | |
134 | } | |
135 | ||
136 | func (g *GitGetter) checkout(dst string, ref string) error { | |
137 | cmd := exec.Command("git", "checkout", ref) | |
138 | cmd.Dir = dst | |
139 | return getRunCommand(cmd) | |
140 | } | |
141 | ||
142 | func (g *GitGetter) clone(dst, sshKeyFile string, u *url.URL) error { | |
143 | cmd := exec.Command("git", "clone", u.String(), dst) | |
144 | setupGitEnv(cmd, sshKeyFile) | |
145 | return getRunCommand(cmd) | |
146 | } | |
147 | ||
148 | func (g *GitGetter) update(dst, sshKeyFile, ref string) error { | |
149 | // Determine if we're a branch. If we're NOT a branch, then we just | |
150 | // switch to master prior to checking out | |
151 | cmd := exec.Command("git", "show-ref", "-q", "--verify", "refs/heads/"+ref) | |
152 | cmd.Dir = dst | |
153 | ||
154 | if getRunCommand(cmd) != nil { | |
155 | // Not a branch, switch to master. This will also catch non-existent | |
156 | // branches, in which case we want to switch to master and then | |
157 | // checkout the proper branch later. | |
158 | ref = "master" | |
159 | } | |
160 | ||
161 | // We have to be on a branch to pull | |
162 | if err := g.checkout(dst, ref); err != nil { | |
163 | return err | |
164 | } | |
165 | ||
166 | cmd = exec.Command("git", "pull", "--ff-only") | |
167 | cmd.Dir = dst | |
168 | setupGitEnv(cmd, sshKeyFile) | |
169 | return getRunCommand(cmd) | |
170 | } | |
171 | ||
172 | // fetchSubmodules downloads any configured submodules recursively. | |
173 | func (g *GitGetter) fetchSubmodules(dst, sshKeyFile string) error { | |
174 | cmd := exec.Command("git", "submodule", "update", "--init", "--recursive") | |
175 | cmd.Dir = dst | |
176 | setupGitEnv(cmd, sshKeyFile) | |
177 | return getRunCommand(cmd) | |
178 | } | |
179 | ||
180 | // setupGitEnv sets up the environment for the given command. This is used to | |
181 | // pass configuration data to git and ssh and enables advanced cloning methods. | |
182 | func setupGitEnv(cmd *exec.Cmd, sshKeyFile string) { | |
183 | var sshOpts []string | |
184 | ||
185 | if sshKeyFile != "" { | |
186 | // We have an SSH key temp file configured, tell ssh about this. | |
187 | sshOpts = append(sshOpts, "-i", sshKeyFile) | |
188 | } | |
189 | ||
190 | cmd.Env = append(os.Environ(), | |
191 | // Set the ssh command to use for clones. | |
192 | "GIT_SSH_COMMAND=ssh "+strings.Join(sshOpts, " "), | |
193 | ) | |
194 | } | |
195 | ||
196 | // checkGitVersion is used to check the version of git installed on the system | |
197 | // against a known minimum version. Returns an error if the installed version | |
198 | // is older than the given minimum. | |
199 | func checkGitVersion(min string) error { | |
200 | want, err := version.NewVersion(min) | |
201 | if err != nil { | |
202 | return err | |
203 | } | |
204 | ||
205 | out, err := exec.Command("git", "version").Output() | |
206 | if err != nil { | |
207 | return err | |
208 | } | |
209 | ||
210 | fields := strings.Fields(string(out)) | |
211 | if len(fields) != 3 { | |
212 | return fmt.Errorf("Unexpected 'git version' output: %q", string(out)) | |
213 | } | |
214 | ||
215 | have, err := version.NewVersion(fields[2]) | |
216 | if err != nil { | |
217 | return err | |
218 | } | |
219 | ||
220 | if have.LessThan(want) { | |
221 | return fmt.Errorf("Required git version = %s, have %s", want, have) | |
222 | } | |
223 | ||
224 | return nil | |
225 | } |