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

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: Line breaks and "go fmt" one more time 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 "fmt"
22 "io/ioutil"
23 "net/http"
24 "net/url"
25 "strconv"
26 "strings"
27 )
28
29 // BugPriorities are the possible values for "Priority-*" labels for issues.
30 var BugPriorities = []string{"Critical", "High", "Medium", "Low", "Never"}
31
32 var apiScope = []string{
33 "https://www.googleapis.com/auth/projecthosting",
34 "https://www.googleapis.com/auth/userinfo.email",
35 }
36
37 const issueApiURL = "https://www.googleapis.com/projecthosting/v2/projects/"
38 const issueURL = "https://code.google.com/p/skia/issues/detail?id="
39 const personApiURL = "https://www.googleapis.com/userinfo/v2/me"
40
41 // Enum for determining whether a label has been added, removed, or is
42 // unchanged.
43 const (
44 labelAdded = iota
45 labelRemoved
46 labelUnchanged
47 )
48
49 // loadOAuthConfig reads the OAuth given config file path and returns an
50 // appropriate oauth.Config.
51 func loadOAuthConfig(oauthConfigFile string) (*oauth.Config, error) {
52 errFmt := "failed to read OAuth config file: %s"
53 fileContents, err := ioutil.ReadFile(oauthConfigFile)
54 if err != nil {
55 return nil, fmt.Errorf(errFmt, err)
56 }
57 var decodedJson map[string]struct {
58 AuthURL string `json:"auth_uri"`
59 ClientId string `json:"client_id"`
60 ClientSecret string `json:"client_secret"`
61 TokenURL string `json:"token_uri"`
62 }
63 if err := json.Unmarshal(fileContents, &decodedJson); err != nil {
64 return nil, fmt.Errorf(errFmt, err)
65 }
66 config, ok := decodedJson["web"]
67 if !ok {
68 return nil, fmt.Errorf(errFmt, err)
69 }
70 return &oauth.Config{
71 ClientId: config.ClientId,
72 ClientSecret: config.ClientSecret,
73 Scope: strings.Join(apiScope, " "),
74 AuthURL: config.AuthURL,
75 TokenURL: config.TokenURL,
76 }, nil
77 }
78
79 // Issue contains information about an issue.
80 type Issue struct {
81 Id int `json:"id"`
82 Project string `json:"projectId"`
83 Title string `json:"title"`
84 Labels []string `json:"labels"`
85 }
86
87 // URL returns the URL of a given issue.
88 func (i Issue) URL() string {
89 return issueURL + strconv.Itoa(i.Id)
90 }
91
92 // IssueList represents a list of issues from the IssueTracker.
93 type IssueList struct {
94 TotalResults int `json:"totalResults"`
95 Items []*Issue `json:"items"`
96 }
97
98 // IssueTracker is the primary point of contact with the issue tracker,
99 // providing methods for authenticating to and interacting with it.
100 type IssueTracker struct {
101 OAuthConfig *oauth.Config
102 OAuthTransport *oauth.Transport
103 }
104
105 // MakeIssueTracker creates and returns an IssueTracker with authentication
106 // configuration from the given authConfigFile.
107 func MakeIssueTracker(authConfigFile string, redirectURL string) (*IssueTracker, error) {
108 oauthConfig, err := loadOAuthConfig(authConfigFile)
109 if err != nil {
110 return nil, fmt.Errorf(
111 "failed to create IssueTracker: %s", err)
112 }
113 oauthConfig.RedirectURL = redirectURL
114 return &IssueTracker{
115 OAuthConfig: oauthConfig,
116 OAuthTransport: &oauth.Transport{Config: oauthConfig},
117 }, nil
118 }
119
120 // MakeAuthRequestURL returns an authentication request URL which can be used
121 // to obtain an authorization code via user sign-in.
122 func (it IssueTracker) MakeAuthRequestURL() string {
123 // NOTE: Need to add XSRF protection if we ever want to run this on a pu blic
124 // server.
125 return it.OAuthConfig.AuthCodeURL(it.OAuthConfig.RedirectURL)
126 }
127
128 // IsAuthenticated determines whether the IssueTracker has sufficient
129 // permissions to retrieve and edit Issues.
130 func (it IssueTracker) IsAuthenticated() bool {
131 return it.OAuthTransport.Token != nil
132 }
133
134 // UpgradeCode exchanges the single-use authorization code, obtained by
135 // following the URL obtained from IssueTracker.MakeAuthRequestURL, for a
136 // multi-use, session token. This is required before IssueTracker can retrieve
137 // and edit issues.
138 func (it *IssueTracker) UpgradeCode(code string) error {
139 token, err := it.OAuthTransport.Exchange(code)
140 if err == nil {
141 it.OAuthTransport.Token = token
142 return nil
143 } else {
144 return fmt.Errorf(
145 "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(errFmt, "User is not authenticated!")
154 }
155 resp, err := it.OAuthTransport.Client().Get(personApiURL)
156 if err != nil {
157 return "", fmt.Errorf(errFmt, err)
158 }
159 defer resp.Body.Close()
160 body, _ := ioutil.ReadAll(resp.Body)
161 if resp.StatusCode != http.StatusOK {
162 return "", fmt.Errorf(errFmt, fmt.Sprintf(
163 "user data API returned code %d: %v", resp.StatusCode, s tring(body)))
164 }
165 userInfo := struct {
166 Email string `json:"email"`
167 }{}
168 if err := json.Unmarshal(body, &userInfo); err != nil {
169 return "", fmt.Errorf(errFmt, err)
170 }
171 return userInfo.Email, nil
172 }
173
174 // GetBug retrieves the Issue with the given ID from the IssueTracker.
175 func (it IssueTracker) GetBug(project string, id int) (*Issue, error) {
176 errFmt := fmt.Sprintf("error retrieving issue %d: %s", id, "%s")
177 if !it.IsAuthenticated() {
178 return nil, fmt.Errorf(errFmt, "user is not authenticated!")
179 }
180 requestURL := issueApiURL + project + "/issues/" + strconv.Itoa(id)
181 resp, err := it.OAuthTransport.Client().Get(requestURL)
182 if err != nil {
183 return nil, fmt.Errorf(errFmt, err)
184 }
185 defer resp.Body.Close()
186 body, _ := ioutil.ReadAll(resp.Body)
187 if resp.StatusCode != http.StatusOK {
188 return nil, fmt.Errorf(errFmt, fmt.Sprintf(
189 "issue tracker returned code %d:%v", resp.StatusCode, st ring(body)))
190 }
191 var issue Issue
192 if err := json.Unmarshal(body, &issue); err != nil {
193 return nil, fmt.Errorf(errFmt, err)
194 }
195 return &issue, nil
196 }
197
198 // GetBugs retrieves all Issues with the given owner from the IssueTracker,
199 // returning an IssueList.
200 func (it IssueTracker) GetBugs(project string, owner string) (*IssueList, error) {
201 errFmt := "error retrieving issues: %s"
202 if !it.IsAuthenticated() {
203 return nil, fmt.Errorf(errFmt, "user is not authenticated!")
204 }
205 params := map[string]string{
206 "owner": url.QueryEscape(owner),
207 "can": "open",
208 "maxResults": "9999",
209 }
210 requestURL := issueApiURL + project + "/issues?"
211 first := true
212 for k, v := range params {
213 if first {
214 first = false
215 } else {
216 requestURL += "&"
217 }
218 requestURL += k + "=" + v
219 }
220 resp, err := it.OAuthTransport.Client().Get(requestURL)
221 if err != nil {
222 return nil, fmt.Errorf(errFmt, err)
223 }
224 defer resp.Body.Close()
225 body, _ := ioutil.ReadAll(resp.Body)
226 if resp.StatusCode != http.StatusOK {
227 return nil, fmt.Errorf(errFmt, fmt.Sprintf(
228 "issue tracker returned code %d:%v", resp.StatusCode, st ring(body)))
229 }
230
231 var bugList IssueList
232 if err := json.Unmarshal(body, &bugList); err != nil {
233 return nil, fmt.Errorf(errFmt, err)
234 }
235 return &bugList, nil
236 }
237
238 // SubmitIssueChanges creates a comment on the given Issue which modifies it
239 // according to the contents of the passed-in Issue struct.
240 func (it IssueTracker) SubmitIssueChanges(issue *Issue, comment string) error {
241 errFmt := "Error updating issue " + strconv.Itoa(issue.Id) + ": %s"
242 if !it.IsAuthenticated() {
243 return fmt.Errorf(errFmt, "user is not authenticated!")
244 }
245 oldIssue, err := it.GetBug(issue.Project, issue.Id)
246 if err != nil {
247 return fmt.Errorf(errFmt, err)
248 }
249 postData := struct {
250 Content string `json:"content"`
251 Updates struct {
252 Title *string `json:"summary"`
253 Labels []string `json:"labels"`
254 } `json:"updates"`
255 }{
256 Content: comment,
257 }
258 if issue.Title != oldIssue.Title {
259 postData.Updates.Title = &issue.Title
260 }
261 // TODO(borenet): Add other issue attributes, eg. Owner.
262 labels := make(map[string]int)
263 for _, label := range issue.Labels {
264 labels[label] = labelAdded
265 }
266 for _, label := range oldIssue.Labels {
267 if _, ok := labels[label]; ok {
268 labels[label] = labelUnchanged
269 } else {
270 labels[label] = labelRemoved
271 }
272 }
273 labelChanges := make([]string, 0)
274 for labelName, present := range labels {
275 if present == labelRemoved {
276 labelChanges = append(labelChanges, "-"+labelName)
277 } else if present == labelAdded {
278 labelChanges = append(labelChanges, labelName)
279 }
280 }
281 if len(labelChanges) > 0 {
282 postData.Updates.Labels = labelChanges
283 }
284
285 postBytes, err := json.Marshal(&postData)
286 if err != nil {
287 return fmt.Errorf(errFmt, err)
288 }
289 requestURL := issueApiURL + issue.Project + "/issues/" +
290 strconv.Itoa(issue.Id) + "/comments"
291 resp, err := it.OAuthTransport.Client().Post(
292 requestURL, "application/json", bytes.NewReader(postBytes))
293 if err != nil {
294 return fmt.Errorf(errFmt, err)
295 }
296 defer resp.Body.Close()
297 body, _ := ioutil.ReadAll(resp.Body)
298 if resp.StatusCode != http.StatusOK {
299 return fmt.Errorf(errFmt, fmt.Sprintf(
300 "Issue tracker returned code %d:%v", resp.StatusCode, st ring(body)))
301 }
302 return nil
303 }
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