Index: tools/bug_chomper/src/issue_tracker/issue_tracker.go |
diff --git a/tools/bug_chomper/src/issue_tracker/issue_tracker.go b/tools/bug_chomper/src/issue_tracker/issue_tracker.go |
new file mode 100644 |
index 0000000000000000000000000000000000000000..11f478f29f61cda57114ac26f25bb580806d2f57 |
--- /dev/null |
+++ b/tools/bug_chomper/src/issue_tracker/issue_tracker.go |
@@ -0,0 +1,272 @@ |
+// Copyright (c) 2014 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+/* |
+ Utilities for interacting with the GoogleCode issue tracker. |
+ |
+ Example usage: |
+ issueTracker := issue_tracker.MakeIssueTraker(myOAuthConfigFile) |
+ authURL := issueTracker.MakeAuthRequestURL() |
+ // Visit the authURL to obtain an authorization code. |
+ issueTracker.UpgradeCode(code) |
+ // Now issueTracker can be used to retrieve and edit issues. |
+*/ |
+package issue_tracker |
+ |
+import ( |
+ "bytes" |
+ "code.google.com/p/goauth2/oauth" |
+ "encoding/json" |
+ "errors" |
+ "fmt" |
+ "io/ioutil" |
+ "net/http" |
+ "net/url" |
+ "strconv" |
+) |
+ |
+// BugPriorities are the possible values for "Priority-*" labels for issues. |
+var BugPriorities = []string{"Critical", "High", "Medium", "Low", "Never"} |
+ |
+const apiScope = "https://www.googleapis.com/auth/projecthosting" |
+const apiURL = "https://www.googleapis.com/projecthosting/v2/projects/" |
+const issueURL = "https://code.google.com/p/skia/issues/detail?id=" |
+ |
+// Enum for determining whether a label has been added, removed, or is |
+// unchanged. |
+const ( |
+ labelNew = iota |
+ labelOld = iota |
+ labelBoth = iota |
+) |
+ |
+// loadOAuthConfig reads the OAuth given config file path and returns an |
+// appropriate oauth.Config. |
+func loadOAuthConfig(oauthConfigFile string) (*oauth.Config, error) { |
+ fileContents, err := ioutil.ReadFile(oauthConfigFile) |
+ if err != nil { |
+ 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.
|
+ } |
+ var decodedJson map[string]struct { |
+ AuthURL string `json:"auth_uri"` |
+ ClientId string `json:"client_id"` |
+ ClientSecret string `json:"client_secret"` |
+ TokenURL string `json:"token_uri"` |
+ } |
+ err = json.Unmarshal(fileContents, &decodedJson) |
+ if err != nil { |
+ return nil, err |
+ } |
+ config, ok := decodedJson["web"] |
+ if !ok { |
+ return nil, err |
+ } |
+ return &oauth.Config{ |
+ ClientId: config.ClientId, |
+ ClientSecret: config.ClientSecret, |
+ Scope: apiScope, |
+ AuthURL: config.AuthURL, |
+ TokenURL: config.TokenURL, |
+ }, nil |
+} |
+ |
+// Issue contains information about an issue. |
+type Issue struct { |
+ Id int `json:"id"` |
+ Project string `json:"projectId"` |
+ Title string `json:"title"` |
+ Labels []string `json:"labels"` |
+} |
+ |
+// URL returns the URL of a given issue. |
+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
|
+ return issueURL + strconv.Itoa(i.Id) |
+} |
+ |
+// IssueList represents a list of issues from the IssueTracker. |
+type IssueList struct { |
+ TotalResults int `json:"totalResults"` |
+ Items []*Issue `json:"items"` |
+} |
+ |
+// IssueTracker is the primary point of contact with the issue tracker, |
+// providing methods for authenticating to and interacting with it. |
+type IssueTracker struct { |
+ oauthConfig *oauth.Config |
+ oauthTransport *oauth.Transport |
+} |
+ |
+// MakeIssueTracker creates and returns an IssueTracker with authentication |
+// configuration from the given authConfigFile. |
+func MakeIssueTracker(authConfigFile string) (*IssueTracker, error) { |
+ oauthConfig, err := loadOAuthConfig(authConfigFile) |
+ if err != nil { |
+ return nil, errors.New( |
+ "Unable to read auth config file: " + err.Error()) |
+ } |
+ return &IssueTracker{ |
+ oauthConfig: oauthConfig, |
+ oauthTransport: &oauth.Transport{Config: oauthConfig}, |
+ }, nil |
+} |
+ |
+// MakeAuthRequestURL returns an authentication request URL which can be used |
+// to obtain an authorization code via user sign-in. |
+func (it *IssueTracker) MakeAuthRequestURL(redirectURL string) string { |
+ it.oauthConfig.RedirectURL = redirectURL |
+ return it.oauthConfig.AuthCodeURL(redirectURL) |
+} |
+ |
+// IsAuthenticated determines whether the IssueTracker has sufficient |
+// permissions to retrieve and edit Issues. |
+func (it *IssueTracker) IsAuthenticated() bool { |
+ return it.oauthTransport.Token != nil |
+} |
+ |
+// UpgradeCode exchanges the single-use authorization code, obtained by |
+// following the URL obtained from IssueTracker.MakeAuthRequestURL, for a |
+// multi-use, session token. This is required before IssueTracker can retrieve |
+// and edit issues. |
+func (it *IssueTracker) UpgradeCode(code string) error { |
+ token, err := it.oauthTransport.Exchange(code) |
+ if err == nil { |
+ it.oauthTransport.Token = token |
+ } |
+ return err |
+} |
+ |
+// GetBug retrieves the Issue with the given ID from the IssueTracker. |
+func (it *IssueTracker) GetBug(project string, id int) (*Issue, error) { |
+ if !it.IsAuthenticated() { |
+ return nil, errors.New("User is not authenticated!") |
+ } |
+ requestURL := apiURL + project + "/issues/" + strconv.Itoa(id) |
+ resp, err := it.oauthTransport.Client().Get(requestURL) |
+ if err != nil { |
+ return nil, err |
+ } |
+ defer resp.Body.Close() |
+ body, _ := ioutil.ReadAll(resp.Body) |
+ if resp.StatusCode != http.StatusOK { |
+ return nil, errors.New(fmt.Sprintf( |
+ "Issue tracker returned code %d:%v", |
+ resp.StatusCode, string(body))) |
+ } |
+ var issue Issue |
+ err = json.Unmarshal(body, &issue) |
+ if err != nil { |
+ return nil, err |
+ } |
+ return &issue, nil |
+} |
+ |
+// GetBugs retrieves all Issues with the given owner from the IssueTracker, |
+// returning an IssueList. |
+func (it *IssueTracker) GetBugs( |
+ project string, owner string) (*IssueList, error) { |
+ if !it.IsAuthenticated() { |
+ return nil, errors.New("User is not authenticated!") |
+ } |
+ params := map[string]string{ |
+ "owner": url.QueryEscape(owner), |
+ "can": "open", |
+ "maxResults": "9999", |
+ } |
+ requestURL := apiURL + project + "/issues?" |
+ first := true |
+ for k, v := range params { |
+ if first { |
+ first = false |
+ } else { |
+ requestURL += "&" |
+ } |
+ requestURL += k + "=" + v |
+ } |
+ resp, err := it.oauthTransport.Client().Get(requestURL) |
+ if err != nil { |
+ return nil, err |
+ } |
+ defer resp.Body.Close() |
+ body, _ := ioutil.ReadAll(resp.Body) |
+ if resp.StatusCode != http.StatusOK { |
+ return nil, errors.New(fmt.Sprintf( |
+ "Issue tracker returned code %d:%v", |
+ resp.StatusCode, string(body))) |
+ } |
+ |
+ var bugList IssueList |
+ err = json.Unmarshal(body, &bugList) |
+ if err != nil { |
+ return nil, err |
+ } |
+ return &bugList, nil |
+} |
+ |
+// SubmitIssueChanges creates a comment on the given Issue which modifies it |
+// according to the contents of the passed-in Issue struct. |
+func (it *IssueTracker) SubmitIssueChanges( |
+ issue *Issue, comment string) error { |
+ errPrefix := "Error updating issue " + strconv.Itoa(issue.Id) + ": " |
+ if !it.IsAuthenticated() { |
+ return errors.New(errPrefix + "User is not authenticated!") |
+ } |
+ oldIssue, err := it.GetBug(issue.Project, issue.Id) |
+ if err != nil { |
+ return err |
+ } |
+ postData := struct { |
+ Content string `json:"content"` |
+ Updates struct { |
+ Title *string `json:"summary"` |
+ Labels []string `json:"labels"` |
+ } `json:"updates"` |
+ }{} |
+ postData.Content = comment |
+ if issue.Title != oldIssue.Title { |
+ postData.Updates.Title = &issue.Title |
+ } |
+ // TODO(borenet): Add other issue attributes, eg. Owner. |
+ labels := make(map[string]int) |
+ for _, label := range issue.Labels { |
+ labels[label] = labelNew |
+ } |
+ for _, label := range oldIssue.Labels { |
+ if _, ok := labels[label]; ok { |
+ labels[label] = labelBoth |
+ } else { |
+ labels[label] = labelOld |
+ } |
+ } |
+ labelChanges := make([]string, 0) |
+ for labelName, present := range labels { |
+ if present == labelOld { |
+ labelChanges = append(labelChanges, "-"+labelName) |
+ } else if present == labelNew { |
+ labelChanges = append(labelChanges, labelName) |
+ } |
+ } |
+ if len(labelChanges) > 0 { |
+ postData.Updates.Labels = labelChanges |
+ } |
+ |
+ postBytes, err := json.Marshal(&postData) |
+ if err != nil { |
+ return err |
+ } |
+ requestURL := apiURL + issue.Project + "/issues/" + |
+ strconv.Itoa(issue.Id) + "/comments" |
+ resp, err := it.oauthTransport.Client().Post( |
+ requestURL, "application/json", bytes.NewReader(postBytes)) |
+ if err != nil { |
+ return err |
+ } |
+ defer resp.Body.Close() |
+ body, _ := ioutil.ReadAll(resp.Body) |
+ if resp.StatusCode != http.StatusOK { |
+ return errors.New(fmt.Sprintf( |
+ "Issue tracker returned code %d:%v", |
+ resp.StatusCode, string(body))) |
+ } |
+ return nil |
+} |