-
Notifications
You must be signed in to change notification settings - Fork 43
/
git.go
340 lines (299 loc) · 8.19 KB
/
git.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
package git
import (
"fmt"
"net/url"
"os"
"strings"
"sync"
"time"
"github.com/abiosoft/caddy-git/gitos"
"github.com/caddyserver/caddy"
)
const (
// Number of retries if git pull fails
numRetries = 3
// variable for latest tag
latestTag = "{latest}"
)
// Git represent multiple repositories.
type Git []*Repo
// Repo retrieves repository at i or nil if not found.
func (g Git) Repo(i int) *Repo {
if i < len(g) {
return g[i]
}
return nil
}
// RepoURL is the repository url.
type RepoURL string
// String satisfies stringer and attempts to strip off authentication
// info from url if it exists.
func (r RepoURL) String() string {
u, err := url.Parse(string(r))
if err != nil {
return string(r)
}
if u.User != nil {
u.User = url.User(u.User.Username())
}
return u.String()
}
// Val returns git friendly Val that can be
// passed to git clone.
func (r RepoURL) Val() string {
if strings.HasPrefix(string(r), "ssh://") {
return strings.TrimPrefix(string(r), "ssh://")
}
return string(r)
}
// Repo is the structure that holds required information
// of a git repository.
type Repo struct {
URL RepoURL // Repository URL
Path string // Directory to pull to
Host string // Git domain host e.g. github.com
Branch string // Git branch
KeyPath string // Path to private ssh key
Interval time.Duration // Interval between pulls
CloneArgs []string // Additonal cli args to pass to git clone
PullArgs []string // Additonal cli args to pass to git pull
Then []Then // Commands to execute after successful git pull
pulled bool // true if there was a successful pull
lastPull time.Time // time of the last successful pull
lastCommit string // hash for the most recent commit
latestTag string // latest tag name
Hook HookConfig // Webhook configuration
sync.Mutex
}
// Pull attempts a git pull.
// It retries at most numRetries times if error occurs
func (r *Repo) Pull() error {
r.Lock()
defer r.Unlock()
// prevent a pull if the last one was less than 5 seconds ago
if gos.TimeSince(r.lastPull) < 5*time.Second {
return nil
}
// keep last commit hash for comparison later
lastCommit := r.lastCommit
var err error
// Attempt to pull at most numRetries times
for i := 0; i < numRetries; i++ {
if err = r.pull(); err == nil {
break
}
Logger().Println(err)
}
if err != nil {
return err
}
// check if there are new changes,
// then execute post pull command
if r.lastCommit == lastCommit {
Logger().Println("No new changes.")
return nil
}
return r.execThen()
}
// pull performs git pull, or git clone if repository does not exist.
func (r *Repo) pull() error {
// if not pulled, perform clone
if !r.pulled {
return r.clone()
}
// if latest tag config is set
if r.Branch == latestTag {
return r.checkoutLatestTag()
}
params := append([]string{"pull"}, append(r.PullArgs, "origin", r.Branch)...)
var err error
if err = r.gitCmd(params, r.Path); err == nil {
r.pulled = true
r.lastPull = time.Now()
Logger().Printf("%v pulled.\n", r.URL)
r.lastCommit, err = r.mostRecentCommit()
}
return err
}
// clone performs git clone.
func (r *Repo) clone() error {
params := append([]string{"clone", "-b", r.Branch}, append(r.CloneArgs, r.URL.Val(), r.Path)...)
tagMode := r.Branch == latestTag
if tagMode {
params = append([]string{"clone"}, append(r.CloneArgs, r.URL.Val(), r.Path)...)
}
var err error
if err = r.gitCmd(params, ""); err == nil {
r.pulled = true
r.lastPull = time.Now()
Logger().Printf("%v pulled.\n", r.URL)
r.lastCommit, err = r.mostRecentCommit()
// if latest tag config is set.
if tagMode {
return r.checkoutLatestTag()
}
}
return err
}
// checkoutLatestTag checks out the latest tag of the repository.
func (r *Repo) checkoutLatestTag() error {
tag, err := r.fetchLatestTag()
if err != nil {
Logger().Println("Error retrieving latest tag.")
return err
}
if tag == "" {
Logger().Println("No tags found for Repo: ", r.URL)
return fmt.Errorf("No tags found for Repo: %v", r.URL)
} else if tag == r.latestTag {
Logger().Println("No new tags.")
return nil
}
params := []string{"checkout", "tags/" + tag}
if err = r.gitCmd(params, r.Path); err == nil {
r.latestTag = tag
r.lastCommit, err = r.mostRecentCommit()
Logger().Printf("Tag %v checkout done.\n", tag)
}
return err
}
// checkoutCommit checks out the specified commitHash.
func (r *Repo) checkoutCommit(commitHash string) error {
var err error
params := []string{"checkout", commitHash}
if err = r.gitCmd(params, r.Path); err == nil {
Logger().Printf("Commit %v checkout done.\n", commitHash)
}
return err
}
// gitCmd performs a git command.
func (r *Repo) gitCmd(params []string, dir string) error {
// if key is specified, use ssh key
if r.KeyPath != "" {
return r.gitCmdWithKey(params, dir)
}
return runCmd(gitBinary, params, dir)
}
// gitCmdWithKey is used for private repositories and requires an ssh key.
// Note: currently only limited to Linux and OSX.
func (r *Repo) gitCmdWithKey(params []string, dir string) error {
var gitSSH, script gitos.File
// ensure temporary files deleted after usage
defer func() {
if gitSSH != nil {
gos.Remove(gitSSH.Name())
}
if script != nil {
gos.Remove(script.Name())
}
}()
var err error
// write git.sh script to temp file
gitSSH, err = writeScriptFile(gitWrapperScript())
if err != nil {
return err
}
// write git bash script to file
script, err = writeScriptFile(bashScript(gitSSH.Name(), r, params))
if err != nil {
return err
}
return runCmd(script.Name(), nil, dir)
}
// Prepare prepares for a git pull
// and validates the configured directory
func (r *Repo) Prepare() error {
// check if directory exists or is empty
// if not, create directory
fs, err := gos.ReadDir(r.Path)
if err != nil || len(fs) == 0 {
return gos.MkdirAll(r.Path, os.FileMode(0755))
}
// validate git repo
isGit := false
for _, f := range fs {
if f.IsDir() && f.Name() == ".git" {
isGit = true
break
}
}
if isGit {
// check if same repository
var repoURL string
if repoURL, err = r.originURL(); err == nil {
if strings.TrimSuffix(repoURL, ".git") == strings.TrimSuffix(r.URL.Val(), ".git") {
r.pulled = true
return nil
}
}
if err != nil {
return fmt.Errorf("cannot retrieve repo url for %v Error: %v", r.Path, err)
}
return fmt.Errorf("another git repo '%v' exists at %v", repoURL, r.Path)
}
return fmt.Errorf("cannot git clone into %v, directory not empty", r.Path)
}
// getMostRecentCommit gets the hash of the most recent commit to the
// repository. Useful for checking if changes occur.
func (r *Repo) mostRecentCommit() (string, error) {
command := gitBinary + ` --no-pager log -n 1 --pretty=format:"%H"`
c, args, err := caddy.SplitCommandAndArgs(command)
if err != nil {
return "", err
}
return runCmdOutput(c, args, r.Path)
}
// fetchLatestTag retrieves the most recent tag in the repository.
func (r *Repo) fetchLatestTag() (string, error) {
// fetch updates to get latest tag
params := []string{"fetch", "origin", "--tags"}
err := r.gitCmd(params, r.Path)
if err != nil {
return "", err
}
// retrieve latest tag
command := gitBinary + ` describe origin --abbrev=0 --tags`
c, args, err := caddy.SplitCommandAndArgs(command)
if err != nil {
return "", err
}
return runCmdOutput(c, args, r.Path)
}
// originURL retrieves remote origin url for the git repository at path
func (r *Repo) originURL() (string, error) {
_, err := gos.Stat(r.Path)
if err != nil {
return "", err
}
args := []string{"config", "--get", "remote.origin.url"}
return runCmdOutput(gitBinary, args, r.Path)
}
// execThen executes r.Then.
// It is trigged after successful git pull
func (r *Repo) execThen() error {
var errs error
for _, command := range r.Then {
err := command.Exec(r.Path)
if err == nil {
Logger().Printf("Command '%v' successful.\n", command.Command())
}
errs = mergeErrors(errs, err)
}
return errs
}
func mergeErrors(errs ...error) error {
if len(errs) == 0 {
return nil
}
var err error
for _, e := range errs {
if err == nil {
err = e
continue
}
if e != nil {
err = fmt.Errorf("%v\n%v", err.Error(), e.Error())
}
}
return err
}