e4854f5ee6d4c27142f991eaf05babc9294f0b98
[platform/framework/web/crosswalk.git] / src / third_party / skia / tools / bug_chomper / src / issue_tracker / issue_tracker.go
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 public
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, string(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, string(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, string(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, string(body)))
301         }
302         return nil
303 }