Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(67)

Side by Side Diff: tools/bug_chomper/src/issue_tracker/issue_tracker.go

Issue 274693002: BugChomper utility - rewrite in Go (Closed) Base URL: https://skia.googlesource.com/skia.git@master
Patch Set: Address comments Created 6 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « tools/bug_chomper/run_server.sh ('k') | tools/bug_chomper/src/server/server.go » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright (c) 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 /*
6 Utilities for interacting with the GoogleCode issue tracker.
7
8 Example usage:
9 issueTracker := issue_tracker.MakeIssueTraker(myOAuthConfigFile)
10 authURL := issueTracker.MakeAuthRequestURL()
11 // Visit the authURL to obtain an authorization code.
12 issueTracker.UpgradeCode(code)
13 // Now issueTracker can be used to retrieve and edit issues.
14 */
15 package issue_tracker
16
17 import (
18 "bytes"
19 "code.google.com/p/goauth2/oauth"
20 "encoding/json"
21 "errors"
22 "fmt"
23 "io/ioutil"
24 "net/http"
25 "net/url"
26 "strconv"
27 "strings"
28 )
29
30 // BugPriorities are the possible values for "Priority-*" labels for issues.
31 var BugPriorities = []string{"Critical", "High", "Medium", "Low", "Never"}
32
33 var apiScope = []string{
34 "https://www.googleapis.com/auth/projecthosting",
35 "https://www.googleapis.com/auth/userinfo.email",
36 }
37
38 const issueApiURL = "https://www.googleapis.com/projecthosting/v2/projects/"
39 const issueURL = "https://code.google.com/p/skia/issues/detail?id="
40 const personApiURL = "https://www.googleapis.com/userinfo/v2/me"
41
42 // Enum for determining whether a label has been added, removed, or is
43 // unchanged.
44 const (
45 labelAdded = iota
46 labelRemoved = iota
47 labelUnchanged = iota
borenet 2014/05/07 22:27:38 Changed for clarity.
jcgregorio 2014/05/08 15:11:57 No need to repeat iota, see http://golang.org/doc/
borenet 2014/05/09 15:40:11 Done.
48 )
49
50 // loadOAuthConfig reads the OAuth given config file path and returns an
51 // appropriate oauth.Config.
52 func loadOAuthConfig(oauthConfigFile string) (*oauth.Config, error) {
53 errFmt := "failed to read OAuth config file: %s"
54 fileContents, err := ioutil.ReadFile(oauthConfigFile)
55 if err != nil {
56 return nil, fmt.Errorf(errFmt, err)
57 }
58 var decodedJson map[string]struct {
59 AuthURL string `json:"auth_uri"`
60 ClientId string `json:"client_id"`
61 ClientSecret string `json:"client_secret"`
62 TokenURL string `json:"token_uri"`
63 }
64 err = json.Unmarshal(fileContents, &decodedJson)
65 if err != nil {
66 return nil, fmt.Errorf(errFmt, err)
67 }
68 config, ok := decodedJson["web"]
69 if !ok {
70 return nil, fmt.Errorf(errFmt, err)
71 }
72 return &oauth.Config{
73 ClientId: config.ClientId,
74 ClientSecret: config.ClientSecret,
75 Scope: strings.Join(apiScope, " "),
76 AuthURL: config.AuthURL,
77 TokenURL: config.TokenURL,
78 AccessType: "offline",
borenet 2014/05/07 22:27:38 Not sure I like this. I think I'd rather just hav
jcgregorio 2014/05/08 15:11:57 That's reasonable given the tool, so "online" woul
borenet 2014/05/09 15:40:11 "offline" is the zero-value, so I just removed thi
borenet 2014/05/12 20:05:51 I meant "online" here.
79 }, nil
80 }
81
82 // Issue contains information about an issue.
83 type Issue struct {
84 Id int `json:"id"`
85 Project string `json:"projectId"`
86 Title string `json:"title"`
87 Labels []string `json:"labels"`
88 }
89
90 // URL returns the URL of a given issue.
91 func (i Issue) URL() string {
92 return issueURL + strconv.Itoa(i.Id)
93 }
94
95 // IssueList represents a list of issues from the IssueTracker.
96 type IssueList struct {
97 TotalResults int `json:"totalResults"`
98 Items []*Issue `json:"items"`
99 }
100
101 // IssueTracker is the primary point of contact with the issue tracker,
102 // providing methods for authenticating to and interacting with it.
103 type IssueTracker struct {
104 oauthConfig *oauth.Config
105 oauthTransport *oauth.Transport
106 }
107
108 // MakeIssueTracker creates and returns an IssueTracker with authentication
109 // configuration from the given authConfigFile.
110 func MakeIssueTracker(authConfigFile string) (*IssueTracker, error) {
111 oauthConfig, err := loadOAuthConfig(authConfigFile)
112 if err != nil {
113 return nil, fmt.Errorf(
114 "failed to create IssueTracker: %s", err)
115 }
116 return &IssueTracker{
117 oauthConfig: oauthConfig,
118 oauthTransport: &oauth.Transport{Config: oauthConfig},
119 }, nil
120 }
121
122 // MakeAuthRequestURL returns an authentication request URL which can be used
123 // to obtain an authorization code via user sign-in.
124 func (it *IssueTracker) MakeAuthRequestURL(redirectURL string) string {
125 it.oauthConfig.RedirectURL = redirectURL
126 return it.oauthConfig.AuthCodeURL(redirectURL)
jcgregorio 2014/05/08 15:11:57 I didn't realize how bad the goauth2 library is, i
borenet 2014/05/09 15:40:11 So my intention was to make this something which *
127 }
128
129 // IsAuthenticated determines whether the IssueTracker has sufficient
130 // permissions to retrieve and edit Issues.
131 func (it *IssueTracker) IsAuthenticated() bool {
jcgregorio 2014/05/08 15:11:57 it IssueTracker Check all instances, if the call
borenet 2014/05/09 15:40:11 Done.
132 return it.oauthTransport.Token != nil
133 }
134
135 // UpgradeCode exchanges the single-use authorization code, obtained by
136 // following the URL obtained from IssueTracker.MakeAuthRequestURL, for a
137 // multi-use, session token. This is required before IssueTracker can retrieve
138 // and edit issues.
139 func (it *IssueTracker) UpgradeCode(code string) error {
140 token, err := it.oauthTransport.Exchange(code)
141 if err == nil {
142 it.oauthTransport.Token = token
143 return nil
144 } else {
145 return fmt.Errorf("failed to exchange single-user auth code: %s" , err)
146 }
147 }
148
149 // GetLoggedInUser retrieves the email address of the authenticated user.
150 func (it *IssueTracker) GetLoggedInUser() (string, error) {
151 errFmt := "error retrieving user email: %s"
152 if !it.IsAuthenticated() {
153 return "", fmt.Errorf(
154 errFmt, errors.New("User is not authenticated!"))
jcgregorio 2014/05/08 15:11:57 fmt.Errorf(errFmt, errors.New( can be more cleanl
borenet 2014/05/09 15:40:11 Done.
155 }
156 resp, err := it.oauthTransport.Client().Get(personApiURL)
157 if err != nil {
158 return "", fmt.Errorf(errFmt, err)
159 }
160 defer resp.Body.Close()
161 body, _ := ioutil.ReadAll(resp.Body)
162 if resp.StatusCode != http.StatusOK {
163 return "", fmt.Errorf(errFmt, errors.New(fmt.Sprintf(
164 "user data API returned code %d: %v",
165 resp.StatusCode, string(body))))
166 }
167 userInfo := struct {
168 Email string `json:"email"`
169 }{}
170 err = json.Unmarshal(body, &userInfo)
171 if err != nil {
172 return "", fmt.Errorf(errFmt, err)
173 }
174 return userInfo.Email, nil
175 }
176
177 // GetBug retrieves the Issue with the given ID from the IssueTracker.
178 func (it *IssueTracker) GetBug(project string, id int) (*Issue, error) {
179 errFmt := fmt.Sprintf("error retrieving issue %d: %s", id)
jcgregorio 2014/05/08 15:11:57 I don't think this works the way you want it to, s
borenet 2014/05/09 15:40:11 Modified it in a slightly dumb way.
180 if !it.IsAuthenticated() {
181 return nil, fmt.Errorf(
182 errFmt, errors.New("user is not authenticated!"))
183 }
184 requestURL := issueApiURL + project + "/issues/" + strconv.Itoa(id)
185 resp, err := it.oauthTransport.Client().Get(requestURL)
186 if err != nil {
187 return nil, fmt.Errorf(errFmt, err)
188 }
189 defer resp.Body.Close()
190 body, _ := ioutil.ReadAll(resp.Body)
191 if resp.StatusCode != http.StatusOK {
192 return nil, fmt.Errorf(errFmt, errors.New(fmt.Sprintf(
193 "issue tracker returned code %d:%v",
194 resp.StatusCode, string(body))))
195 }
196 var issue Issue
197 err = json.Unmarshal(body, &issue)
198 if err != nil {
199 return nil, fmt.Errorf(errFmt, err)
200 }
jcgregorio 2014/05/08 15:11:57 if err := json.Unmarshal(body, &issue); err != nil
borenet 2014/05/09 15:40:11 Done.
201 return &issue, nil
202 }
203
204 // GetBugs retrieves all Issues with the given owner from the IssueTracker,
205 // returning an IssueList.
206 func (it *IssueTracker) GetBugs(
207 project string, owner string) (*IssueList, error) {
208 errFmt := "error retrieving issues: %s"
209 if !it.IsAuthenticated() {
210 return nil, fmt.Errorf(errFmt,
211 errors.New("user is not authenticated!"))
212 }
213 params := map[string]string{
214 "owner": url.QueryEscape(owner),
215 "can": "open",
216 "maxResults": "9999",
217 }
218 requestURL := issueApiURL + project + "/issues?"
219 first := true
220 for k, v := range params {
221 if first {
222 first = false
223 } else {
224 requestURL += "&"
225 }
226 requestURL += k + "=" + v
227 }
228 resp, err := it.oauthTransport.Client().Get(requestURL)
229 if err != nil {
230 return nil, fmt.Errorf(errFmt, err)
231 }
232 defer resp.Body.Close()
233 body, _ := ioutil.ReadAll(resp.Body)
234 if resp.StatusCode != http.StatusOK {
235 return nil, fmt.Errorf(errFmt, errors.New(fmt.Sprintf(
236 "issue tracker returned code %d:%v",
237 resp.StatusCode, string(body))))
238 }
239
240 var bugList IssueList
241 err = json.Unmarshal(body, &bugList)
242 if err != nil {
243 return nil, fmt.Errorf(errFmt, err)
244 }
245 return &bugList, nil
246 }
247
248 // SubmitIssueChanges creates a comment on the given Issue which modifies it
249 // according to the contents of the passed-in Issue struct.
250 func (it *IssueTracker) SubmitIssueChanges(
251 issue *Issue, comment string) error {
252 errFmt := "Error updating issue " + strconv.Itoa(issue.Id) + ": %s"
253 if !it.IsAuthenticated() {
254 return fmt.Errorf(
255 errFmt, errors.New("user is not authenticated!"))
256 }
257 oldIssue, err := it.GetBug(issue.Project, issue.Id)
258 if err != nil {
259 return fmt.Errorf(errFmt, err)
260 }
261 postData := struct {
262 Content string `json:"content"`
263 Updates struct {
264 Title *string `json:"summary"`
265 Labels []string `json:"labels"`
266 } `json:"updates"`
267 }{}
268 postData.Content = comment
jcgregorio 2014/05/08 15:11:57 }{ Content: comment, }
borenet 2014/05/09 15:40:11 Done.
269 if issue.Title != oldIssue.Title {
270 postData.Updates.Title = &issue.Title
271 }
272 // TODO(borenet): Add other issue attributes, eg. Owner.
273 labels := make(map[string]int)
274 for _, label := range issue.Labels {
275 labels[label] = labelAdded
276 }
277 for _, label := range oldIssue.Labels {
278 if _, ok := labels[label]; ok {
279 labels[label] = labelUnchanged
280 } else {
281 labels[label] = labelRemoved
282 }
283 }
284 labelChanges := make([]string, 0)
285 for labelName, present := range labels {
286 if present == labelRemoved {
287 labelChanges = append(labelChanges, "-"+labelName)
288 } else if present == labelAdded {
289 labelChanges = append(labelChanges, labelName)
290 }
291 }
292 if len(labelChanges) > 0 {
293 postData.Updates.Labels = labelChanges
294 }
295
296 postBytes, err := json.Marshal(&postData)
297 if err != nil {
298 return fmt.Errorf(errFmt, err)
299 }
300 requestURL := issueApiURL + issue.Project + "/issues/" +
301 strconv.Itoa(issue.Id) + "/comments"
302 resp, err := it.oauthTransport.Client().Post(
303 requestURL, "application/json", bytes.NewReader(postBytes))
304 if err != nil {
305 return fmt.Errorf(errFmt, err)
306 }
307 defer resp.Body.Close()
308 body, _ := ioutil.ReadAll(resp.Body)
309 if resp.StatusCode != http.StatusOK {
310 return fmt.Errorf(errFmt, errors.New(fmt.Sprintf(
311 "Issue tracker returned code %d:%v",
312 resp.StatusCode, string(body))))
313 }
314 return nil
315 }
OLDNEW
« no previous file with comments | « tools/bug_chomper/run_server.sh ('k') | tools/bug_chomper/src/server/server.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698