| 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..e4854f5ee6d4c27142f991eaf05babc9294f0b98
|
| --- /dev/null
|
| +++ b/tools/bug_chomper/src/issue_tracker/issue_tracker.go
|
| @@ -0,0 +1,303 @@
|
| +// 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"
|
| + "fmt"
|
| + "io/ioutil"
|
| + "net/http"
|
| + "net/url"
|
| + "strconv"
|
| + "strings"
|
| +)
|
| +
|
| +// BugPriorities are the possible values for "Priority-*" labels for issues.
|
| +var BugPriorities = []string{"Critical", "High", "Medium", "Low", "Never"}
|
| +
|
| +var apiScope = []string{
|
| + "https://www.googleapis.com/auth/projecthosting",
|
| + "https://www.googleapis.com/auth/userinfo.email",
|
| +}
|
| +
|
| +const issueApiURL = "https://www.googleapis.com/projecthosting/v2/projects/"
|
| +const issueURL = "https://code.google.com/p/skia/issues/detail?id="
|
| +const personApiURL = "https://www.googleapis.com/userinfo/v2/me"
|
| +
|
| +// Enum for determining whether a label has been added, removed, or is
|
| +// unchanged.
|
| +const (
|
| + labelAdded = iota
|
| + labelRemoved
|
| + labelUnchanged
|
| +)
|
| +
|
| +// loadOAuthConfig reads the OAuth given config file path and returns an
|
| +// appropriate oauth.Config.
|
| +func loadOAuthConfig(oauthConfigFile string) (*oauth.Config, error) {
|
| + errFmt := "failed to read OAuth config file: %s"
|
| + fileContents, err := ioutil.ReadFile(oauthConfigFile)
|
| + if err != nil {
|
| + return nil, fmt.Errorf(errFmt, err)
|
| + }
|
| + 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"`
|
| + }
|
| + if err := json.Unmarshal(fileContents, &decodedJson); err != nil {
|
| + return nil, fmt.Errorf(errFmt, err)
|
| + }
|
| + config, ok := decodedJson["web"]
|
| + if !ok {
|
| + return nil, fmt.Errorf(errFmt, err)
|
| + }
|
| + return &oauth.Config{
|
| + ClientId: config.ClientId,
|
| + ClientSecret: config.ClientSecret,
|
| + Scope: strings.Join(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 {
|
| + 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, redirectURL string) (*IssueTracker, error) {
|
| + oauthConfig, err := loadOAuthConfig(authConfigFile)
|
| + if err != nil {
|
| + return nil, fmt.Errorf(
|
| + "failed to create IssueTracker: %s", err)
|
| + }
|
| + oauthConfig.RedirectURL = redirectURL
|
| + 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() string {
|
| + // NOTE: Need to add XSRF protection if we ever want to run this on a public
|
| + // server.
|
| + return it.OAuthConfig.AuthCodeURL(it.OAuthConfig.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 nil
|
| + } else {
|
| + return fmt.Errorf(
|
| + "failed to exchange single-user auth code: %s", err)
|
| + }
|
| +}
|
| +
|
| +// GetLoggedInUser retrieves the email address of the authenticated user.
|
| +func (it IssueTracker) GetLoggedInUser() (string, error) {
|
| + errFmt := "error retrieving user email: %s"
|
| + if !it.IsAuthenticated() {
|
| + return "", fmt.Errorf(errFmt, "User is not authenticated!")
|
| + }
|
| + resp, err := it.OAuthTransport.Client().Get(personApiURL)
|
| + if err != nil {
|
| + return "", fmt.Errorf(errFmt, err)
|
| + }
|
| + defer resp.Body.Close()
|
| + body, _ := ioutil.ReadAll(resp.Body)
|
| + if resp.StatusCode != http.StatusOK {
|
| + return "", fmt.Errorf(errFmt, fmt.Sprintf(
|
| + "user data API returned code %d: %v", resp.StatusCode, string(body)))
|
| + }
|
| + userInfo := struct {
|
| + Email string `json:"email"`
|
| + }{}
|
| + if err := json.Unmarshal(body, &userInfo); err != nil {
|
| + return "", fmt.Errorf(errFmt, err)
|
| + }
|
| + return userInfo.Email, nil
|
| +}
|
| +
|
| +// GetBug retrieves the Issue with the given ID from the IssueTracker.
|
| +func (it IssueTracker) GetBug(project string, id int) (*Issue, error) {
|
| + errFmt := fmt.Sprintf("error retrieving issue %d: %s", id, "%s")
|
| + if !it.IsAuthenticated() {
|
| + return nil, fmt.Errorf(errFmt, "user is not authenticated!")
|
| + }
|
| + requestURL := issueApiURL + project + "/issues/" + strconv.Itoa(id)
|
| + resp, err := it.OAuthTransport.Client().Get(requestURL)
|
| + if err != nil {
|
| + return nil, fmt.Errorf(errFmt, err)
|
| + }
|
| + defer resp.Body.Close()
|
| + body, _ := ioutil.ReadAll(resp.Body)
|
| + if resp.StatusCode != http.StatusOK {
|
| + return nil, fmt.Errorf(errFmt, fmt.Sprintf(
|
| + "issue tracker returned code %d:%v", resp.StatusCode, string(body)))
|
| + }
|
| + var issue Issue
|
| + if err := json.Unmarshal(body, &issue); err != nil {
|
| + return nil, fmt.Errorf(errFmt, 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) {
|
| + errFmt := "error retrieving issues: %s"
|
| + if !it.IsAuthenticated() {
|
| + return nil, fmt.Errorf(errFmt, "user is not authenticated!")
|
| + }
|
| + params := map[string]string{
|
| + "owner": url.QueryEscape(owner),
|
| + "can": "open",
|
| + "maxResults": "9999",
|
| + }
|
| + requestURL := issueApiURL + 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, fmt.Errorf(errFmt, err)
|
| + }
|
| + defer resp.Body.Close()
|
| + body, _ := ioutil.ReadAll(resp.Body)
|
| + if resp.StatusCode != http.StatusOK {
|
| + return nil, fmt.Errorf(errFmt, fmt.Sprintf(
|
| + "issue tracker returned code %d:%v", resp.StatusCode, string(body)))
|
| + }
|
| +
|
| + var bugList IssueList
|
| + if err := json.Unmarshal(body, &bugList); err != nil {
|
| + return nil, fmt.Errorf(errFmt, 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 {
|
| + errFmt := "Error updating issue " + strconv.Itoa(issue.Id) + ": %s"
|
| + if !it.IsAuthenticated() {
|
| + return fmt.Errorf(errFmt, "user is not authenticated!")
|
| + }
|
| + oldIssue, err := it.GetBug(issue.Project, issue.Id)
|
| + if err != nil {
|
| + return fmt.Errorf(errFmt, err)
|
| + }
|
| + postData := struct {
|
| + Content string `json:"content"`
|
| + Updates struct {
|
| + Title *string `json:"summary"`
|
| + Labels []string `json:"labels"`
|
| + } `json:"updates"`
|
| + }{
|
| + 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] = labelAdded
|
| + }
|
| + for _, label := range oldIssue.Labels {
|
| + if _, ok := labels[label]; ok {
|
| + labels[label] = labelUnchanged
|
| + } else {
|
| + labels[label] = labelRemoved
|
| + }
|
| + }
|
| + labelChanges := make([]string, 0)
|
| + for labelName, present := range labels {
|
| + if present == labelRemoved {
|
| + labelChanges = append(labelChanges, "-"+labelName)
|
| + } else if present == labelAdded {
|
| + labelChanges = append(labelChanges, labelName)
|
| + }
|
| + }
|
| + if len(labelChanges) > 0 {
|
| + postData.Updates.Labels = labelChanges
|
| + }
|
| +
|
| + postBytes, err := json.Marshal(&postData)
|
| + if err != nil {
|
| + return fmt.Errorf(errFmt, err)
|
| + }
|
| + requestURL := issueApiURL + issue.Project + "/issues/" +
|
| + strconv.Itoa(issue.Id) + "/comments"
|
| + resp, err := it.OAuthTransport.Client().Post(
|
| + requestURL, "application/json", bytes.NewReader(postBytes))
|
| + if err != nil {
|
| + return fmt.Errorf(errFmt, err)
|
| + }
|
| + defer resp.Body.Close()
|
| + body, _ := ioutil.ReadAll(resp.Body)
|
| + if resp.StatusCode != http.StatusOK {
|
| + return fmt.Errorf(errFmt, fmt.Sprintf(
|
| + "Issue tracker returned code %d:%v", resp.StatusCode, string(body)))
|
| + }
|
| + return nil
|
| +}
|
|
|