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 |