forked from sourcegraph/checkup
-
Notifications
You must be signed in to change notification settings - Fork 0
/
github.go
312 lines (262 loc) · 8.87 KB
/
github.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
package checkup
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"path/filepath"
"strings"
"time"
"github.com/google/go-github/github"
"golang.org/x/oauth2"
)
var errFileNotFound = fmt.Errorf("file not found on github")
// GitHub is a way to store checkup results in a GitHub repository.
type GitHub struct {
// AccessToken is the API token used to authenticate with GitHub (required).
AccessToken string `json:"access_token"`
// RepositoryOwner is the account which owns the repository on GitHub (required).
// For https://github.com/octocat/kit, the owner is "octocat".
RepositoryOwner string `json:"repository_owner"`
// RepositoryName is the name of the repository on GitHub (required).
// For https://github.com/octocat/kit, the name is "kit".
RepositoryName string `json:"repository_name"`
// CommitterName is the display name of the user corresponding to the AccessToken (required).
// If the AccessToken is for user @octocat, then this might be "Mona Lisa," her name.
CommitterName string `json:"committer_name"`
// CommitterEmail is the email address of the user corresponding to the AccessToken (required).
// If the AccessToken is for user @octocat, then this might be "[email protected]".
CommitterEmail string `json:"committer_email"`
// Branch is the git branch to store the files to (required).
Branch string `json:"branch"`
// Dir is the subdirectory in the Git tree in which to store the files (required).
// For example, to write to the directory "updates" in the Git repo, this should be "updates".
Dir string `json:"dir"`
// Check files older than CheckExpiry will be
// deleted on calls to Maintain(). If this is
// the zero value, no old check files will be
// deleted.
CheckExpiry time.Duration `json:"check_expiry,omitempty"`
client *github.Client `json:"-"`
}
// ensureClient builds an GitHub API client if none exists and stores it on the struct.
func (gh *GitHub) ensureClient() error {
if gh.client != nil {
return nil
}
if gh.AccessToken == "" {
return fmt.Errorf("Please specify access_token in storage configuration")
}
gh.client = github.NewClient(oauth2.NewClient(
oauth2.NoContext,
oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: gh.AccessToken},
),
))
return nil
}
// fullPathName ensures the configured Dir value is present in the filename and
// returns a filename with the Dir prefixed before the input filename if necessary.
func (gh *GitHub) fullPathName(filename string) string {
if strings.HasPrefix(filename, gh.Dir) {
return filename
} else {
return filepath.Join(gh.Dir, filename)
}
}
// readFile reads a file from the Git repository at its latest revision.
// This method returns the plaintext contents, the SHA associated with the contents
// If an error occurs, the contents and sha will be nil & empty.
func (gh *GitHub) readFile(filename string) ([]byte, string, error) {
if err := gh.ensureClient(); err != nil {
return nil, "", err
}
contents, _, resp, err := gh.client.Repositories.GetContents(
context.Background(),
gh.RepositoryOwner,
gh.RepositoryName,
gh.fullPathName(filename),
&github.RepositoryContentGetOptions{Ref: "heads/" + gh.Branch},
)
if err != nil {
if resp != nil && resp.StatusCode == http.StatusNotFound {
return nil, "", errFileNotFound
}
return nil, "", err
}
decoded, err := contents.GetContent()
return []byte(decoded), *contents.SHA, err
}
// writeFile commits the contents to the Git repo at the given filename & revision.
// If the Git repo does not yet have a file at this filename, it will create the file.
// Otherwise, it will simply update the file with the new contents.
func (gh *GitHub) writeFile(filename string, sha string, contents []byte) error {
if err := gh.ensureClient(); err != nil {
return err
}
var err error
var writeFunc func(context.Context, string, string, string, *github.RepositoryContentFileOptions) (*github.RepositoryContentResponse, *github.Response, error)
opts := &github.RepositoryContentFileOptions{
Message: github.String(fmt.Sprintf("[checkup] store %s [ci skip]", gh.fullPathName(filename))),
Content: contents,
Committer: &github.CommitAuthor{
Name: &gh.CommitterName,
Email: &gh.CommitterEmail,
},
}
if gh.Branch != "" {
opts.Branch = &gh.Branch
}
// If no SHA specified, then create the file.
// Otherwise, update the file at the specified SHA.
if sha == "" {
writeFunc = gh.client.Repositories.CreateFile
log.Printf("github: creating %s on branch '%s'", gh.fullPathName(filename), gh.Branch)
} else {
opts.SHA = github.String(sha)
writeFunc = gh.client.Repositories.UpdateFile
log.Printf("github: updating %s on branch '%s'", gh.fullPathName(filename), gh.Branch)
}
_, _, err = writeFunc(
context.Background(),
gh.RepositoryOwner,
gh.RepositoryName,
gh.fullPathName(filename),
opts,
)
return err
}
// deleteFile deletes a file from a Git tree and returns any applicable errors.
// If an empty SHA is passed as an argument, errFileNotFound is returned.
func (gh *GitHub) deleteFile(filename string, sha string) error {
if err := gh.ensureClient(); err != nil {
return err
}
if sha == "" {
return errFileNotFound
}
log.Printf("github: deleting %s on branch '%s'", gh.fullPathName(filename), gh.Branch)
_, _, err := gh.client.Repositories.DeleteFile(
context.Background(),
gh.RepositoryOwner,
gh.RepositoryName,
gh.fullPathName(filename),
&github.RepositoryContentFileOptions{
Message: github.String(fmt.Sprintf("[checkup] delete %s [ci skip]", gh.fullPathName(filename))),
SHA: github.String(sha),
Branch: &gh.Branch,
Committer: &github.CommitAuthor{
Name: &gh.CommitterName,
Email: &gh.CommitterEmail,
},
},
)
if err != nil {
return err
}
return nil
}
// readIndex reads the index JSON from the Git repo into a map.
// It returns the populated map & the Git SHA associated with the contents.
// If the index file is not found in the Git repo, an empty index is returned with no error.
// If any error occurs, a nil index and empty SHA are returned along with the error.
func (gh *GitHub) readIndex() (map[string]int64, string, error) {
index := map[string]int64{}
contents, sha, err := gh.readFile(indexName)
if err != nil && err != errFileNotFound {
return nil, "", err
}
if err == errFileNotFound {
return index, "", nil
}
err = json.Unmarshal(contents, &index)
return index, sha, err
}
// writeIndex marshals the index into JSON and writes the file to the Git repo.
// It returns any errors associated with marshaling the data or writing the file.
func (gh *GitHub) writeIndex(index map[string]int64, sha string) error {
contents, err := json.Marshal(index)
if err != nil {
return err
}
return gh.writeFile(indexName, sha, contents)
}
// Store stores results in the Git repo & updates the index.
func (gh *GitHub) Store(results []Result) error {
// Write results to a new file
name := *GenerateFilename()
contents, err := json.Marshal(results)
if err != nil {
return err
}
err = gh.writeFile(name, "", contents)
// Read current index file
index, indexSHA, err := gh.readIndex()
if err != nil {
return err
}
// Add new file to index
index[name] = time.Now().UnixNano()
// Write new index
return gh.writeIndex(index, indexSHA)
}
// Fetch returns a checkup record -- Not tested!
func (gh *GitHub) Fetch(name string) ([]Result, error) {
contents, _, err := gh.readFile(name)
if err != nil {
return nil, err
}
var r []Result
err = json.Unmarshal(contents, &r)
return r, err
}
// GetIndex returns the checkup index
func (gh *GitHub) GetIndex() (map[string]int64, error) {
m, _, e := gh.readIndex()
return m, e
}
// Maintain deletes check files that are older than gh.CheckExpiry.
func (gh *GitHub) Maintain() error {
if gh.CheckExpiry == 0 {
return nil
}
if err := gh.ensureClient(); err != nil {
return err
}
index, indexSHA, err := gh.readIndex()
if err != nil {
return err
}
ref, _, err := gh.client.Git.GetRef(context.Background(), gh.RepositoryOwner, gh.RepositoryName, "heads/"+gh.Branch)
if err != nil {
return err
}
tree, _, err := gh.client.Git.GetTree(context.Background(), gh.RepositoryOwner, gh.RepositoryName, *ref.Object.SHA, true)
if err != nil {
return err
}
for _, treeEntry := range tree.Entries {
fileName := treeEntry.GetPath()
if fileName == filepath.Join(gh.Dir, indexName) {
continue
}
if gh.Dir != "" && !strings.HasPrefix(fileName, gh.Dir) {
log.Printf("github: maintain: skipping %s because it isn't in the configured subdirectory", fileName)
continue
}
nsec, ok := index[filepath.Base(fileName)]
if !ok {
log.Printf("github: maintain: skipping %s because it's not in the index", fileName)
continue
}
if time.Since(time.Unix(0, nsec)) > gh.CheckExpiry {
log.Printf("github: maintain: deleting %s", fileName)
if err = gh.deleteFile(fileName, treeEntry.GetSHA()); err != nil {
return err
}
delete(index, fileName)
}
}
return gh.writeIndex(index, indexSHA)
}