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

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: 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
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 )
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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698