blob: 60eb1e0aea848cfd3cadee62952bb383821fe403 [file] [log] [blame]
Adrià Vilanova Martínez250457f2021-08-08 01:23:14 +02001package main
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "log"
8 "os"
9 "regexp"
10 "strconv"
11 "strings"
12 "sync"
13 "text/template"
14 "time"
15
16 "cloud.google.com/go/pubsub"
17 "github.com/ReneKroon/ttlcache/v2"
18 "go.skia.org/infra/go/monorail/v1"
19 "google.golang.org/api/option"
20)
21
22var (
23 gcloudProjectId = "avm99963-bugs"
24 pubsubSubscriptionId = "gerrit-events-sub"
25 monorailEndpointURL = "https://bugs.avm99963.com/_ah/api/monorail/v1/"
26)
27
28type Bug struct {
29 Project string
30 Number int64
31 WillClose bool
32}
33
34type Account struct {
35 Name string `json:"name"`
36 Email string `json:"email"`
37 Username string `json:"username"`
38}
39
40type Message struct {
41 Timestamp int64 `json:"timestamp"`
42 Reviewer Account `json:"reviewer"`
43 Message string `json:"message"`
44}
45
46type Approval struct {
47 ApprovalType string `json:"type"`
48 Description string `json:"description"`
49 Value string `json:"value"`
50 OldValue string `json:"oldValue"`
51 GrantedOn int64 `json:"grantedOn"`
52 By Account `json:"by"`
53}
54
55type File struct {
56 File string `json:"file"`
57 FileOld string `json:"fileOld"`
58 FileType string `json:"type"`
59 Insertions int `json:"insertions"`
60 Deletions int `json:"deletions"`
61}
62
63type PatchsetComment struct {
64 File string `json:"file"`
65 Line int `json:"line"`
66 Reviewer Account `json:"reviewer"`
67 Message string `json:"message"`
68}
69
70type PatchSet struct {
71 Number int `json:"number"`
72 Revision string `json:"revision"`
73 Parents []string `json:"parents"`
74 Ref string `json:"ref"`
75 Uploader Account `json:"uploader"`
76 Author Account `json:"author"`
77 CreatedOn int64 `json:"createdOn"`
78 Kind string `json:"kind"`
79 Approvals []Approval `json:"approvals"`
80 Comments []PatchsetComment `json:"comments"`
81 Files []File `json:"files"`
82 SizeInsertions int `json:"sizeInsertions"`
83 SizeDeletions int `json:"sizeDeletions"`
84}
85
86type Dependency struct {
87 Id string `json:"id"`
88 Number int `json:"number"`
89 Revision string `json:"revision"`
90 Ref string `json:"ref"`
91 IsCurrentPatchSet bool `json:"isCurrentPatchSet"`
92}
93
94type Label struct {
95 Label string `json:"label"`
96 Status string `json:"status"`
97 By Account `json:"by"`
98}
99
100type Requirement struct {
101 FallbackText string `json:"fallbackText"`
102 RequirementType string `json:"type"`
103 Data map[string]interface{} `json:"data"`
104}
105
106type SubmitRecord struct {
107 Status string `json:"status"`
108 Labels []Label `json:"labels"`
109 Requirements []Requirement `json:"requirements"`
110}
111
112type Change struct {
113 Project string `json:"project"`
114 Branch string `json:"branch"`
115 Id string `json:"id"`
116 Number int `json:"number"`
117 Subject string `json:"subject"`
118 Owner Account `json:"owner"`
119 Url string `json:"url"`
120 CommitMessage string `json:"commitMessage"`
121 Hashtags []string `json:"hashtags"`
122 CreatedOn int64 `json:"createdOn"`
123 LastUpdated int64 `json:"lastUpdated"`
124 Open bool `json:"open"`
125 Status string `json:"status"`
126 Private bool `json:"private"`
127 Wip bool `json:"wip"`
128 Comments []Message `json:"comments"`
129 TrackingIds []int `json:"trackingIds"`
130 CurrentPatchSet PatchSet `json:"currentPatchSet"`
131 PatchSets []PatchSet `json:"patchSets"`
132 DependsOn []Dependency `json:"dependsOn"`
133 NeededBy []Dependency `json:"neededBy"`
134 SubmitRecords SubmitRecord `json:"submitRecords"`
135 AllReviewers []Account `json:"allReviewers"`
136}
137
138type ChangeMergedEvent struct {
139 EventType string `json:"type"`
140 Change Change `json:"change"`
141 PatchSet PatchSet `json:"patchSet"`
142 Submitter Account `json:"submitter"`
143 NewRev string `json:"newRev"`
144 EventCreatedOn int64 `json:"eventCreatedOn"`
145}
146
147var (
148 bugsRegex = regexp.MustCompile(`(Bug:|BUG=|Fixed:)((?:\s*(?:[a-z]\w[-\w]*:)?\d+,)*\s*(?:[a-z]\w[-\w]*:)?\d+),?`)
149)
150
151func getBugsList(message string) []Bug {
152 bugsFooters := bugsRegex.FindAllStringSubmatch(message, -1)
153 var allBugs []Bug
154 for _, footerMatches := range bugsFooters {
155 // This shouldn't happen, but we protect against this case
156 // TODO(avm99963): This should be replaced by something like what DCHECK() does in Chromium
157 if len(footerMatches) < 3 {
158 continue
159 }
160 willCloseBug := footerMatches[1] == "Fixed:"
161 bugs := strings.Split(footerMatches[2], ",")
162 for i, _ := range bugs {
163 bug := strings.TrimSpace(bugs[i])
164 res := strings.Split(bug, ":")
165 if len(res) > 2 {
166 continue
167 }
168 if len(res) == 1 {
169 number, err := strconv.ParseInt(res[0], 10, 64)
170 if err != nil {
171 log.Printf("Couldn't parse bug number from %v: %v", bug, err)
172 }
173 allBugs = append(allBugs, Bug{
174 Project: "misc",
175 Number: number,
176 WillClose: willCloseBug,
177 })
178 } else if len(res) == 2 {
179 number, err := strconv.ParseInt(res[1], 10, 64)
180 if err != nil {
181 log.Printf("Couldn't parse bug number from %v: %v", bug, err)
182 }
183 allBugs = append(allBugs, Bug{
184 Project: res[0],
185 Number: number,
186 WillClose: willCloseBug,
187 })
188 }
189 }
190 }
191 return allBugs
192}
193
194func handlePubSubMessage(ctx context.Context, msg *pubsub.Message, monorailSvc *monorail.Service, template *template.Template, mu sync.Mutex, cache ttlcache.SimpleCache) error {
195 mu.Lock()
196 defer mu.Unlock()
197
198 var m ChangeMergedEvent
199 err := json.Unmarshal(msg.Data, &m)
200 if err != nil {
201 msg.Ack()
202 return err
203 }
204
205 // We're only interested in merged changes
206 //
207 // Even if the struct is meant for change-merged events, unknown fields are
208 // ignored.
209 if m.EventType != "change-merged" {
210 log.Printf("Ignoring message with event '%v'.\n", m.EventType)
211 msg.Ack()
212 return nil
213 }
214
215 if _, err := cache.Get(strconv.Itoa(m.Change.Number)); err != ttlcache.ErrNotFound {
216 log.Printf("Not processing CL:%d since it has been processed before", m.Change.Number)
217 msg.Ack()
218 return nil
219 }
220
221 log.Printf("Processing CL:%d\n", m.Change.Number)
222
223 patchSetTime := time.Unix(m.PatchSet.CreatedOn, 0)
224 patchSetTimeString := patchSetTime.Format(time.RFC1123Z)
225
226 messageBuf := new(bytes.Buffer)
227 template.Execute(messageBuf, struct {
228 Event ChangeMergedEvent
229 CommitURL string
230 Date string
231 }{
232 m,
233 "undefined",
234 patchSetTimeString,
235 })
236 if err != nil {
237 msg.Ack()
238 return err
239 }
240
241 message := messageBuf.String()
242
243 ctxRequest, cancel := context.WithDeadline(ctx, time.Now().Add(45*time.Second))
244 defer cancel()
245
246 bugs := getBugsList(m.Change.CommitMessage)
247 log.Printf("[CL:%d] Bugs: %v\n", m.Change.Number, bugs)
248
249 for _, b := range bugs {
250 comment := monorail.ProtoApiPb2V1IssueCommentWrapper{
251 Content: message,
252 Updates: &monorail.ProtoApiPb2V1Update{},
253 }
254
255 if b.WillClose {
256 comment.Updates.Status = "Fixed"
257 }
258
259 _, err := monorailSvc.Issues.Comments.Insert(b.Project, b.Number, &comment).Context(ctxRequest).Do()
260 if err != nil {
261 log.Printf("[CL:%d] Couldn't add comment to bug %v: %v\n", m.Change.Number, b, err)
262 }
263
264 log.Printf("[CL:%d] Added comment to bug %v\n", m.Change.Number, b)
265 }
266
267 cache.Set(strconv.Itoa(m.Change.Number), true)
268
269 msg.Ack()
270
271 return nil
272}
273
274func main() {
275 log.Println("Starting gitwatcher...")
276
277 ctx := context.Background()
278
279 template, err := template.New("MonorailMessage").Parse(`The following change refers to this bug:
280 {{.Event.Change.Url}}
281
282commit {{.Event.NewRev}}
283Author: {{.Event.PatchSet.Author.Name}} <{{.Event.PatchSet.Author.Email}}>
284Date: {{.Date}}
285
286{{.Event.Change.CommitMessage}}`)
287 if err != nil {
288 log.Fatal(err)
289 }
290
291 cache := ttlcache.NewCache()
292 cache.SetTTL(time.Duration(10 * time.Minute))
293
294 pubsubClient, err := pubsub.NewClient(ctx, gcloudProjectId)
295 if err != nil {
296 log.Fatal(err)
297 }
298 defer pubsubClient.Close()
299
300 jsonFile, ok := os.LookupEnv("GOOGLE_APPLICATION_CREDENTIALS")
301 if !ok {
302 log.Println("GOOGLE_APPLICATION_CREDENTIALS environment variable is not set.")
303 return
304 }
305
306 monorailSvc, err := monorail.NewService(ctx, option.WithEndpoint(monorailEndpointURL), option.WithCredentialsFile(jsonFile))
307 if err != nil {
308 log.Fatal(err)
309 }
310
311 var mu sync.Mutex
312
313 sub := pubsubClient.Subscription(pubsubSubscriptionId)
314 err = sub.Receive(ctx, func(ctx context.Context, msg *pubsub.Message) {
315 err := handlePubSubMessage(ctx, msg, monorailSvc, template, mu, cache)
316 if err != nil {
317 log.Printf("Couldn't handle pubsub message: %v\n", err)
318 }
319 })
320}