forked from gojp/goreportcard
/
check.go
301 lines (261 loc) · 7.05 KB
/
check.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
package handlers
import (
"container/heap"
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"time"
"golang.org/x/tools/go/vcs"
"github.com/boltdb/bolt"
)
const (
// DBPath is the relative (or absolute) path to the bolt database file
DBPath string = "goreportcard.db"
// RepoBucket is the bucket in which repos will be cached in the bolt DB
RepoBucket string = "repos"
// MetaBucket is the bucket containing the names of the projects with the
// top 100 high scores, and other meta information
MetaBucket string = "meta"
)
// trimScheme removes a scheme (e.g. https://) from the URL for more
// convenient pasting from browsers.
func trimScheme(repo string) string {
schemeSep := "://"
schemeSepIdx := strings.Index(repo, schemeSep)
if schemeSepIdx > -1 {
return repo[schemeSepIdx+len(schemeSep):]
}
return repo
}
// CheckHandler handles the request for checking a repo
func CheckHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
repo := trimScheme(r.FormValue("repo"))
repoRoot, err := vcs.RepoRootForImportPath(repo, true)
if err != nil || repoRoot.Root == "" || repoRoot.Repo == "" {
log.Println("Failed to create repoRoot:", repoRoot, err)
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`Please enter a valid 'go get'-able package name`))
return
}
log.Printf("Checking repo %q...", repo)
forceRefresh := r.Method != "GET" // if this is a GET request, try to fetch from cached version in boltdb first
resp, err := newChecksResp(repo, forceRefresh)
if err != nil {
log.Println("ERROR: from newChecksResp:", err)
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`Could not go get the repository.`))
return
}
respBytes, err := json.Marshal(resp)
if err != nil {
log.Println("ERROR: could not marshal json:", err)
http.Error(w, err.Error(), 500)
return
}
// write to boltdb
db, err := bolt.Open(DBPath, 0755, &bolt.Options{Timeout: 1 * time.Second})
if err != nil {
log.Println("Failed to open bolt database: ", err)
return
}
defer db.Close()
// is this a new repo? if so, increase the count in the high scores bucket later
isNewRepo := false
var oldRepoBytes []byte
err = db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(RepoBucket))
if b == nil {
return fmt.Errorf("repo bucket not found")
}
oldRepoBytes = b.Get([]byte(repo))
return nil
})
if err != nil {
log.Println(err)
}
// get the old score and store it for stats updating
var oldScore *float64
if isNewRepo = oldRepoBytes == nil; !isNewRepo {
oldRepo := checksResp{}
err = json.Unmarshal(oldRepoBytes, &oldRepo)
if err != nil {
log.Println("ERROR: could not unmarshal json:", err)
http.Error(w, err.Error(), 500)
return
}
oldScore = &oldRepo.Average
}
// if this is a new repo, or the user force-refreshed, update the cache
if isNewRepo || forceRefresh {
err = db.Update(func(tx *bolt.Tx) error {
log.Printf("Saving repo %q to cache...", repo)
b := tx.Bucket([]byte(RepoBucket))
if b == nil {
return fmt.Errorf("repo bucket not found")
}
// save repo to cache
err = b.Put([]byte(repo), respBytes)
if err != nil {
return err
}
// fetch meta-bucket
mb := tx.Bucket([]byte(MetaBucket))
if mb == nil {
return fmt.Errorf("high score bucket not found")
}
// update total repos count
if isNewRepo {
err = updateReposCount(mb, resp, repo)
if err != nil {
return err
}
}
err = updateHighScores(mb, resp, repo)
if err != nil {
return err
}
return updateStats(mb, resp, repo, oldScore)
})
if err != nil {
log.Println("Bolt writing error:", err)
}
}
err = db.Update(func(tx *bolt.Tx) error {
// fetch meta-bucket
mb := tx.Bucket([]byte(MetaBucket))
if mb == nil {
return fmt.Errorf("meta bucket not found")
}
return updateRecentlyViewed(mb, repo)
})
b, err := json.Marshal(map[string]string{"redirect": "/report/" + repo})
if err != nil {
log.Println("JSON marshal error:", err)
}
w.WriteHeader(http.StatusOK)
w.Write(b)
return
}
func updateHighScores(mb *bolt.Bucket, resp checksResp, repo string) error {
// check if we need to update the high score list
if resp.Files < 100 {
// only repos with >= 100 files are considered for the high score list
return nil
}
// start updating high score list
scoreBytes := mb.Get([]byte("scores"))
if scoreBytes == nil {
scoreBytes, _ = json.Marshal([]scoreHeap{})
}
scores := &scoreHeap{}
json.Unmarshal(scoreBytes, scores)
heap.Init(scores)
if len(*scores) > 0 && (*scores)[0].Score > resp.Average*100.0 && len(*scores) == 50 {
// lowest score on list is higher than this repo's score, so no need to add, unless
// we do not have 50 high scores yet
return nil
}
// if this repo is already in the list, remove the original entry:
for i := range *scores {
if (*scores)[i].Repo == repo {
heap.Remove(scores, i)
break
}
}
// now we can safely push it onto the heap
heap.Push(scores, scoreItem{
Repo: repo,
Score: resp.Average * 100.0,
Files: resp.Files,
})
if len(*scores) > 50 {
// trim heap if it's grown to over 50
*scores = (*scores)[1:51]
}
scoreBytes, err := json.Marshal(&scores)
if err != nil {
return err
}
err = mb.Put([]byte("scores"), scoreBytes)
if err != nil {
return err
}
return nil
}
func updateStats(mb *bolt.Bucket, resp checksResp, repo string, oldScore *float64) error {
scores := make([]int, 101, 101)
statsBytes := mb.Get([]byte("stats"))
if statsBytes == nil {
statsBytes, _ = json.Marshal(scores)
}
err := json.Unmarshal(statsBytes, &scores)
if err != nil {
return err
}
scores[int(resp.Average*100)]++
if oldScore != nil {
scores[int(*oldScore*100)]--
}
newStats, err := json.Marshal(scores)
if err != nil {
return err
}
err = mb.Put([]byte("stats"), newStats)
if err != nil {
return err
}
return nil
}
func updateReposCount(mb *bolt.Bucket, resp checksResp, repo string) (err error) {
log.Printf("New repo %q, adding to repo count...", repo)
totalInt := 0
total := mb.Get([]byte("total_repos"))
if total != nil {
err = json.Unmarshal(total, &totalInt)
if err != nil {
return fmt.Errorf("could not unmarshal total repos count: %v", err)
}
}
totalInt++ // increase repo count
total, err = json.Marshal(totalInt)
if err != nil {
return fmt.Errorf("could not marshal total repos count: %v", err)
}
mb.Put([]byte("total_repos"), total)
log.Println("Repo count is now", totalInt)
return nil
}
type recentItem struct {
Repo string
}
func updateRecentlyViewed(mb *bolt.Bucket, repo string) error {
b := mb.Get([]byte("recent"))
if b == nil {
b, _ = json.Marshal([]recentItem{})
}
recent := []recentItem{}
json.Unmarshal(b, &recent)
// add it to the slice, if it is not in there already
for i := range recent {
if recent[i].Repo == repo {
return nil
}
}
recent = append(recent, recentItem{Repo: repo})
if len(recent) > 5 {
// trim recent if it's grown to over 5
recent = (recent)[1:6]
}
b, err := json.Marshal(&recent)
if err != nil {
return err
}
err = mb.Put([]byte("recent"), b)
if err != nil {
return err
}
return nil
}