Chromium Code Reviews| OLD | NEW |
|---|---|
| (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 ) | |
| 28 | |
| 29 // BugPriorities are the possible values for "Priority-*" labels for issues. | |
| 30 var BugPriorities = []string{"Critical", "High", "Medium", "Low", "Never"} | |
| 31 | |
| 32 const apiScope = "https://www.googleapis.com/auth/projecthosting" | |
| 33 const apiURL = "https://www.googleapis.com/projecthosting/v2/projects/" | |
| 34 const issueURL = "https://code.google.com/p/skia/issues/detail?id=" | |
| 35 | |
| 36 // Enum for determining whether a label has been added, removed, or is | |
| 37 // unchanged. | |
| 38 const ( | |
| 39 labelNew = iota | |
| 40 labelOld = iota | |
| 41 labelBoth = iota | |
| 42 ) | |
| 43 | |
| 44 // loadOAuthConfig reads the OAuth given config file path and returns an | |
| 45 // appropriate oauth.Config. | |
| 46 func loadOAuthConfig(oauthConfigFile string) (*oauth.Config, error) { | |
| 47 fileContents, err := ioutil.ReadFile(oauthConfigFile) | |
| 48 if err != nil { | |
| 49 return nil, err | |
|
jcgregorio
2014/05/07 19:32:25
For cases where you return errors like this you sh
borenet
2014/05/07 22:27:37
Done.
| |
| 50 } | |
| 51 var decodedJson map[string]struct { | |
| 52 AuthURL string `json:"auth_uri"` | |
| 53 ClientId string `json:"client_id"` | |
| 54 ClientSecret string `json:"client_secret"` | |
| 55 TokenURL string `json:"token_uri"` | |
| 56 } | |
| 57 err = json.Unmarshal(fileContents, &decodedJson) | |
| 58 if err != nil { | |
| 59 return nil, err | |
| 60 } | |
| 61 config, ok := decodedJson["web"] | |
| 62 if !ok { | |
| 63 return nil, err | |
| 64 } | |
| 65 return &oauth.Config{ | |
| 66 ClientId: config.ClientId, | |
| 67 ClientSecret: config.ClientSecret, | |
| 68 Scope: apiScope, | |
| 69 AuthURL: config.AuthURL, | |
| 70 TokenURL: config.TokenURL, | |
| 71 }, nil | |
| 72 } | |
| 73 | |
| 74 // Issue contains information about an issue. | |
| 75 type Issue struct { | |
| 76 Id int `json:"id"` | |
| 77 Project string `json:"projectId"` | |
| 78 Title string `json:"title"` | |
| 79 Labels []string `json:"labels"` | |
| 80 } | |
| 81 | |
| 82 // URL returns the URL of a given issue. | |
| 83 func (i *Issue) URL() string { | |
|
jcgregorio
2014/05/07 19:32:25
Doesn't need to be *Issue, just Issue, since you a
borenet
2014/05/07 22:27:37
Done, but I thought this performed a copy if it's
jcgregorio
2014/05/08 15:11:56
That's for arguments, in the case of the
On 2014
| |
| 84 return issueURL + strconv.Itoa(i.Id) | |
| 85 } | |
| 86 | |
| 87 // IssueList represents a list of issues from the IssueTracker. | |
| 88 type IssueList struct { | |
| 89 TotalResults int `json:"totalResults"` | |
| 90 Items []*Issue `json:"items"` | |
| 91 } | |
| 92 | |
| 93 // IssueTracker is the primary point of contact with the issue tracker, | |
| 94 // providing methods for authenticating to and interacting with it. | |
| 95 type IssueTracker struct { | |
| 96 oauthConfig *oauth.Config | |
| 97 oauthTransport *oauth.Transport | |
| 98 } | |
| 99 | |
| 100 // MakeIssueTracker creates and returns an IssueTracker with authentication | |
| 101 // configuration from the given authConfigFile. | |
| 102 func MakeIssueTracker(authConfigFile string) (*IssueTracker, error) { | |
| 103 oauthConfig, err := loadOAuthConfig(authConfigFile) | |
| 104 if err != nil { | |
| 105 return nil, errors.New( | |
| 106 "Unable to read auth config file: " + err.Error()) | |
| 107 } | |
| 108 return &IssueTracker{ | |
| 109 oauthConfig: oauthConfig, | |
| 110 oauthTransport: &oauth.Transport{Config: oauthConfig}, | |
| 111 }, nil | |
| 112 } | |
| 113 | |
| 114 // MakeAuthRequestURL returns an authentication request URL which can be used | |
| 115 // to obtain an authorization code via user sign-in. | |
| 116 func (it *IssueTracker) MakeAuthRequestURL(redirectURL string) string { | |
| 117 it.oauthConfig.RedirectURL = redirectURL | |
| 118 return it.oauthConfig.AuthCodeURL(redirectURL) | |
| 119 } | |
| 120 | |
| 121 // IsAuthenticated determines whether the IssueTracker has sufficient | |
| 122 // permissions to retrieve and edit Issues. | |
| 123 func (it *IssueTracker) IsAuthenticated() bool { | |
| 124 return it.oauthTransport.Token != nil | |
| 125 } | |
| 126 | |
| 127 // UpgradeCode exchanges the single-use authorization code, obtained by | |
| 128 // following the URL obtained from IssueTracker.MakeAuthRequestURL, for a | |
| 129 // multi-use, session token. This is required before IssueTracker can retrieve | |
| 130 // and edit issues. | |
| 131 func (it *IssueTracker) UpgradeCode(code string) error { | |
| 132 token, err := it.oauthTransport.Exchange(code) | |
| 133 if err == nil { | |
| 134 it.oauthTransport.Token = token | |
| 135 } | |
| 136 return err | |
| 137 } | |
| 138 | |
| 139 // GetBug retrieves the Issue with the given ID from the IssueTracker. | |
| 140 func (it *IssueTracker) GetBug(project string, id int) (*Issue, error) { | |
| 141 if !it.IsAuthenticated() { | |
| 142 return nil, errors.New("User is not authenticated!") | |
| 143 } | |
| 144 requestURL := apiURL + project + "/issues/" + strconv.Itoa(id) | |
| 145 resp, err := it.oauthTransport.Client().Get(requestURL) | |
| 146 if err != nil { | |
| 147 return nil, err | |
| 148 } | |
| 149 defer resp.Body.Close() | |
| 150 body, _ := ioutil.ReadAll(resp.Body) | |
| 151 if resp.StatusCode != http.StatusOK { | |
| 152 return nil, errors.New(fmt.Sprintf( | |
| 153 "Issue tracker returned code %d:%v", | |
| 154 resp.StatusCode, string(body))) | |
| 155 } | |
| 156 var issue Issue | |
| 157 err = json.Unmarshal(body, &issue) | |
| 158 if err != nil { | |
| 159 return nil, err | |
| 160 } | |
| 161 return &issue, nil | |
| 162 } | |
| 163 | |
| 164 // GetBugs retrieves all Issues with the given owner from the IssueTracker, | |
| 165 // returning an IssueList. | |
| 166 func (it *IssueTracker) GetBugs( | |
| 167 project string, owner string) (*IssueList, error) { | |
| 168 if !it.IsAuthenticated() { | |
| 169 return nil, errors.New("User is not authenticated!") | |
| 170 } | |
| 171 params := map[string]string{ | |
| 172 "owner": url.QueryEscape(owner), | |
| 173 "can": "open", | |
| 174 "maxResults": "9999", | |
| 175 } | |
| 176 requestURL := apiURL + project + "/issues?" | |
| 177 first := true | |
| 178 for k, v := range params { | |
| 179 if first { | |
| 180 first = false | |
| 181 } else { | |
| 182 requestURL += "&" | |
| 183 } | |
| 184 requestURL += k + "=" + v | |
| 185 } | |
| 186 resp, err := it.oauthTransport.Client().Get(requestURL) | |
| 187 if err != nil { | |
| 188 return nil, err | |
| 189 } | |
| 190 defer resp.Body.Close() | |
| 191 body, _ := ioutil.ReadAll(resp.Body) | |
| 192 if resp.StatusCode != http.StatusOK { | |
| 193 return nil, errors.New(fmt.Sprintf( | |
| 194 "Issue tracker returned code %d:%v", | |
| 195 resp.StatusCode, string(body))) | |
| 196 } | |
| 197 | |
| 198 var bugList IssueList | |
| 199 err = json.Unmarshal(body, &bugList) | |
| 200 if err != nil { | |
| 201 return nil, err | |
| 202 } | |
| 203 return &bugList, nil | |
| 204 } | |
| 205 | |
| 206 // SubmitIssueChanges creates a comment on the given Issue which modifies it | |
| 207 // according to the contents of the passed-in Issue struct. | |
| 208 func (it *IssueTracker) SubmitIssueChanges( | |
| 209 issue *Issue, comment string) error { | |
| 210 errPrefix := "Error updating issue " + strconv.Itoa(issue.Id) + ": " | |
| 211 if !it.IsAuthenticated() { | |
| 212 return errors.New(errPrefix + "User is not authenticated!") | |
| 213 } | |
| 214 oldIssue, err := it.GetBug(issue.Project, issue.Id) | |
| 215 if err != nil { | |
| 216 return err | |
| 217 } | |
| 218 postData := struct { | |
| 219 Content string `json:"content"` | |
| 220 Updates struct { | |
| 221 Title *string `json:"summary"` | |
| 222 Labels []string `json:"labels"` | |
| 223 } `json:"updates"` | |
| 224 }{} | |
| 225 postData.Content = comment | |
| 226 if issue.Title != oldIssue.Title { | |
| 227 postData.Updates.Title = &issue.Title | |
| 228 } | |
| 229 // TODO(borenet): Add other issue attributes, eg. Owner. | |
| 230 labels := make(map[string]int) | |
| 231 for _, label := range issue.Labels { | |
| 232 labels[label] = labelNew | |
| 233 } | |
| 234 for _, label := range oldIssue.Labels { | |
| 235 if _, ok := labels[label]; ok { | |
| 236 labels[label] = labelBoth | |
| 237 } else { | |
| 238 labels[label] = labelOld | |
| 239 } | |
| 240 } | |
| 241 labelChanges := make([]string, 0) | |
| 242 for labelName, present := range labels { | |
| 243 if present == labelOld { | |
| 244 labelChanges = append(labelChanges, "-"+labelName) | |
| 245 } else if present == labelNew { | |
| 246 labelChanges = append(labelChanges, labelName) | |
| 247 } | |
| 248 } | |
| 249 if len(labelChanges) > 0 { | |
| 250 postData.Updates.Labels = labelChanges | |
| 251 } | |
| 252 | |
| 253 postBytes, err := json.Marshal(&postData) | |
| 254 if err != nil { | |
| 255 return err | |
| 256 } | |
| 257 requestURL := apiURL + issue.Project + "/issues/" + | |
| 258 strconv.Itoa(issue.Id) + "/comments" | |
| 259 resp, err := it.oauthTransport.Client().Post( | |
| 260 requestURL, "application/json", bytes.NewReader(postBytes)) | |
| 261 if err != nil { | |
| 262 return err | |
| 263 } | |
| 264 defer resp.Body.Close() | |
| 265 body, _ := ioutil.ReadAll(resp.Body) | |
| 266 if resp.StatusCode != http.StatusOK { | |
| 267 return errors.New(fmt.Sprintf( | |
| 268 "Issue tracker returned code %d:%v", | |
| 269 resp.StatusCode, string(body))) | |
| 270 } | |
| 271 return nil | |
| 272 } | |
| OLD | NEW |